Tech Stack
| Layer | Technology | Notes |
|---|---|---|
| Backend Framework | FastAPI (Python 3.11+) | Async-first, type-safe |
| Templating | Jinja2 | Server-side rendered HTML pages |
| ORM | SQLAlchemy 2.0 | Async-compatible, declarative models |
| Database | PostgreSQL (Neon) | Serverless Postgres in production |
| Migrations | Alembic | Schema versioning and migrations |
| CSS Framework | Tailwind CSS | Utility-first styling |
| UI Components | Franken UI | Component library built on UIkit + Tailwind |
| Authentication | Session cookies + JWT + OAuth (Authlib) | Multi-auth support |
| Hosting | Render | Web service + Supabase PostgreSQL |
Architecture
splanly follows a server-side rendered (SSR) architecture with FastAPI serving Jinja2 templates for the main application. A separate /api/v1 namespace provides RESTful JSON endpoints designed for consumption by a Next.js frontend (or any SPA/mobile client).
Request Flow
Browser / SPA
│
├─ SSR Pages ──→ FastAPI Routers ──→ Jinja2 Templates ──→ HTML
│
└─ API v1 ────→ FastAPI API Routers ──→ JSON Responses
│
├──→ Services (business logic)
│
└──→ SQLAlchemy ORM ──→ PostgreSQL
Key Design Decisions
- Multi-tenant by design — Firm-owned data is scoped to a
firm_id. PostgreSQL RLS is enabled on public tables and denies unconfigured PostgREST roles by default. - SSR first, API second — The primary interface is server-rendered HTML. The API layer (
/api/v1) exists for the planned Next.js frontend migration. - Middleware-driven auth — A session middleware (
app/middleware/) handles authentication state across both SSR and API requests. - Approval workflows — Built-in approval system for leave requests, engagement extensions, and other firm-defined actions.
Project Structure
pm-tool/
├── app/
│ ├── api/
│ │ └── v1/ # REST API endpoints (JSON)
│ │ ├── auth.py # Auth endpoints (JWT, OAuth)
│ │ ├── clients.py # Client CRUD
│ │ ├── engagements.py # Engagement management
│ │ ├── assignments.py # Assignment operations
│ │ ├── team_members.py # Team member management
│ │ ├── leaves.py # Leave management
│ │ ├── dashboard.py # Dashboard data endpoints
│ │ ├── approval.py # Approval workflow endpoints
│ │ ├── extensions.py # Engagement extension requests
│ │ └── deps.py # Shared dependencies
│ ├── routers/ # SSR page routes (Jinja2)
│ │ ├── auth.py, dashboard.py, clients.py
│ │ ├── engagements.py, assignments.py
│ │ ├── team_members.py, leaves.py
│ │ ├── invitations.py, reports.py
│ │ ├── license.py, admin_settings.py
│ │ └── contact.py, health.py, outbox.py
│ ├── models/ # SQLAlchemy models
│ │ ├── models.py # Core models
│ │ ├── invitation.py # Invitation model
│ │ ├── super_admin.py # Super admin model
│ │ └── firm_business_role.py# Business role model
│ ├── schemas/ # Pydantic validation schemas
│ ├── services/ # Business logic layer
│ ├── middleware/ # Auth middleware, request context
│ ├── templates/ # Jinja2 HTML templates
│ │ ├── auth/, dashboard/, clients/
│ │ ├── engagements/, team_members/
│ │ ├── legal/ # Legal pages (this file's siblings)
│ │ └── docs.html, faq.html
│ ├── static/ # CSS, JS, images
│ ├── main.py # FastAPI app entry point
│ ├── config.py # Settings (pydantic-settings)
│ ├── database.py # SQLAlchemy engine & session
│ ├── csrf.py, csrf_utils.py # CSRF protection
│ ├── email_worker.py # Email sending service
│ ├── flash.py # Flash message helpers
│ └── exceptions.py # Custom exception handlers
├── alembic/ # Database migrations
├── tests/ # Pytest test suite
├── requirements.txt
├── run_server.py # Dev server runner
├── alembic.ini
├── render.yaml # Render deployment config
└── .env.example # Environment variable template
Data Models
All models inherit from SQLAlchemy's declarative Base and are defined in app/models/models.py (plus supplementary files). Every business entity is scoped to a firm for multi-tenancy.
| Model | Description | Key Relationships |
|---|---|---|
User |
Individual user account with email, hashed password, and OAuth fields | → FirmUser → Firm |
Firm |
A CA firm (tenant). Contains name, license details, and plan info | → FirmUsers, Branches, Clients, TeamMembers |
FirmUser |
Junction table linking Users to Firms with role (admin, manager, member) | User ↔ Firm, Role |
Branch |
Office branch within a firm (e.g., Chennai, Mumbai) | → Firm |
TeamMember |
A staff member within the firm, with designation, skills, and availability | → Firm, Branch, Assignments, Leaves |
Client |
A client of the CA firm | → Firm, Engagements |
Engagement |
A service engagement (audit, tax filing, etc.) for a client | → Client, Firm, Assignments, EngagementInstances |
EngagementInstance |
A recurring instance of an engagement (e.g., FY 2025-26 audit) | → Engagement |
Assignment |
Links a TeamMember to an EngagementInstance with dates and hours | → TeamMember, EngagementInstance |
Leave |
Leave request/record for a team member | → TeamMember, ApprovalRequest |
ApprovalRule |
Configurable approval workflow rules per firm | → Firm |
ApprovalRequest |
Pending/approved/rejected approval for a workflow action | → ApprovalRule, FirmUser |
Invitation |
Firm invitation to onboard a new user | → Firm |
FirmBusinessRole |
Custom business roles defined within a firm (e.g., Senior Associate) | → Firm |
SuperAdmin |
Platform-level admin accounts (SkilledCA team) | — |
SystemSetting |
Key-value configuration settings per firm or global | → Firm (optional) |
EmailOutbox |
Queued outbound emails for reliable delivery | — |
API v1 Endpoints
The /api/v1 namespace provides JSON REST endpoints for the planned Next.js frontend. All endpoints require JWT authentication unless noted.
| Namespace | Methods | Description |
|---|---|---|
/api/v1/auth | POST, GET | Login, refresh token, get current user, switch firm |
/api/v1/dashboard | GET | Dashboard stats (members, clients, engagements, bench) |
/api/v1/team-members | GET, POST, PATCH, DELETE | Team member CRUD with approval checks |
/api/v1/clients | GET, POST, PATCH, DELETE | Client CRUD with approval checks |
/api/v1/engagements | GET, POST, PATCH, DELETE | Engagement CRUD with approval checks |
/api/v1/assignments | GET, POST, PATCH | Assignment CRUD with allocation validation |
/api/v1/leaves | GET, POST, PATCH | Leave CRUD with approval checks |
/api/v1/approval-requests | GET, POST | List, approve, reject pending approval requests |
/api/v1/extensions | GET, POST | Extension requests (auto-creates assignment on approve) |
Full endpoint documentation with request/response schemas is available in the developer dashboard (internal only).
Authentication
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/clients |
List all clients for the firm |
| POST | /api/v1/clients |
Create a new client |
| GET | /api/v1/clients/{id} |
Get client details |
| PUT | /api/v1/clients/{id} |
Update client |
| DELETE | /api/v1/clients/{id} |
Delete client |
Engagements /api/v1/engagements
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/engagements |
List engagements (filterable by client, status) |
| POST | /api/v1/engagements |
Create a new engagement |
| GET | /api/v1/engagements/{id} |
Get engagement details with instances |
| PUT | /api/v1/engagements/{id} |
Update engagement |
| DELETE | /api/v1/engagements/{id} |
Delete engagement |
Assignments /api/v1/assignments
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/assignments |
List assignments (filterable by member, engagement, date) |
| POST | /api/v1/assignments |
Create a new assignment |
| GET | /api/v1/assignments/{id} |
Get assignment details |
| PUT | /api/v1/assignments/{id} |
Update assignment |
| DELETE | /api/v1/assignments/{id} |
Delete assignment |
Team Members /api/v1/team-members
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/team-members |
List all team members for the firm |
| POST | /api/v1/team-members |
Add a new team member |
| GET | /api/v1/team-members/{id} |
Get team member details with assignments |
| PUT | /api/v1/team-members/{id} |
Update team member |
| DELETE | /api/v1/team-members/{id} |
Remove team member |
Leaves /api/v1/leaves
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/leaves |
List leave records (filterable by member, status, date range) |
| POST | /api/v1/leaves |
Submit a leave request |
| PATCH | /api/v1/leaves/{id}/approve |
Approve a leave request |
| PATCH | /api/v1/leaves/{id}/reject |
Reject a leave request |
| DELETE | /api/v1/leaves/{id} |
Cancel/delete a leave record |
Dashboard /api/v1/dashboard
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/dashboard/summary |
Dashboard summary (team count, active engagements, etc.) |
| GET | /api/v1/dashboard/workload |
Team workload overview and utilization |
Approvals /api/v1/approvals
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/approvals |
List pending approval requests |
| PATCH | /api/v1/approvals/{id} |
Approve or reject a request |
Authentication
splanly supports three authentication methods, all accessible through the SSR and API layers:
1. Session-Based (SSR)
Default for server-rendered pages. Upon login, a signed session cookie is set using itsdangerous. The session stores the user_id and firm_id, validated by middleware on every request.
Cookie: staffplan_session=<signed-token>
2. JWT (API)
For API v1 endpoints, authentication uses Bearer tokens generated via PyJWT. Tokens are issued on login and include user_id, firm_id, and an expiry claim.
Authorization: Bearer <jwt-token>
3. OAuth 2.0 (Google)
Users can sign in with Google using Authlib. The flow is handled at /api/v1/auth/oauth/google, which redirects to Google and processes the callback to create or link a user account.
CSRF Protection: All state-changing requests (POST, PUT, DELETE) on SSR routes require a valid CSRF token, enforced via fastapi-csrf-protect. API endpoints using Bearer JWT are exempt from CSRF checks.
Deployment
splanly is deployed on Render with a Neon PostgreSQL database.
Render Configuration
The render.yaml blueprint configures the web service:
services:
- type: web
name: staffplan
env: python
buildCommand: pip install -r requirements.txt
startCommand: alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port $PORT
Migrations run automatically on every deploy (alembic upgrade head) before the server starts.
Database
Neon provides a serverless PostgreSQL instance. The DATABASE_URL is injected from Render's linked database resource. Connection pooling is handled at the Neon level.
CI/CD
Render auto-deploys on push to the main branch. Pre-deploy, Alembic runs any pending migrations. There is no separate CI pipeline configured — tests can be run locally with pytest.
Environment Variables
Configuration is managed through environment variables, loaded by pydantic-settings in app/config.py. See .env.example for the full template.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string (e.g., postgresql+psycopg2://...) |
SECRET_KEY |
Yes | Secret for signing sessions and CSRF tokens. Auto-generated on Render. |
ENV |
No | Environment: development, production, or test. Defaults to development. |
SESSION_COOKIE_NAME |
No | Custom session cookie name. Defaults to staffplan_session. |
SMTP_HOST |
No | SMTP server for sending transactional emails. |
SMTP_PORT |
No | SMTP port (e.g., 587). |
SMTP_USER |
No | SMTP authentication username. |
SMTP_PASSWORD |
No | SMTP authentication password. |
GOOGLE_CLIENT_ID |
No | Google OAuth client ID for SSO. |
GOOGLE_CLIENT_SECRET |
No | Google OAuth client secret. |
JWT_SECRET_KEY |
No | Secret for signing JWTs. Falls back to SECRET_KEY if not set. |
JWT_ALGORITHM |
No | JWT algorithm. Defaults to HS256. |
Local Development
Get splanly running locally in a few steps:
Prerequisites
- Python 3.11+
- PostgreSQL (local or remote)
pipor a virtual environment manager
Setup
# Clone the repository
git clone <repo-url>
cd pm-tool
# Create and activate virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
venv\Scripts\activate # Windows
# Install dependencies
pip install -r requirements.txt
# Copy environment template and configure
cp .env.example .env
# Edit .env with your DATABASE_URL and SECRET_KEY
# Run database migrations
alembic upgrade head
# Start the development server
python run_server.py
The server starts at http://localhost:8000. API documentation is available via the developer dashboard.
Running Tests
# Run all tests
pytest
# Run with verbose output
pytest -v
# Run specific test file
pytest tests/test_auth.py
Questions or issues? Reach out to samarth@skilledca.in or refer to the README.md and CONTEXT.md files in the repository root for additional context.