Internal hardening. No user-visible feature changes. The PostgREST service is gone — FastAPI now talks to Postgres directly via a psycopg async pool — and the project finally has a real backend test suite (213 tests against a real Postgres testcontainer with per-test transaction rollback). Plus a handful of correctness + security fixes surfaced by the post-migration audits.
Changed — architecture
- Dropped PostgREST. Backend services now use
psycopg[pool]async pool directly. Pool is opened/closed in the FastAPI lifespan; every service module migrated to the new helpers (db.fetch,db.fetchrow,db.fetchval,db.execute,db.db()). One fewer container, one fewer hop.POSTGREST_URL/POSTGREST_API_KEY/POSTGREST_AUTHenv vars removed; the pool readsDATABASE_URLdirectly. Stack is now three containers (postgres + openstudy + frontend), not four. - Auth code consumption is now atomic — single
DELETE … RETURNING *, closing the previous TOCTOU window. - Lecture-topics insertion is transactional —
add_lecture_topicswraps the optional lecture create + every topic insert in one psycopg transaction. /api/healthno longer blocks the event loop — storage stat moved off the request thread.- Rate limiter prefers
CF-Connecting-IPover the spoofableX-Forwarded-For.
Added — test infrastructure
- pytest + testcontainers-postgres with per-test transaction rollback (
force_rollback=True+ a_TxnPoolconnection pin) — clean DB every test without container churn. - 213 backend tests: ~145 service-layer + 60 MCP-tool tests across 11 files (one per entity family) + 5 helper unit tests + 3 multi-step end-to-end scenarios (full OAuth lifecycle, login rate-limit, TOTP enroll+login).
validated_colshelper — filters dict keys to Pydantic-declared schema fields before f-stringing into INSERT/UPDATE SQL. Defence in depth applied at all 16 patch/insert sites across 7 service files.
Added — OAuth
POST /oauth/revoke(RFC 7009) — public clients call this on logout. Advertised in/.well-known/oauth-authorization-server.
Fixed
update_settingsno longer NULLs valid timezone/locale when callers passNonefor unset parameters (exclude_none=True).update_settingsfallback insert usesON CONFLICT (id) DO UPDATE— concurrent first-callers no longer race the PK constraint into a 500./api/auth/totp/setupis now an upsert. On a fresh DB the previous bare UPDATE matched zero rows but returned 200 with a generated secret —/totp/enablethen 400'd.consume_auth_coderejects non-S256 PKCE unconditionally (OAuth 2.1 spec compliance).record_eventordering tie-breaker —clock_timestamp()+id DESCso events inserted in the same transaction order deterministically.storage._logsavepoint — swallowed FK violations no longer poison the outer request transaction.- Pool-level UUID loader — psycopg's
uuid.UUIDnow decodes tostrso Pydantic schemas don't need per-service conversions.
Removed
postgrest-pydependency, the PostgREST container, and the legacyapp.db.client()helper — all call sites migrated.
Migration notes (upgrading from v0.5.0)
- Pull, then
./deploy.sh— runs with--remove-orphansso the PostgREST container is cleaned up automatically. POSTGREST_URL/POSTGREST_API_KEY/POSTGREST_AUTHare no longer read; safe to remove from.env.- No schema changes.