Skip to content
GitHub
Decisions

ADR-050: TanStack Start adoption + frontend SPA discipline

Status: Accepted (2026-04-21)

Context

ADR-002 had assumed the Supabase reference pattern of Next.js + @supabase/ssr for the dashboard and operations frontends. That pattern is well-supported by Supabase + Vercel templates, but during TA-21 (ADR-046) two findings flipped the choice:

  • The customer-data path is API-proxy (per ADR-034). With no direct-Supabase-SDK in the data path, the SSR ergonomics that motivate Next.js + @supabase/ssr largely disappear; the auth-helper portion remains valuable but does not require Next.js as the host framework.
  • TanStack Start (Vite + TanStack Router + TanStack Query) fits Spectral’s UI density — operator surfaces, agent chat, dashboards — without the cache-poisoning class of issues that comes with SSR-by-default. SPA at alpha; SSR-per-route reserved for cases where a specific route’s UX demands it.

This ADR captures the TanStack Start adoption and the frontend SPA discipline that flows from it.

Decision

D1 — TanStack Start as the frontend framework for both apps

This ADR is the canonical record of the TanStack Start framework choice; ADR-046 D3 first captured the selection during TA-21. apps/dashboard and apps/operations are TanStack Start projects (Vite + TanStack Router + TanStack Query + TanStack Form). Independent Vite builds; no shared frontend code beyond explicit workspace packages under packages/.

D2 — SPA mode at alpha; SSR-per-route reserved

Default rendering mode is SPA. SSR is opt-in per route via TanStack Start’s ssr config flag, used only when a specific route’s UX demands it (e.g., a public marketing landing). Authenticated dashboards do not benefit from SSR for SEO and pay the cache-poisoning class of risk if SSR is the default.

D3 — Auth via @supabase/supabase-js getClaims(); no @supabase/ssr

The auth helper for cookie sync across SPA, server functions, and middleware is @supabase/supabase-js (PKCE flow + getClaims() for JWT claim extraction). @supabase/ssr is not used; its primary value (cookie sync across RSC / Route Handlers / middleware) is Next.js-specific and does not generalize to TanStack Start. Server functions (used for the auth path only) handle cookie operations directly.

D4 — Data path is API-proxy only

Per ADR-034 D1. Frontends never call PostgREST or supabase.from(...). Data flow is Frontend → FastAPI → Postgres. A lint rule rejects supabase-js data-access imports from the frontend packages; only auth-helper imports are permitted. The lint lands when the frontend epics begin.

@supabase/supabase-js initialized with cookieOptions.domain = "runspectral.com" for cross-subdomain session sharing across app., ops., codex.. Per ADR-046 D8.

D6 — Operations Start carries Pattern A JWKS-local auth middleware

Per ADR-046 D9. Validates JWT against Supabase JWKS in-process; checks OPERATIONS_SCOPES; gates every route. Contract test enforces parity with FastAPI’s auth check (per ADR-039 D4a). The Codex docs site (Cloudflare Pages, per ADR-048 D3) reuses the same pattern in a Pages Function (per ADR-052 D8).

D7 — Build orchestration via turbo

turbo.json defines build, typecheck, lint pipelines. Dev orchestration via a concurrently ladder: pnpm dev runs the full-local stack; per-surface scripts (pnpm dev:api, pnpm dev:dashboard, etc.) for focused work. Per ADR-046 D13.

D8 — Architecture-validator extensions

A TanStack Start route convention check lands as part of the close-pass validator extensions: routes live under apps/<app>/src/routes/ per the file-based router pattern; route definitions follow the Start convention (createFileRoute, loaders, search-validators). Catches drift early.

D9 — Docs do not run on Start

Per ADR-048 D3, docs.runspectral.com and codex.runspectral.com run on Cloudflare Pages. TanStack Start serves only customer- and operator-facing app routes; docs live outside the Start processes. The original ADR-046 D5 (two TanStack Start processes with audience-aligned static mounts) was narrowed by ADR-048 D3.

Alternatives considered

Next.js + @supabase/ssr (the ADR-002 implicit pattern). Rejected per ADR-034: the data path is API-proxy, removing the strongest motivation for SSR. Cache-poisoning class of risk and Node-runtime lock-in were the closing arguments.

Remix. Considered. Comparable to Next.js for our use case; lacks TanStack Router’s nested-loader ergonomics for operator surfaces.

Astro for everything (including dashboards). Rejected. Astro shines for content-heavy static surfaces (chosen for docs-user and docs-codex); not the right fit for interactive dashboards.

Bare Vite + react-router (no Start). Rejected. TanStack Start adds the auth-path server functions, getClaims() integration, and route-tree typing. Going bare means re-implementing those pieces.

SSR-by-default. Rejected for the cache-poisoning risk and the lack of SEO benefit on auth-gated pages.

Consequences

  • The Next.js scaffolds in apps/dashboard and apps/operations are obsolete and rebuild as TanStack Start projects (Vite + Router + Query + Form).
  • apps/operations/ becomes a properly populated parallel-to-dashboard app (per ADR-047), not the vestigial scaffold the v0.2.0 carry-over left.
  • No @supabase/ssr in the dependency graph; just @supabase/supabase-js.
  • Server functions are used for the auth path only — not as a data path.
  • pnpm-workspace.yaml minimumReleaseAge: 10080 (per ADR-046 D14) protects the JS dependency surface against supply-chain compromise.
  • Architecture-validator extensions check route convention + pnpm-workspace.yaml minimumReleaseAge enforcement + (via ADR-048) render.yaml schema.
  • Docs sites do not run on Start — Astro builds output dist/ for Cloudflare Pages.
  • Codex system-design/topology/frontend-architecture.mdx captures the topology: two Start processes (app + ops), two Pages projects (docs-user + docs-codex), shared cookie scope, Pattern A auth in two places.

References

  • ADR-002 — retired; original Supabase platform decision distilled into the ADR-046 addendum
  • ADR-065spectral.core admission discipline (no surface added here)
  • ADR-031 — single-library + app-leaves
  • ADR-034 — frontend data access via API proxy
  • ADR-039 — Pattern A JWKS-local
  • ADR-046 — D3 / D8 / D13 / D14 (introduces TanStack Start adoption)
  • ADR-047 — operations as its own app
  • ADR-048 — D3 narrows ADR-046 D5; docs on Pages
  • ADR-052 — Pages Function reuses Pattern A
  • TA-21 disposition — SPEC-324 comment 8e48c86a
  • Codex system-design/topology/frontend-architecture.mdx — close-pass new page
  • Codex developer-guide/your-first-feature.mdx — close-pass dev-loop updates
  • pnpm-workspace.yamlminimumReleaseAge: 10080
  • turbo.json — pipeline definitions