Skip to content
GitHub
Decisions

ADR-034: Frontend data access via API proxy; realtime via SSE

Status: Accepted (2026-04-20) Supersedes: ADR-002 on the direct-Supabase-SDK-from-frontend framing for data access (the rest of ADR-002 stands)

Context

ADR-002 implicitly assumed the Supabase pattern of apps/dashboard and apps/operations calling supabase.from(...) directly against PostgREST, with RLS as the security boundary and @supabase/ssr handling auth. That pattern is well-supported and well-documented; Vercel, Supabase, and the Next.js community publish working templates.

Two factors made it the wrong default for Spectral:

  • Business-logic gravity. Spectral’s scanning, evaluation, and agent paths are logic-heavy (orchestration, validation, observability, cost control, retention). Direct-DB-from-frontend would scatter that logic across PostgREST, view definitions, and frontend code. FastAPI as the data path gives the logic one home.
  • Coupling containment. Direct-SDK frontends couple the app deeply to Supabase’s specific SDK shape and PostgREST semantics. An API proxy keeps Supabase as an implementation detail with a viable exit path; future re-platforming touches the API tier, not every frontend route.

The realtime story is independent: Spectral’s realtime surface is unidirectional server→client (scan progress, change-set state, agent activity). WebSockets and Supabase Realtime add a bidirectional channel that the use case does not need. SSE from FastAPI handles the requirement with centralized auth, backpressure, and observability.

Decision

D1 — Frontend uses Supabase Auth only, not Supabase data

@supabase/ssr for session management (cookie sync across RSC, Route Handlers, middleware — it is the sharp tool; do not reinvent). No supabase.from(...) or PostgREST calls from apps/dashboard or apps/operations. All data: Frontend → FastAPI → Postgres. The anon key is not exposed to the browser; business logic gravity is contained in FastAPI; Supabase stays as an implementation detail with a viable exit path.

A lint rule rejects supabase-js data-access imports from the frontend packages; only @supabase/ssr auth-helper imports are permitted. The lint lands when the frontend epics begin.

D2 — Realtime via sse-starlette from FastAPI

Spectral’s realtime surface is unidirectional server→client. SSE from FastAPI handles the requirement with centralized auth, backpressure, and observability — and no WebSocket gateway. Supabase Realtime is not used for customer-facing surfaces.

Supabase Realtime is used internally as the streaming hop between workers and apps/api for agent token streaming (per ADR-060 / TA-15) — channel keyed by conversation_id; apps/api proxies the channel as SSE to the client. That is an internal implementation detail, not a customer-facing data path.

D3 — TanStack Start adopted as the frontend pattern (per ADR-050 / TA-21 D8)

apps/dashboard and apps/operations use TanStack Start (SPA shell + file-based routes + first-class server functions for the auth path). The data path remains API-proxy-only; TanStack Start’s server functions are used for auth + cookie management, not for direct DB access.

Alternatives considered

Direct Supabase SDK + RLS-primary (ADR-002 implicit). Rejected per the business-logic-gravity and coupling-containment arguments above, plus the CVE-2025-48757 class evaluated in ADR-033. The pro-direct-SDK position’s strongest asset (@supabase/ssr auth ergonomics) is orthogonal to its data-access position; the auth ergonomics are kept and the data-access pattern dropped.

API proxy plus Supabase data SDK as a fallback for “simple” reads. Rejected. Two data paths is two surfaces of risk and two surfaces of bug. The audit posture, observability, and tenancy enforcement live cleanly in one place.

WebSocket / Supabase Realtime for UI live updates. Rejected for Spectral’s bidirectional-not-needed use cases; SSE covers the requirement with centralized observability.

Server-rendered HTML (Astro/Remix-style) as the frontend frame. Considered briefly. Rejected per ADR-050 — TanStack Start’s SPA-with-server-functions shape is a better fit for Spectral’s UI density and operator surface.

Consequences

  • ADR-002 is partially superseded on the direct-SDK framing for data access. The Supabase-as-platform, Auth, and pgvector commitments stand.
  • @supabase/ssr is the only Supabase SDK in the frontend. A lint rule (lands with the first frontend epic) rejects supabase-js data-access imports.
  • sse-starlette is added to apps/api when the first realtime surface lands; a thin SSE broadcast helper is added to spectral.core if streaming between contexts becomes needed.
  • Internal workers→API streaming hop uses Supabase Realtime per ADR-060; that hop is invisible to customers and does not contradict D1.
  • Future re-platforming off Supabase touches the API tier and the auth pages, not every frontend route. The exit path is real.
  • Codex system-design/foundations/architecture.mdx close-pass captures the API-proxy data flow alongside the schema layout and the TA-21 four-runtime model.

References

  • ADR-002 — retired; original Supabase platform decision distilled into the ADR-046 addendum
  • ADR-032 — storage topology
  • ADR-033 — tenancy enforcement layering
  • ADR-039 — Supabase Auth confirmation + JWT validation surface
  • ADR-046 — Render hosting (frontend SPA discipline)
  • ADR-050 — TanStack Start adoption
  • ADR-060 — internal workers→Realtime→API→SSE hop
  • TA-1 disposition — SPEC-304 comment 5c9c25f0
  • Codex system-design/foundations/architecture.mdx — close-pass updates
  • Codex system-design/topology/frontend-architecture.mdx — TA-21 frontend design