Frontend Architecture
Spectral’s frontend topology is two SPA processes (the customer dashboard and the staff Operations app) plus two static Pages projects (public user docs and the staff Codex). All data flows through FastAPI; the auth helper is the only Supabase JS surface used. Decision lineage in ADR-050 (frontend pattern), ADR-046 (hosting), and ADR-047 (Operations app as its own deployable).
Three origins, five subdomains
Section titled “Three origins, five subdomains”| Hostname | Origin | Auth | Audience |
|---|---|---|---|
app.runspectral.com | Render dashboard | Supabase Auth (JWT in cookie) | Customer |
ops.runspectral.com | Render operations | JWKS-local middleware + OPERATIONS_SCOPES | Staff |
api.runspectral.com | Render api (FastAPI) | Authorization: Bearer JWT | Programmatic |
docs.runspectral.com | Cloudflare Pages docs-user | None (public) | Anyone |
codex.runspectral.com | Cloudflare Pages docs-codex + Pages Function | Pages Function JWKS-local + OPERATIONS_SCOPES | Staff |
Cookie scope: Domain=runspectral.com eTLD+1, set by the Supabase auth helper at initialization. The same login session is naturally readable on app., ops., and (via the Pages Function) codex..
Rendering mode and data path
Section titled “Rendering mode and data path”The two SPA frontends share the same primitives.
- Default rendering mode: SPA. Server-side rendering is opt-in per route, used only where a specific route’s UX demands it.
- Auth helper only. The Supabase JS client handles auth (PKCE flow + JWT claim extraction). It is not used for data access.
- Data path: Frontend → FastAPI → Postgres. Frontends never call PostgREST or the Supabase data API directly. A lint rule rejects data-access imports from the frontend packages.
Specific framework choices, build toolchain, and per-route routing primitives live in ADR-050.
Operator auth (Pattern A)
Section titled “Operator auth (Pattern A)”Server-side middleware runs on every operator-facing route (per ADR-046 D9):
- Read JWT from session cookie.
- Validate signature + expiry against Supabase JWKS (cached locally, with
kid-miss bypass). - Check
app_metadata.account_role === "operations"(per ADR-039). - Reject with the staff-only UX on failure.
A contract test enforces parity between the frontend’s JWT validation and the API’s Python JWKS-local validation on the same inputs (signature, expiry, audience, issuer, scope set).
The Codex Pages Function uses the same pattern with the same scope — the auth gate is a direct port. JWKS cache lives in Cloudflare KV (10-minute TTL with kid-miss bypass) since Pages Free isolate lifetime is non-deterministic.
Realtime (server → client)
Section titled “Realtime (server → client)”Customer-facing realtime is unidirectional, server → client: scan progress, change-set state, agent activity. Implementation: sse-starlette from FastAPI.
Internally, the workers → API streaming hop uses Supabase Realtime as a channel keyed by conversation_id; apps/api proxies that channel as SSE to the client. That internal hop is invisible to customers and is the only Supabase Realtime use in production.
Docs sites
Section titled “Docs sites”docs-user(public) — Astro static; deploys todocs.runspectral.comviawrangler pages deploy.docs-codex(staff-internal) — Astro static plus a Pages Function for JWKS-local auth +OPERATIONS_SCOPES. Deploys tocodex.runspectral.com.