# Architecture This describes the implementation as reviewed on 2026-09-14. It distinguishes current behaviour from improvements still to be implemented. ## Request flow ```text Browser → Next.js public routes ├─ /api/* proxy → FastAPI → cached DataFrames / PostgreSQL marts │ ├─ Typesense (search and suggestions) │ └─ postcodes.io (postcode lookup) └─ /admin, /cms-api, /blog → Payload → payload schema + media volume Next.js server rendering → FastAPI directly through FASTAPI_URL ``` `nextjs-app/lib/api.ts` contains typed fetch wrappers and revalidation defaults. The proxy is `nextjs-app/app/(frontend)/api/[...path]/route.ts`. Payload uses `/cms-api` so its routes do not collide with the FastAPI proxy. The proxy denies `/api/flags`; server-side rendering reads flags directly from FastAPI. ## Data ownership | Layer | Owner and role | |---|---| | Source data | GIAS, DfE EES, Ofsted, finance, deprivation and council admission-distance sources | | `raw` | Singer taps and the PostgreSQL target configured in `pipeline/meltano.yml` | | Staging/intermediate/marts | dbt models in `pipeline/transform`; marts are materialized tables | | `marts.dim_school`, `marts.dim_location` | School identity and location, filtered to supported England establishments | | `marts.fact_*` | Performance and supplementary datasets; coverage and years vary | | Typesense `schools` alias | Search documents built by `pipeline/scripts/sync_typesense.py` | | `payload` | CMS collections and migrations in `nextjs-app/`; independent of dbt | | Media volume | Uploaded blog media; requires backup and cannot be regenerated from school datasets | `backend/models.py` maps existing marts for reading. It does not create the school schema. There is no startup schema-version migration or CSV reimport. Payload's `nextjs-app/migrations/` is active and must not be confused with the removed legacy backend migration code. Coordinates normally come from GIAS British National Grid coordinates transformed by PostGIS in `dim_location.sql`. `pipeline/scripts/geocode_postcodes.py` is a manual fallback utility, not a task wired into the current school-data DAG. Backend postcode searches also use postcodes.io; that lookup does not populate school coordinates in the database. ## Backend boundaries - `app.py`: routes, middleware, search filtering, sitemap/place publication and response assembly. - `data_loader.py`: SQL loading, process-local DataFrame caches, Typesense calls, postcode lookups, supplementary queries and benchmark calculation. - `database.py`: synchronous SQLAlchemy engine and sessions. - `schemas.py`: metric definitions, column mappings and display metadata; despite its name this is not a collection of Pydantic API response models. - `places.py` and `localities.py`: place registry and curated locality information. - `flags.py`: Unleash-backed feature flags, disabled when no server is configured. - `gias_codes.py` / `ofsted_codes.py`: source-code translation and display rules. Search starts from a cached latest-row-per-school snapshot. Detail pages read history from the full DataFrame and supplementary data from marts. Comparisons batch supplementary queries across selected URNs. Async routes still contain synchronous dependency calls; a fully asynchronous database layer is not present. ## Frontend boundaries `app/(frontend)` owns the public root layout and pages. `app/(payload)` owns the CMS root layout. Do not add a shared `app/layout.tsx`: these groups deliberately have separate root layouts. Root metadata files remain in `app/`. Server pages fetch initial data and pass it to client views. Client state uses React hooks, URL search parameters and the comparison context/localStorage. There is no SWR dependency. Leaflet maps are loaded through dynamic wrappers; Chart.js renders performance and comparison charts. `components/school/` contains detail sections, with section decisions and data preparation in `lib/schoolSections.ts`. `lib/types.ts` contains manually maintained API types. `payload-types.ts` and the Payload import map are generated artifacts. ## Publication and caching today 1. Airflow DAGs extract and validate source data, then run selected dbt builds. 2. Relevant DAGs rebuild Typesense and swap the `schools` alias. 3. They call `POST /api/admin/reload` with `X-API-Key` to refresh school DataFrames. 4. A separate weekly sitemap DAG calls `POST /api/admin/regenerate-sitemap`, rebuilding places and sitemaps. GIAS is scheduled daily, Ofsted monthly, and annual datasets are manually triggered. The DAG definitions are authoritative for selectors and dependencies. Caches exist in several independent layers: backend DataFrames and registries, backend HTTP Cache-Control/ETags, Next.js fetch/page revalidation, and browser or shared HTTP caches where configured. Place fetches request a one-week revalidation interval. HTTP ETags are computed after route execution, not before database work. Known limitations: reload clears the old DataFrames before verifying replacement data; places/sitemaps refresh separately; Next.js caches are not explicitly purged by the pipeline; Typesense import results are not validated before alias publication. Do not describe this sequence as an atomic dataset release. These are follow-up reliability tasks, not changes implemented by the documentation cleanup. ## Deployment references See [DEPLOY.md](DEPLOY.md). PR checks include frontend typechecking/tests, backend unit tests, image builds and AI review. Staging journeys run after merging. Production promotion retags a selected commit's images. Current health polling checks HTTP success, not the deployed commit identity; overlapping staging runs remain a release-verification concern.