Skip to content

Developer onboarding guide

Welcome. This guide gets a new dev from a fresh clone to a running app, the CMS open in a browser, and a first PR. The backend is a single Django + Wagtail service; the Architecture doc covers the bigger picture.

Active build. The repo has content models (Passage, Book, and Writing, with letters/essays/chapters unified behind one kind field), plus taxonomy, a custom User model, Firebase auth, and infrastructure (Docker, AWS CDK). Mobile API endpoints are landing incrementally. API framework is DRF (ADR-0004), auth is Firebase (ADR-0003).

Prerequisites

Install these before starting:

Tool Install (macOS) Install (Linux) Purpose
uv brew install uv curl -LsSf https://astral.sh/uv/install.sh \| sh Python deps + venv + Python toolchain (installs 3.12+ for you)
Docker + Compose Docker Desktop your distro's docker + docker-compose-plugin Local Postgres 18 + Redis 8
git preinstalled / brew install git apt install git Version control

Python itself is not a prerequisite. uv provisions the interpreter pinned by requires-python (>=3.12). Node/AWS CDK are only needed for infrastructure work (see deployment.md), not day-to-day app development.

Verify:

uv --version
docker --version && docker compose version

Quick start (10 minutes)

# 1. Clone the repo
git clone git@github.com:Fueled/cs-lewis-backend.git
cd cs-lewis-backend

# 2. First-time setup: .env + deps + migrate + seed
make bootstrap

# 3. Start the server
make dev                       # http://localhost:8000

# 4. Verify it's alive
curl localhost:8000/-/alive/   # liveness (bare 200)
curl localhost:8000/-/health/  # DB + cache checks

make bootstrap runs make env install hooks migrate seed. It creates .env from the example, installs deps into a project-local .venv/, installs git hooks (lefthook), applies migrations, and seeds the database (sample dev users). Every step is idempotent, so re-running is safe.

Log in to the CMS:

User Password Role
admin@cslewis.local admin123 Superuser
editor@cslewis.local editor123 Staff (no superuser)

With no DATABASE_URL / REDIS_URL set, the app falls back to sqlite + local-memory cache, so no services are needed. For Postgres/Redis parity with prod, use Docker Compose:

docker compose up --build          # web on :8000, Postgres :5432, Redis :6379

Compose runs migrate on start and serves via granian (the production server).

The admin lives at /studio/ and /backstage/, not /cms/ and /admin/. Those prefixes are env-configurable (WAGTAIL_ADMIN_URL, DJANGO_ADMIN_URL) so the login pages aren't the routes bots scan. On the deployed dev environment these are https://dev-cslewis.fueled.engineering/studio/ (Wagtail CMS) and https://dev-cslewis.fueled.engineering/backstage/ (Django admin). See API Overview → Environments.

Environment variables

Local values live in .env (copied from .env.example); nothing sensitive is committed. For the full variable list, defaults, and Firebase setup, see Environment variables in the reference docs.

Services

Local services provisioned by compose.yml:

Service Port Image Purpose
web 8000 built from .aws/docker/Dockerfile-aws Django + Wagtail via granian
db 5432 postgres:18 Application database
redis 6379 redis:8-alpine Cache + (future) Celery broker

Project architecture

A single Python service (Django + Wagtail) serves the editorial CMS and, once built, the mobile API from one codebase. It runs on AWS ECS (EC2), backed by Aurora Serverless v2 (Postgres), ElastiCache Redis, and S3/CloudFront. See Architecture, ADR-0001 (app stack) and ADR-0002 (IaC + compute).

Key rules

  1. Config comes from the environment (12-factor). No secrets in code; real secrets live in AWS Secrets Manager.
  2. Settings are layered: shared config in config/settings/base.py, overridden by local.py (local dev) / prod.py (all AWS envs, dev and prod) / test.py. Add new settings to base.py unless they're env-specific.
  3. API framework is DRF; auth is Firebase. Follow the application style in ADR-0004: function-based views, thin serializers, business logic in services.
  4. Migrations run as a one-off task in AWS, not on container start. Don't add migrate to the image CMD. (Compose does it locally because it's single-instance.)

Directory map

Path Purpose
src/ Django project root (manage.py lives here, run commands from src/)
src/data/ Single Django app: all models + migrations
src/readers/ Read-only business logic (plain Python)
src/actions/ State-changing business logic (plain Python)
src/interfaces/api/ DRF views + URLs
src/interfaces/cms/ Django admin + Wagtail admin (forms, viewsets, templates)
src/interfaces/cli/ Management commands (seed, seed_tags, seed_related, seed_writings, sync_themes, import_book, import_articles)
src/tests/ All tests
src/config/settings/ Layered settings: base, local, prod, test
src/config/urls.py Root URL config (admin prefixes from settings)
.aws/iac/ AWS CDK (TypeScript), shared/network/compute/storage stacks
.aws/docker/ Dockerfile-aws (app image), Dockerfile-seed
.aws/codebuild/ Buildspecs + ECS task-def template
.github/workflows/ CI/CD (deploy to dev/prod, docs)
docs/ Reader-facing docs (this site)
.context/ Internal working memory / decision wiki
compose.yml Local Postgres + Redis + web

Common commands

make help lists all targets. Most common:

Command Does
make bootstrap First-time setup: .env + deps + git hooks + migrate + seed (sample dev users)
make hooks (Re-)install git hooks (lefthook), included in bootstrap
make dev Run the dev server
make migrate / make makemigrations Database migrations
make seed Seed sample dev users (idempotent)
uv run python src/manage.py seed_writings [--publish] Seed sample Writings (letters/essays/chapters); --publish makes them live + What's New–eligible
uv run python src/manage.py import_book --book … --excerpts … Import a processed book → Book + draft Passages
make createsuperuser Create an admin user (prompts for email)
make shell Django shell
make test Run the test suite (pytest)
make lint / make format Ruff lint / auto-format
make check Django system checks
make services / make services-down Start/stop local Postgres + Redis (Docker)
make docs-serve Preview docs site (hot-reload)
make docs-check Strict docs build, fails on broken links/nav

For lower-level or one-off commands:

uv add <package>                          # add a runtime dep (updates pyproject + lock)
uv run python src/manage.py shell_plus    # django-extensions enhanced shell
docker compose up --build                 # full stack: web + Postgres + Redis
docker compose down                       # add -v to wipe the pgdata volume
cd .aws/iac && just <recipe>              # infrastructure, see docs/guides/deployment.md

Add a feature

The codebase follows Django RAPID Architecture. It uses horizontal layers, not vertical apps. Read the django skill in .agents/skills/django/SKILL.md for the full convention. The shape:

  • Models go in src/data/models/ (single Django app, no startapp).
  • Read-only business logic goes in src/readers/.
  • State-changing logic goes in src/actions/.
  • API views go in src/interfaces/api/.
  • Admin customization (Django + Wagtail) goes in src/interfaces/cms/.
  • Management commands go in src/interfaces/cli/management/commands/.
  • Content types are Wagtail Snippets, not Pages. The CMS is headless, so the mobile app reads content through the API, never Wagtail page routing (ADR-0008). Build new content models as snippets with the feature mixins (draft/live, revisions) like Passage/Writing/Book; run makemigrations + migrate.
  • The mobile API uses DRF with function-based views and drf-spectacular for OpenAPI (see ADR-0004).
  • Keep swappable services (e.g. the tag Ranker) behind an interface so the Postgres → Meilisearch swap stays mechanical.

CI/CD pipeline

GitHub Actions → OIDC role → CodeBuild → ECR → ECS. PRs get both a lint gate and a test gate (ci.yml, two parallel jobs).

Trigger Workflow Effect
Pull request (any branch) ci.ymllint lefthook run pre-commit --all-files: ruff check/format, docs strict-build, IaC tsc --noEmit. Fails if anything would need fixing
Pull request (any branch) ci.ymltest manage.py check + migrations check + pytest against a real Postgres service container
Push to main (touching src/, .aws/, pyproject.toml) dev-aws-backend.yml Deploy to dev (dev-cslewis.fueled.engineering)
Push a v*.*.* tag prod-aws-deploy.yml Deploy to prod
Push to main (touching docs/, zensical.toml) docs.yml Rebuild the docs site

Git workflow

  • Branch from main
  • Conventional commits: feat:, fix:, chore:, refactor:, docs: (lowercase scope, imperative subject ≤72 chars, no trailing period)
  • Open a PR against main; follow .github/PULL_REQUEST_TEMPLATE.md
  • No Co-Authored-By or AI watermarks in commits
  • Git hooks (lefthook, installed by make bootstrap/make hooks) run ruff check+format on staged src/**/*.py at commit time and autofix/restage. The same checks re-run in CI (ci.ymllint) on the full repo. See lefthook.yml at the project root

Testing

Tests use pytest + pytest-django. Locally, config/settings/test.py defaults to an in-memory sqlite database, so there is zero setup. CI sets DATABASE_URL to a real Postgres service container instead (ci.ymltest), since sqlite doesn't exercise Postgres-specific behaviour (GIN indexes, etc.). To match CI locally:

make test              # or: uv run pytest, sqlite, zero setup

# against real Postgres (mirrors CI):
make services           # starts the compose.yml db
DATABASE_URL=postgres://cslewis:cslewis@localhost:5432/cslewis uv run pytest

Tests live in src/tests/, not inside each layer.

Common gotchas

  1. Run manage.py from src/. That's where it lives, and it puts config on the path.
  2. Zero-setup local uses sqlite + locmem. Fine for runserver, but use docker compose up when you need Postgres/Redis behaviour (GIN indexes, real cache).

The admin URLs, the DRF + Firebase stack, and the AWS migration task are covered above in Quick start and Key rules.

Onboarding checklist

  • Complete the Quick start steps (app running, health check passing)
  • Log into the CMS at /studio/ with a superuser
  • Read API Overview and Architecture
  • Read AGENT_GUIDE.md at the project root
  • Run the linter (uv run ruff check src) and verify it passes
  • Pick up your first ticket and open a PR
  • Ask your onboarding buddy if anything is unclear

Contacts

  • Team Lead: Saurabh Kumar
  • Slack: #cs-lewis (general), #cs-lewis-engineering (engineering)
  • Tracker: Linear, team CSL