Skip to content
GitHub
Topology

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).

HostnameOriginAuthAudience
app.runspectral.comRender dashboardSupabase Auth (JWT in cookie)Customer
ops.runspectral.comRender operationsJWKS-local middleware + OPERATIONS_SCOPESStaff
api.runspectral.comRender api (FastAPI)Authorization: Bearer JWTProgrammatic
docs.runspectral.comCloudflare Pages docs-userNone (public)Anyone
codex.runspectral.comCloudflare Pages docs-codex + Pages FunctionPages Function JWKS-local + OPERATIONS_SCOPESStaff

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..

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.

Server-side middleware runs on every operator-facing route (per ADR-046 D9):

  1. Read JWT from session cookie.
  2. Validate signature + expiry against Supabase JWKS (cached locally, with kid-miss bypass).
  3. Check app_metadata.account_role === "operations" (per ADR-039).
  4. 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.

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-user (public) — Astro static; deploys to docs.runspectral.com via wrangler pages deploy.
  • docs-codex (staff-internal) — Astro static plus a Pages Function for JWKS-local auth + OPERATIONS_SCOPES. Deploys to codex.runspectral.com.