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:
- Frontend — http://localhost:5173
- Backend health — http://localhost:8000/health
- API docs (Swagger) — http://localhost:8000/docs
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, notasync def; the SSE stream is the only exception (ADR 0009). - Database conventions.
snake_casetables and columns; UUID primary keys; alwayscreated_at, plusupdated_atwhere 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.