Skip to content

Local development

Terminal window
docker compose -f docker-compose.dev.yml up --build
# frontend → http://localhost:3000
# backend → http://localhost:8000 (interactive API docs at /docs)

Source is mounted and both sides hot-reload. The default docker compose up (no -f) is the production stack — use the dev file for iteration.

Terminal window
# Backend — host Python is often externally-managed, so use a venv
cd backend
python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/alembic upgrade head # against a reachable Postgres
.venv/bin/uvicorn main:app --reload
# Frontend — requires Node 20+
cd frontend
npm install
npm run dev

The test suite (and the repo’s own tooling) can run the backend against SQLite with no infrastructure at all; point DATABASE_URL at Postgres when you want parity with production.

Run what CI runs — all four must pass:

Terminal window
cd backend && .venv/bin/pytest # backend tests (SQLite, no infra)
cd frontend && npm run test # vitest
cd frontend && npx tsc --noEmit # type-check
cd frontend && npx eslint . # lint

If the change is user-visible, run it in a browser too — tests alone don’t prove UI behaviour.

These are architectural rules, not preferences — some are enforced by ESLint:

  1. Every mutation emits an event using the catalogued vocabulary — add the event type to the catalogue before emitting it.
  2. Every tenant-scoped query is scoped by competition_id at the data-access layer.
  3. Permission checks go through require_permission — never an inline role check. Missing permission? Add it to the catalogue first.
  4. One hook module per frontend domain — components never import the API client directly (ESLint-enforced).
  5. Colours and spacing come from design tokens — no raw hex in components (ESLint-enforced, brand mark excepted).
  6. New backend features register through the module loader — see Developing modules.
  7. Every new frontend page/surface is “born extracted” — its user-facing strings go into frontend/messages/en.json via t() from the first commit (that file is Crowdin’s translation source), or they’re invisible to translators. See Interface languages.

New issues start as needs-triage; a maintainer confirms, labels, and — if it’s slated for a release — milestones it onto the public roadmap.