ADR-047: Operations is its own deployable, not a section of the customer dashboard
Status: Accepted (2026-04-21) Supersedes: ADR-009
Context
ADR-009 (written in v0.2.0; retired and folded into the addendum below) framed the staff operations console as a section inside the customer dashboard rather than a separate app. The 0.3.0 rebuild discipline forced re-examination during TA-21 (ADR-046). The conclusion: under the 0.3.0 architecture, the operations console is the control plane through which Spectral runs the technology that is the business — its complexity trajectory and the auth boundary it carries warrant first-class deployable status.
Three concrete forces drove the supersession:
- Auth boundary lives at the deployment layer. Staff scopes (
AccountRole.OPERATIONS+OPERATIONS_SCOPESper ADR-039 D11 / SPEC-306 D11 amendment) are never granted to customer-facing users. Coupling staff-only routes inside the customer SPA mixes two failure surfaces and forces the auth check into application code rather than the deployment edge. - Independent deploy cadence. Operations evolves with the operator surface tools (DLQ inspection, cluster triage per ADR-060 D7/D8, agent chat, World Agent / Ops Agent UIs); the customer dashboard evolves with the customer surface. Coupling their deploys is incidental.
- Audience-aligned subdomain layout. ADR-046 D2 picks
app.runspectral.comfor the customer dashboard andops.runspectral.comfor operations. Subdomain separation makes the auth gate trivial (Pattern A JWKS-local middleware on Operations Start) and is invisible in the customer surface.
Decision
D1 — Operations is its own deployable
apps/operations/ is its own TanStack Start project, deployed to its own Render service (spectral-ops-staging / spectral-ops-production), reachable at ops.runspectral.com.
D2 — Operations Start carries Pattern A JWKS-local auth middleware
Per ADR-046 D9. The middleware validates the Supabase JWT against JWKS in-process, checks for the operations scope, and gates every route. Contract test enforces parity with FastAPI’s auth check.
D3 — Operations does not import customer-dashboard code
The two SPAs are separate Vite builds. Shared types live in workspace packages under packages/ if and only if they are genuinely shared contract; ad-hoc shared utilities are not allowed across the boundary.
D4 — Codex docs (codex.runspectral.com) share the operations auth gate
Per ADR-048 D3, codex runs on Cloudflare Pages with a Pages Function that validates the JWT against the Supabase JWKS and checks the same operations scope — the auth check is a direct port of Operations Start’s Pattern A. Session cookie scoped at runspectral.com eTLD+1 (per ADR-046 D8) is naturally visible to codex.runspectral.com.
Alternatives considered
Keep ADR-009 — operations as a section inside the customer dashboard (the v0.2.0 framing). Rejected for the three forces above. Doable, but mixes staff and customer failure surfaces and pushes the auth gate inward.
Operations as a separate app but sharing the same TanStack Start process via host-routing (the original TA-21 D5 “two TanStack Start processes with audience-aligned static mounts”). Refined by ADR-048 D3 to drop static mounts; a single Start process handling both customer and staff routes was considered and rejected — auth boundary is cleanest at the deployment edge.
Operations as a server-rendered page set inside apps/api (no separate frontend). Rejected. Operations needs interactive UI density; serving HTML from FastAPI conflates the API tier with frontend concerns.
Consequences
- ADR-009 is retired and folded into the addendum below. The “section, not separate app” framing no longer applies.
- Two TanStack Start projects (
apps/dashboard,apps/operations) with no shared frontend code beyond explicit workspace packages. Independent deploy cadence. - The Codex docs site runs on Cloudflare Pages with a JWKS-local auth Pages Function (per ADR-048 D3) — the same operations scope gate.
- The Operations Start auth-parity contract test is load-bearing — JWKS-local validation in TypeScript (
getClaims()) must agree with the Python FastAPI middleware on the same JWT inputs (signature, expiry, audience, issuer, scope set). apps/operations/directory must be properly populated as a parallel-to-dashboard app, not the vestigial Next.js scaffold the v0.2.0 carry-over left.- The cross-subdomain cookie scope (
Domain=runspectral.com, per ADR-046 D8) is what makes both Operations Start and the Codex Pages Function readable from the same login session.
References
- ADR-009 — retired and folded into the addendum below
- ADR-039 — Pattern A JWKS-local + scope taxonomy
- ADR-046 — TA-21 hosting choice (introduces this supersession via D4)
- ADR-048 — TA-19 D3 (codex on Cloudflare Pages with auth function)
- ADR-050 — TanStack Start adoption
- ADR-052 — TA-22 (Pages Function reuses JWKS-local pattern)
- TA-21 disposition — SPEC-324 comment
8e48c86a - Codex
system-design/foundations/architecture.mdx— close-pass updates (4-runtime-service model) - Codex
system-design/topology/frontend-architecture.mdx— close-pass new page
Historical context — ADR-009 (Operations Dashboard — Section, Not a Separate App)
ADR-009 was retired and folded here on 2026-04-28 per the doctrine in docs/decisions/overview.md. Brief context for readers asking “why didn’t operations always live at its own subdomain?”:
- ADR-009 (Accepted 2026-04-09; v0.2.0 scale) placed operations as a
/operationsroute tree inside the customer Next.js dashboard (apps/dashboard);OperationsShellenforced role gating at the React layout level; default-landing was role-aware (ops users →/operations; customers → first workspace). Six pages were in scope at the time: overview, strategies, interventions, models, audit, feedback. - v0.2.0 drivers in favor: single auth session for staff moving between operations and customer workspaces (no cookie-sharing across domains); one build pipeline for shared components / hooks / generated API types; deployment cost minimization (no second Cloudflare target); fewer moving parts during launch readiness.
- ADR-009 explicitly listed four “revisit if” triggers: operations feature count exceeding ~8 pages with measurable bundle impact; operations needing a different deployment target (VPN / stricter egress); operations needing a different auth model (SSO-only with no Supabase fallback); a customer incident tracing to operations-tree code. The 0.3.0 rebuild + operator surface expansion (DLQ inspection, cluster triage, agent chat, World Agent / Ops Agent UIs) fired the first two; ADR-046’s audience-aligned subdomain layout closed the case structurally rather than incrementally.
- The full ADR-009 text is preserved in git history at the commit prior to its retirement.