107 lines
5.7 KiB
Markdown
107 lines
5.7 KiB
Markdown
# 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.
|