Team Task Manager (TTM) is a backend-first Django SaaS project for team workspaces, projects, tasks, comments, and audit activity.
The project is intentionally built around service-oriented domain logic, selector-based reads, and centralized permission helpers instead of fat views or serializers.
- Python 3.13
- Django 5.1
- Django REST Framework
- Simple JWT
- PostgreSQL via
DATABASE_URL - Render-friendly static handling with WhiteNoise
TTM follows a strict domain architecture:
serviceshandle all state changes and business workflows.selectorshandle read/query use cases.core.permissionscontains centralized authorization helpers.- views, forms, and serializers stay thin and delegate to services/selectors.
- multi-model workflows use
transaction.atomic().
accounts: profile model and authentication pagesworkspaces: workspaces, memberships, invitationsprojects: projects inside workspacestasks: tasks, assignment, status changescomments: task comments with soft deleteactivity: append-only workspace activity logapi: DRF endpoints and JWT authenticationcore: shared permissions, slug utilities, exceptions
team_task_manager/
|-- accounts/
|-- activity/
|-- api/
|-- comments/
|-- core/
|-- projects/
|-- tasks/
|-- team_task_manager/
|-- templates/
`-- workspaces/
Important files:
team_task_manager/settings.py: project configuration,DATABASE_URL, DRF, static/mediacore/permissions.py: shared permission checkscore/slugs.py: immutable slug generation helpersworkspaces/services.py: workspace ownership and invitation workflowsprojects/services.py: project creation workflowtasks/services.py: task creation, assignment, status change workflowscomments/services.py: comment create and soft delete workflowsactivity/services.py: append-only activity writerapi/serializers.py: thin serializers delegating writes to servicesapi/views.py: DRF endpoints and workspace activity API
- Write logic lives in app services such as
workspaces/services.py,projects/services.py,tasks/services.py, andcomments/services.py. - Read logic lives in app selectors such as
workspaces/selectors.py,projects/selectors.py,tasks/selectors.py,comments/selectors.py, andactivity/selectors.py. - Permission rules live in
core/permissions.py. - HTML views and DRF serializers call those layers instead of implementing business rules directly.
- The project uses the default Django
Usermodel to keep authentication standard and avoid unnecessary custom auth complexity. - Business workflows live in app-level services so HTML views and DRF endpoints reuse the same write logic.
- Read access is implemented through selectors scoped by membership to keep multi-tenant filtering explicit and testable.
- Authorization rules are centralized in
core.permissionsso access decisions are not reimplemented across views and serializers. - Slugs are immutable after creation and enforced by scoped unique constraints at the database layer.
ActivityLogis append-only and written only from services to keep audit history consistent without introducing an event bus.- Comments use soft delete semantics in the UI and API while preserving domain-level permission checks around deletion.
- Create and activate a virtual environment.
- Install dependencies:
pip install -r requirements.txt- Copy environment settings:
copy .env.example .env- Update
DATABASE_URLandDJANGO_SECRET_KEYin.env..envis loaded automatically by Django settings, so local commands use the configured database without manual environment export. - Run migrations:
python manage.py migrate- Create a superuser:
python manage.py createsuperuser- Start the server:
python manage.py runserverThe repository includes a production-oriented Docker setup for deploying the Django app against an external PostgreSQL database.
Files:
Dockerfiledocker-compose.ymldocker/entrypoint.sh
Recommended .env values for Docker on a VPS:
DEBUG=False
DJANGO_SECRET_KEY=change-me
DATABASE_URL=postgresql://myuser:password@db-host:5432/myproject
DB_SSL_REQUIRE=False
ALLOWED_HOSTS=your-domain.com,server-ip
CSRF_TRUSTED_ORIGINS=https://your-domain.com
WEB_CONCURRENCY=3Build and start:
docker compose up --build -dThe container startup flow runs:
python manage.py migrate --noinputpython manage.py collectstatic --noinputgunicornwith a Uvicorn worker on port8000
Stop the service:
docker compose downView logs:
docker compose logs -f webFor VPS deployment, place Nginx in front of the container and proxy requests to 127.0.0.1:8000.
TTM reads the database connection from DATABASE_URL.
Example local PostgreSQL value:
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/team_task_manager
DB_SSL_REQUIRE=FalseFor Render, configure DATABASE_URL, DJANGO_SECRET_KEY, DEBUG=False, and ALLOWED_HOSTS.
For external Render Postgres connections, set DB_SSL_REQUIRE=True if your URL does not already include SSL parameters.
The repository includes render.yaml and build.sh for Render deployment.
Deployment behavior on Render:
- dependencies are installed in
build.sh - static files are collected during build
- database migrations run in
preDeployCommand - the app starts with Gunicorn and a Uvicorn worker
DEBUGdefaults toFalsewhen theRENDERenvironment variable is presentALLOWED_HOSTSandCSRF_TRUSTED_ORIGINSautomatically includeRENDER_EXTERNAL_HOSTNAME- secure proxy, HTTPS redirect, secure cookies, and HSTS are enabled on Render
Blueprint flow:
- Push the repository with
render.yaml. - In Render, create a new Blueprint from the repository.
- Render will provision:
- web service
team-task-manager - PostgreSQL database
team-task-manager-db
- web service
- After the first deploy, create an admin user from the Render Shell:
python manage.py createsuperuserManual Render service values:
- Build Command:
./build.sh - Pre-Deploy Command:
python manage.py migrate --no-input - Start Command:
python -m gunicorn team_task_manager.asgi:application -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:$PORT
//accounts/signup//accounts/login//workspaces//workspaces/create//workspaces/<slug>//workspaces/<slug>/members//workspaces/<slug>/activity//workspaces/<slug>/projects//workspaces/<workspace_slug>/projects/<project_slug>//workspaces/<workspace_slug>/projects/<project_slug>/tasks//workspaces/<workspace_slug>/projects/<project_slug>/tasks/<task_slug>//workspaces/<workspace_slug>/projects/<project_slug>/tasks/<task_slug>/edit/
Authentication:
POST /api/auth/token/POST /api/auth/token/refresh/
Resources:
GET, POST /api/workspaces/GET /api/workspaces/<slug>/GET, POST /api/projects/GET /api/workspaces/<workspace_slug>/projects/<project_slug>/GET, POST /api/tasks/GET, PATCH /api/workspaces/<workspace_slug>/projects/<project_slug>/tasks/<task_slug>/GET, POST /api/comments/DELETE /api/comments/<id>/GET /api/activity/GET /api/workspaces/<slug>/activity/
Useful query params:
/api/projects/?workspace=<workspace-slug>/api/tasks/?project=<project-slug>/api/tasks/?status=todo/api/tasks/?assignee=<user-id>/api/tasks/?ordering=-created_at/api/projects/?ordering=created_at/api/activity/?ordering=-created_at/api/comments/?task=<task-slug>
Interactive API docs:
- Swagger UI
- OpenAPI schema:
/api/schema/
API examples:
curl -X POST http://127.0.0.1:8000/api/auth/token/ \
-H "Content-Type: application/json" \
-d "{\"username\":\"owner\",\"password\":\"secret123\"}"curl http://127.0.0.1:8000/api/tasks/?project=backend\&status=todo\&ordering=-created_at \
-H "Authorization: Bearer <access-token>"curl -X PATCH http://127.0.0.1:8000/api/workspaces/engineering/projects/backend/tasks/ship-api/ \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json" \
-d "{\"description\":\"Updated from API\"}"Run the service and API tests with:
python manage.py test workspaces tasks comments api




