Skip to content

Local development & contributing

Get it running

git clone <repository-url>
cd TaskTree
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.env
docker compose up

docker compose up brings up three services: PostgreSQL 16, the FastAPI backend (with auto-reload), and the Vite dev server. Then:

Tear down with docker compose down, or docker compose down -v to also drop the database volume.

View UI changes locally (no deploy)

Default to seeing UI work locally before you ever deploy. A staging deploy is a multi-minute GitHub Actions round-trip; for a colour tweak, a spacing fix, or a dark-mode check it's almost always avoidable. The Vite dev server hot-reloads on save, so a visual change is a 2-second look in the browser.

If your frontend/.env / backend/.env are the example files (which default to AUTH_PROVIDER=local-dev), a plain docker compose up already gives you a no-login, local-dev app at http://localhost:5173 — just edit and refresh.

If your local .env is configured for Entra (e.g. so you can test staging/prod), use the local-dev overlay to skip the Microsoft login without touching your Entra config:

# Terminal 1 — Postgres + backend in local-dev auth (default ports):
docker compose -f docker-compose.yml -f docker-compose.localdev.yml up postgres backend

# Terminal 2 — the fast, hot-reloading frontend in local-dev:
cd frontend && npm run dev:local          # → http://localhost:5173, pick a user, no login

# One-time: seed the thematic "Submersible ROV Mk II" demo project (40 tasks,
# mixed statuses / priorities / due dates) so views have something to show:
docker compose -f docker-compose.yml -f docker-compose.localdev.yml \
  exec backend python scripts/seed_dev_project.py --admin-email you@example.com

npm run dev:local loads frontend/.env.localdev (committed, no secrets) via Vite's --mode localdev; your default npm run dev is unchanged. To check dark mode, open the profile popover (bottom-left) and switch the theme.

Reserve staging for the final pre-merge smoke — the one place to confirm real Entra auth + real data before merging — not for every visual iteration. Batching tweaks and previewing locally is the single biggest saver of CI/Actions minutes on this project. (Pushes to main that touch only docs/markdown no longer trigger an app deploy — see deploy.yml's paths-ignore.)

Repository layout

backend/              FastAPI service, SQLAlchemy ORM, Alembic migrations, pytest
frontend/             React 18 + Vite + TypeScript, Tailwind, Vitest, Playwright
infra/                Bicep templates + deploy scripts
docs/
  user/               End-user docs        (published)
  developer/          Developer docs        (published — these pages)
  adr/                Architecture Decision Records (in-repo, not published)
  internal/           Operator runbook, install notes (in-repo, not published)
  archived/           Frozen v1 IMPLEMENTATION_PLAN.md (in-repo, not published)
docker-compose.yml    Local dev stack (+ *.localdev.yml local-dev-auth overlay, *.worktree.yml second-stack overlay)
CLAUDE.md             Conventions for AI-assisted sessions
PROJECT_STATE.md      Current focus, backlog, session notes (the living doc)
CHANGELOG.md          Keep a Changelog format

Configuration

Backend config is a pydantic Settings (backend/app/config.py) loaded from environment / .env. The knobs you'll touch in development:

Setting Default Purpose
ENVIRONMENT development guards production-only safety checks
DATABASE_URL local Postgres the connection string
AUTH_PROVIDER local-dev local-dev or entra-id (Auth)
DEV_USERS empty comma-separated dev identities; empty = a single dev user
CORS_ORIGINS the Vite dev server allowed browser origins

Secrets (the Entra client secret, the App Insights connection string, the production database password) come from the environment in deployment and from Key Vault — never committed, never in these docs. Startup refuses an unsafe combination, such as the local-dev auth provider in a production environment.

The Definition of Done

A change is not done until lint, format, typecheck, and tests all pass on the stack you touched — and tests ship in the same change as the code they verify.

Backend, from backend/:

uv run ruff check .          # lint
uv run ruff format --check . # format
uv run pytest -v             # tests (with the coverage gate)

Frontend, from frontend/:

npm run lint
npm run typecheck
npm test

pre-commit runs the linters on every commit; install it once with pre-commit install.

Conventions worth knowing

  • ADR-first. Any structural decision gets an Architecture Decision Record — Context / Decision / Consequences — written before the implementation. ADRs are append-only.
  • Sync endpoints. Handlers are def, not async def; the SSE stream is the only exception (ADR 0009).
  • Database conventions. snake_case tables and columns; UUID primary keys; always created_at, plus updated_at where mutability matters; clock_timestamp() for per-write ordering (Concurrency).
  • API conventions. REST under /api/v1/, plural resource names, {"detail": "…"} error bodies, and the public contract never changes without updating the OpenAPI spec, the tests, and the frontend consumers in the same change.
  • Type hints everywhere on the backend; strict TypeScript on the frontend.
  • Conventional commits (feat:, fix:, chore:, …).
  • The changelog gets an [Unreleased] entry in the same change as any user-visible behaviour change.
  • Don't swallow exceptions — handle them meaningfully or let them propagate.

Parallel worktrees

Features are developed in isolated git worktrees so several sessions can run in parallel over one shared .git.

# Create a worktree for issue #48 (or -Slug for work with no issue):
powershell -File scripts/new-worktree.ps1 -Issue 48
powershell -File scripts/new-worktree.ps1 -Slug kanban-board

It branches off the latest origin/main, copies the gitignored .env + local Claude config (a fresh checkout lacks these), and installs backend + frontend deps. Work happens in ../TaskTree-issue-48 (or ../TaskTree-<slug>).

Retire it after the PR merges, from the primary checkout:

powershell -File scripts/remove-worktree.ps1 -Issue 48

Run it from the primary checkout, not from inside the worktree. gh pr merge --delete-branch deletes the remote branch but can't switch/delete the local one from inside a worktree — it errors main is already checked out; that's expected, and the helper does the local cleanup (remove + prune + delete the merged branch).

Running a second worktree's stack

docker-compose.yml's host ports are parameterised and Compose isolates each worktree's containers/volumes (the project name derives from the directory), so the only things stopping two live stacks are the host ports and the entra-id auth default. The committed docker-compose.worktree.yml overlay flips both services to local-dev; pick offset ports and a distinct project name:

POSTGRES_PORT=5433 BACKEND_PORT=8001 FRONTEND_PORT=5181 \
COMPOSE_PROJECT_NAME=tasktree-wt \
docker compose -f docker-compose.yml -f docker-compose.worktree.yml up

(PowerShell: set each with $env:POSTGRES_PORT='5433' first.) The primary stack keeps the defaults — 5432 / 8000 / 5180 — with a plain docker compose up.

Where the deeper "why" lives

These pages describe the what and how at design level. For the reasoning behind a choice, follow the ADR links throughout — they're the durable record of why TaskTree is built the way it is.