Skip to content

Repository files navigation

TMS

Team-based Task Management API

A multi-tenant REST API for teams, projects, tasks, and activity tracking — built on Django.


Python Django DRF PostgreSQL Redis Docker License: MIT


Features · Architecture · Roles & Permissions · API · Quickstart · Frontend · Deployment · Roadmap


Features

Multi-tenant teams Every resource lives under a team; querysets are scoped through the URL hierarchy, so cross-tenant access resolves to a 404 at the ORM level
JWT authentication Short-lived access tokens (15 min) with rotating refresh tokens, blacklist-on-rotation, and full token revocation on password change
Role-based access control Four roles (ownermaintainermemberviewer) mapped to fine-grained scopes such as task:assign and team:invite
Invitation workflow Email-addressed invites with an explicit state machine (pending → accepted / rejected / cancelled), a per-user inbox, and 3-day expiry; membership is created atomically on accept
Projects, tasks & comments Nested resources with status, priority, due dates, multi-assignee support, filtering, search, and ordering
Activity logs An append-only, per-team audit feed recorded transactionally alongside every mutation
Background jobs Celery workers deliver invitation, task-assignment, and status-change emails asynchronously — dispatched only after the transaction commits, with exponential-backoff retries
Database-enforced invariants Unique and check constraints back every application-level rule
OpenAPI documentation Interactive Swagger and ReDoc docs generated by drf-spectacular
Rate limiting 60 req/min anonymous, 120 req/min authenticated, backed by Redis
Production deployment Multi-stage uv Docker build, non-root runtime, gunicorn behind nginx with TLS

Architecture

Client ──▶ nginx (TLS, SPA + static) ──▶ gunicorn / Django ──▶ PostgreSQL 17 (pooled)
                                        │
                                        └──▶ Redis 7 (cache, throttling, Celery broker)
                                                  │
                                                  └──▶ Celery worker & beat ──▶ email (Resend)

Domain model

erDiagram
    CustomUser ||--o{ Membership : "belongs via"
    Team       ||--o{ Membership : "has"
    Team       ||--o{ Project    : "contains"
    Team       ||--o{ Invitation : "issues"
    Team       ||--o{ Log        : "records"
    Project    ||--o{ Task       : "contains"
    Task       ||--o{ Comment    : "has"
    Task       }o--o{ CustomUser : "assignees"
    CustomUser ||--o{ Invitation : "receives"
Loading

Every model inherits a shared BaseModel providing UUIDv7 primary keys — time-ordered and non-enumerable — plus created_at / updated_at timestamps.

Project layout

TMS/
├── base/        # Shared BaseModel (UUIDv7 + timestamps), health endpoint
├── users/       # Custom user model, registration, JWT auth, password change
├── teams/       # Teams, memberships, roles, invitations, RBAC scopes
├── projects/    # Team-scoped projects
├── tasks/       # Project-scoped tasks & comments
├── logs/        # Per-team append-only activity feed
├── cfg/         # Settings, root URLconf, WSGI/ASGI
├── frontend/    # React SPA (Vite + Bun); dist/ is served by nginx
├── docker/      # Dockerfile + dev/prod compose stacks
└── nginx/       # Reverse-proxy configs (dev & TLS-terminated prod)

Roles & Permissions

Roles are ordered supersets — each role inherits everything below it.

Scope Viewer Member Maintainer Owner
team:view · task:view · task:comment
task:create
team:invite
project:create · project:edit · project:delete
task:assign · task:update · task:delete
team:update · team:delete · team:change_roles · team:remove

A team always retains at least one owner — the API refuses the last owner's departure or demotion.

Object-level rules refine the matrix where it makes sense: task assignees may update their own tasks, comment authors manage their own comments, and invite receivers accept or reject their own invitations.


API Surface

All endpoints are versioned under /api/v1/ and paginated (20 per page).

Authentication
Method Endpoint Description
POST /auth/register/ Create an account
POST /auth/login/ Obtain access + refresh token pair
POST /auth/refresh/ Rotate the refresh token
POST /auth/logout/ Blacklist the refresh token
GET PATCH /auth/me/ Retrieve / update the current user
GET /auth/invites/ Invitations addressed to the current user
PUT PATCH /auth/password/ Change password (revokes outstanding refresh tokens)
Teams
Method Endpoint Description
GET POST /teams/ List / create teams
GET PUT PATCH DELETE /teams/{team}/ Manage a team
GET /teams/{team}/members/ List memberships
GET PATCH DELETE /teams/{team}/members/{membership}/ Change role · remove · leave
GET POST /teams/{team}/invites/ List / send invitations (receiver addressed by email)
GET PATCH /teams/{team}/invites/{invite}/ Accept · reject · cancel
GET /teams/{team}/logs/ Team activity feed
Projects, Tasks & Comments
Method Endpoint Description
GET POST /teams/{team}/projects/ List / create projects (searchable)
GET PUT PATCH DELETE /teams/{team}/projects/{project}/ Manage a project
GET POST .../projects/{project}/tasks/ List / create tasks — filter by status & priority, search by title, order by created_at / due
GET PUT PATCH DELETE .../tasks/{task}/ Manage a task
GET POST .../tasks/{task}/comments/ List / add comments
GET PATCH DELETE .../comments/{comment}/ Manage a comment

Interactive documentation

Swagger UI /api/v1/docs/
ReDoc /api/v1/redoc/
OpenAPI schema /api/v1/schema/
Health check /health/ — reports database & cache connectivity with latency

Quickstart

Prerequisites: Docker & Docker Compose, plus Bun 1.3+ to build the frontend.

# 1. Clone
git clone <repo-url> && cd TMS

# 2. Configure
cp .env.example .env.dev        # edit values as needed

# 3. Build the frontend — nginx serves frontend/dist directly
cd frontend && bun install && bun run build && cd ..

# 4. Run — Postgres, Redis, Django (auto-migrate) & nginx
docker compose -f docker/compose.dev.yaml up --build

The full app is now live at http://localhost:8000/ — nginx serves the React SPA from frontend/dist and proxies /api, /admin, and /health to Django. Backend source is bind-mounted, so the dev server hot-reloads; frontend changes need a re-run of bun run build.

Note: build the frontend before compose up. The nginx service bind-mounts frontend/dist; if it doesn't exist yet, Docker creates it as an empty root-owned directory and the SPA serves 403s.

Running natively (without Docker)

Requires Python 3.13+, uv, and reachable PostgreSQL + Redis instances.

uv sync                          # install dependencies
cp .env.example .env             # point POSTGRES_* / REDIS_URL at your services
set -a && source .env && set +a  # settings read plain environment variables — no dotenv loader
uv run manage.py migrate
uv run manage.py runserver
Try it
# Register
curl -X POST http://localhost:8000/api/v1/auth/register/ \
  -H 'Content-Type: application/json' \
  -d '{"username": "ada", "email": "ada@example.com", "password": "correct-horse-battery"}'

# Login → grab the access token
curl -X POST http://localhost:8000/api/v1/auth/login/ \
  -H 'Content-Type: application/json' \
  -d '{"username": "ada", "password": "correct-horse-battery"}'

# Create a team (creator becomes owner automatically)
curl -X POST http://localhost:8000/api/v1/teams/ \
  -H "Authorization: Bearer $ACCESS" \
  -H 'Content-Type: application/json' \
  -d '{"name": "Analytical Engines"}'

Frontend

A single-page React app (React 19 + Vite + react-router-dom) lives in frontend/. It's a Linear-inspired, Discord-flavored dark UI covering the full API surface — teams, projects, tasks, comments, members, invitations, and the activity feed.

There are two ways to run it locally:

1. Served by nginx (the Quickstart default). Build once, and the compose stack serves the SPA and the API from the same origin:

cd frontend && bun install && bun run build && cd ..
docker compose -f docker/compose.dev.yaml up

2. Vite dev server (hot-reload, for active frontend work). Run the API on port 8000 first (see Quickstart), then:

cd frontend
bun install                      # install dependencies
bun run dev                      # Vite dev server → http://localhost:5173

Vite proxies /apihttp://localhost:8000, so no CORS configuration is required in development. Point at a different API by setting VITE_API_URL (defaults to /api/v1).

Command Description
bun run dev Start the Vite dev server with hot-reload on port 5173
bun run build Produce an optimized production build in frontend/dist/
bun run preview Serve the production build locally

Stack: React 19, Vite, react-router-dom v7, lucide-react icons, and @fontsource/inter — no CSS framework; styling is hand-authored design tokens in src/styles/. JWT access/refresh tokens are held in localStorage with transparent refresh-on-401.


Deployment

The production stack (docker/compose.prod.yaml) runs gunicorn (3 workers) behind nginx with TLS via Let's Encrypt certificates mounted from the host, alongside dedicated Celery worker and beat containers for background jobs.

cp .env.example .env.prod        # set DEBUG=False, real secrets, hosts & origins
export DOMAIN=your.domain.example
cd frontend && bun install --frozen-lockfile && bun run build && cd ..
docker compose -f docker/compose.prod.yaml up --build -d

The stack provides:

  • HTTP → HTTPS redirect, TLS 1.2/1.3 only, HTTP/2
  • The React SPA served by nginx from frontend/dist (built on the host, bind-mounted read-only), with hashed assets cached immutably and index.html always revalidated
  • Multi-stage image with uv-locked dependencies, running as a non-root user
  • Migrations and collectstatic on boot; static files served by nginx with one-year immutable caching and gzip
  • Health-gated startup for Postgres and Redis; /health/ endpoint for external monitors
  • Secure cookies and proxy-aware SSL redirect whenever DEBUG=False

Roadmap

  • Extend row-level locking to the role-change path (member removal already locks the team row)
  • Fuzzy task search with Postgres pg_trgm
  • Response caching for hot read endpoints
  • Real-time task updates over WebSockets (Django Channels)
  • Google OAuth sign-in
  • Automated API test suite covering the RBAC permission matrix

Design Rationale

Curious why UUIDv7 keys, code-defined RBAC, or snapshot-style activity logs? See ENGINEERING_DECISIONS.md — every non-obvious choice is documented as a mini-ADR.


Released under the MIT License · © 2026 Sreyash Satpathy