The problem
The zoning-compliance pipeline produces QA signals continuously — hallucinated section numbers, citation drift, missing references. The team needed a real-time dashboard to surface these before they reached end-users.
The obvious answer — stand up a data warehouse, run an ETL job, build a BI layer — would have taken weeks and added three new services to operate.
The architecture
postgres → versioned SQL views (qa.qa_v1_*) → postgrest → caddy → react
The operational Postgres database already held all the QA data. Instead of moving it, we exposed it directly:
- Versioned SQL views (
qa_v1_citation_drift,qa_v1_hallucination_rate, …) computed QA metrics as pure SQL. Thev1namespace prefix lets us evolve views without breaking the React frontend. - PostgREST auto-generates a REST API from the
qaschema. Adding a new view = a new endpoint, no code changes. - Caddy handles TLS termination and JWT authentication — the same Caddyfile that fronts everything else on the VPS.
- React frontend reads from the PostgREST endpoints. No ORM, no API server, no custom middleware.
What I built
- Five initial SQL views covering citation drift, hallucination rate, source coverage, latency percentiles, and daily QA pass-rate
- PostgREST configuration scoped to the
qaschema with row-level security on sensitive fields - Caddy reverse-proxy rules with Bearer token auth for the dashboard route
- React SPA with live polling (5-second intervals for the headline metrics, 60-second for the aggregates)
- Materialized view refresh triggered by the Airflow pipeline on each successful run
Tradeoffs
| Decision | What we gained | What we gave up | |---|---|---| | No warehouse | Zero new services, zero ETL lag | Can't join across Iceberg tables | | PostgREST API | Instant endpoints, no API code | Less control over query shape | | Versioned views | Safe evolution, rollback by alias swap | More view aliases to manage | | Caddy vs Nginx | Simple auth config | Slightly less flexible routing |
Outcomes
- 4 days from design to first production dashboard
- New QA signal → live in dashboard: < 1 hour (just write the SQL view)
- Zero new infrastructure services
- One incident resolved: a recursive CTE in
qa_v1_citation_chaincaused full-table scans; replaced with a WITH clause and index hint