Skip to content
GitHub
Decisions

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_SCOPES per 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.com for the customer dashboard and ops.runspectral.com for 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 /operations route tree inside the customer Next.js dashboard (apps/dashboard); OperationsShell enforced 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.