Companion to ../TECHNICAL_ARCHITECTURE.md. This is the FastAPI backend behind auth, community condition reports/moderation, closures, and warning escalation - the "someone has to be able to mark a closure and a moderator has to be able to escalate a warning" half of v1 MVP (see that doc's Backend section for why this moved into MVP on 2026-07-28).
python -m venv .venv
.venv/Scripts/pip install -r requirements-dev.txt # Windows; .venv/bin/pip on macOS/Linux
.venv/Scripts/python -m uvicorn app.main:app --reload # run the dev server
.venv/Scripts/python -m pytest # run everything (prints a coverage summary too)
.venv/Scripts/python -m pytest tests/test_x.py # one file
.venv/Scripts/python -m pytest -k test_name # one test
.venv/Scripts/python -m ruff check . # lint
.venv/Scripts/python -m ruff format . # auto-format
TECHNICAL_ARCHITECTURE.md specifies Postgres (Supabase-hosted) as the real backend database. This dev sandbox has no Docker and no admin rights to install Postgres natively, so a real local Postgres isn't available here. Rather than block on that:
- Local/dev tests run against DuckDB via
duckdb-engine(a real SQLAlchemy dialect - the sameduckdbdependency the data pipeline already relies on, not a new one) - fast, install-free, zero setup beyondpip install. - CI runs the same test suite against a real Postgres (a
postgres:16GitHub Actionsservices:container - GitHub-hosted runners do have Docker) - that job is the actual correctness gate. DuckDB is a local convenience layer only, never the thing that decides whether a change is correct. DATABASE_URLis the only switch.app/config.pydefaults it to a local DuckDB file; CI and production override it via the environment to point at Postgres instead. No code branches on which database is in use.
Real dialect gaps hit and worked around, not silently papered over (duckdb-engine is a genuine, functioning SQLAlchemy dialect, but a less mainstream one than Postgres/MySQL/SQLite - these are the two rough edges found standing this up):
SERIALprimary keys. duckdb-engine's compiler is PostgreSQL-derived, so SQLAlchemy's default "auto" autoincrement on a single-columnIntegerprimary key rendersCREATE TABLE ... SERIAL, and DuckDB has noSERIALtype -CatalogException: Type with name SERIAL does not exist!. Worked around intests/test_db_session.pyby disabling autoincrement (the test supplies its own ids anyway). Real app models that need a DB-generated integer PK will need to pick a DuckDB-compatible pattern explicitly (e.g. aSequence, or lean on Postgres-nativeIDENTITY/UUID defaults that DuckDB also supports) rather than relying on SQLAlchemy's default - noted here rather than solved preemptively, since no real model needs it yet.- No Alembic DDL implementation for the
duckdbdialect. duckdb-engine registers a SQLAlchemy dialect but not an Alembic one -alembic/env.pyhitsKeyError: 'duckdb'immediately otherwise.alembic/env.pyregisters a minimalDuckDBImplby subclassing Alembic'sPostgresqlImpl(same PostgreSQL lineage as #1). This only matters for running migrations locally against DuckDB; CI/production use real Postgres, which Alembic already supports natively. - Index reflection isn't implemented (
DuckDBEngineWarning: duckdb-engine doesn't yet support reflection on indices) - surfaced duringalembic revision --autogenerate, harmless for table/column-level autogenerate but worth knowing if an index-heavy migration's autogenerate diff looks incomplete.
None of these are modeling differences serious enough to justify not testing locally against DuckDB at all - they're narrow, identified, and either worked around in test code or documented as a "decide when it's real" open item, not hidden behind a workaround that would mask an actual Postgres-vs-DuckDB behavior difference.
Supabase Auth issues the JWTs this backend verifies (SUPABASE_JWT_SECRET) - see ../features/AUTHENTICATION.md for the full design and why Supabase specifically. SUPABASE_URL/SUPABASE_ANON_KEY round out the client-facing config the backend may need to hand back. None of these have a default in app/config.py - they're required environment variables (or a local, gitignored .env file); Settings() raises a pydantic.ValidationError at import time if any is missing, rather than the app silently starting with an empty credential. tests/conftest.py sets harmless test-only placeholder values for all three (via os.environ.setdefault, so a real environment's actual values always win) purely so pytest doesn't require live Supabase credentials just to run - no auth-verification logic exists yet to actually need real ones.
app/ is the FastAPI application (main.py's app, config.py's env-driven Settings, db/ for the SQLAlchemy engine/session/base). alembic/ holds migrations, wired to app.db.base's metadata and app.config's DATABASE_URL - see alembic/env.py. tests/ mirrors pipeline/tests/'s shape: conftest.py for shared fixtures (a fresh per-test DuckDB engine/session, a TestClient with get_db overridden), one file per behavior area.
.github/workflows/backend-tests.yml runs ruff check, ruff format --check, and two pytest jobs on every push and on PRs targeting main - one against the DuckDB fixture (fast, always runs), one against a real postgres:16 service (the actual correctness gate for the database this backend really runs on). Same visibility-only posture as the pipeline's CI (see ../TESTING.md's CI section): not yet a required check via branch protection.