Home FAQ Docs Contact Sign in Get started

On this page

Tech Stack

LayerTechnologyNotes
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

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.

ModelDescriptionKey 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.

NamespaceMethodsDescription
/api/v1/authPOST, GETLogin, refresh token, get current user, switch firm
/api/v1/dashboardGETDashboard stats (members, clients, engagements, bench)
/api/v1/team-membersGET, POST, PATCH, DELETETeam member CRUD with approval checks
/api/v1/clientsGET, POST, PATCH, DELETEClient CRUD with approval checks
/api/v1/engagementsGET, POST, PATCH, DELETEEngagement CRUD with approval checks
/api/v1/assignmentsGET, POST, PATCHAssignment CRUD with allocation validation
/api/v1/leavesGET, POST, PATCHLeave CRUD with approval checks
/api/v1/approval-requestsGET, POSTList, approve, reject pending approval requests
/api/v1/extensionsGET, POSTExtension requests (auto-creates assignment on approve)

Full endpoint documentation with request/response schemas is available in the developer dashboard (internal only).

Authentication

MethodEndpointDescription
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

MethodEndpointDescription
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

MethodEndpointDescription
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

MethodEndpointDescription
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

MethodEndpointDescription
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

MethodEndpointDescription
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

MethodEndpointDescription
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.

VariableRequiredDescription
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

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.