Frontend Architecture Decisions (ADR Register)

Program: Command Center Transformation Status legend: PENDING (0B) = evidence captured in Phase 0A, decision to be formalized in Phase 0B · ACCEPTED · HUMAN DECISION · SUPERSEDED

Phase 0B formalization (2026-08-05, session 2): ADR-001/003/004/005/006/007 ACCEPTED below (each carries a Decision + Consequences block). ADR-002 is a HUMAN DECISION (HD-1) with a recommendation recorded for the owner. Evidence lines already resolved by the session-2 fix pass are annotated [RESOLVED 0A.1].

All evidence lines below are measured results from the 2026-08-05 Phase 0A audit unless labeled otherwise. File references are to miz-oki-command-center-ui/ unless prefixed with the monorepo root.

Re-measured 2026-08-29 — see §0 for per-ADR implementation status.


0. Implementation status (re-measured 2026-08-29)

The seven decisions below are unchanged and none is SUPERSEDED. ADRs record decisions, not state, so the bodies are kept verbatim; this section records how far each has been carried into the tree, so a reader can tell a decision from a deployed property.

ADR Decision Implementation status 2026-08-29
001 Product boundary (marketing vs operator) HOLDS — two separate Cloud Run services and two separate deploy paths; the site stays owner-dispatch-only, the console deploys on merge.
002 Supabase Auth, server-verified sessions BUILT, OPERATOR-GATED — Phase 1B shipped the deny-by-default guard core, app_metadata-only roles, and the shared lib/auth/idp-config.ts predicate. middleware.ts:102 still reads REQUIRE_AUTH === 'true', so enforcement in production is an operator flip against real credentials. Build state is not enforcement state.
003 Browser/backend boundary (BFF) SHIPPED — lib/bff/registry.ts (14 services, server-only env vars), lib/bff/gateway.ts, 18 adapter modules, 46 route handlers under app/api/bff/**. The no-NEXT_PUBLIC consequence is test-enforced: the only six matches in lib/bff/ are the guard test registry.test.ts and the comment explaining it. CELL_URLS was drained, not extended (lib/api/client.ts:155 records its removal).
004 State ownership (Query = server state, Zustand = UI state) HOLDS — store/app-store.ts is the Zustand store; TanStack Query owns server state; @reduxjs/toolkit is absent from package.json and swr was removed in Phase 0A.1.
005 Contract strategy SHIPPED — adapters are typed from each deployed service's own source; contract tests live beside them (lib/bff/adapters/adapters.test.ts).
006 Visual system (Operator Dossier) PARTIAL, EXTENDED — Phase 3A slices 1+2 shipped tokens and the banner-cluster primitive. The console consolidation (2026-08-28/29) extended it with the destination shell: app/(console)/layout.tsx renders the destination nav and the posture bar once, in the layout, so every console screen carries governance posture from one source.
007 Migration strategy (additive, redirects + flags, never rewrite-and-cutover) HOLDS — and the console consolidation is its largest test. Six competing roots were retired as 307 redirects, not deletions; /command-center/** children stay routable as deep links; the redirect list and lib/console/destinations.ts RETIRED_ROOTS are held together by lib/console/retired-roots.contract.test.ts. Nothing was cut over.

Standing caveat this register must keep making: ADR-002's row is the one where a reader is most likely to over-read. "ACCEPTED" and "built fail-closed" are claims about the code. Whether operators are actually authenticated is a claim about the serving revision's environment, and this document has never measured that.

ADR-001 — Product boundary

Status: ACCEPTED (0B, 2026-08-05).

Decision: marketing/demo (# MIZ OKI 3.5/, mizoki3.com) remains the narrative product; the Command Center is the authoritative operator product. No demo storytelling inside operational flows; no operator tooling inside the canon-locked marketing tree.

Consequences: operator features land only under miz-oki-command-center-ui/; any page that exists to narrate rather than operate is a candidate for the demo platform instead; the two deploy pipelines stay separate (auto Deploy Router for the UI vs human-approval-only for the site).

Direction: marketing/demo (# MIZ OKI 3.5/, mizoki3.com) remains the narrative product; Command Center becomes the authoritative operator product; no duplicate demo storytelling inside core operational flows.

Evidence (0A): - Marketing site is design-canon LOCKED (20 pinned surfaces, human-approval deploys only) — # MIZ OKI 3.5/docs/DESIGN_CANON.md, canon.lock.json. - The Command Center package contains no canon-pinned files; the two products already deploy as separate Cloud Run services (mizoki-website vs miz-oki-command-center-ui).

ADR-002 — Authentication and identity

Status: ACCEPTED — owner sign-off 2026-08-05 ("Signed off and approved all stage 1 parts", ceo@mediaintelligence.ai). HD-1 is closed: Supabase Auth is the IdP; server-verified sessions replace the client-side Firestore scheme. Enforcement ships behind REQUIRE_AUTH and activates only when the operator provisions real Supabase credentials at deploy time — the code path is built fail-closed but the production flip remains an operator action.

Recommendation to the owner: replace the client-side Firestore credential scheme with a server-verified IdP. Preferred option: Supabase Auth — the SSR plumbing (middleware refresh, /auth/callback code exchange, server client) is already implemented and only lacks real project credentials; Firebase AUTH is dead code; the live scheme is unsalvageable (browser-readable password hashes, client-forgeable sessions). Interim hardening (any option): stop shipping passwordHash/salt to browsers, verify sessions server-side in middleware, and align middleware's checked token with the token login actually issues. Blocked on: owner confirms production-user impact + provisions the Supabase project (or directs an alternative). No migration lands before that.

Convergence (2026-08-05, post-merge): a parallel session's measured decision doc (miz-oki-command-center-ui/docs/AUTH_DECISION.md, landed on main via 9df5935) independently reached the same conclusion — zero firebase/auth usage, Supabase as sole IdP, Firestore tenant-auth migration deferred. Two independent measurements now agree; HD-1 sign-off is the remaining step.

Measured state (0A): - The IdP actually live at /login is NONE of the expected two: it is a custom client-side Firestore tenant auth (lib/tenant-auth.ts via the app-wide TenantAuthProvider) — the browser reads tenants/{id}/users/{email} (incl. passwordHash + salt), verifies PBKDF2 in the browser, mints a session token client-side, writes it to Firestore sessions/{token}, and stores it in localStorage (miz_oki_session). This design is not salvageable as-is (credential material readable by clients; sessions client-forgeable) and is the strongest argument for ADR-002 replacing — not merely configuring — the login path. - middleware.ts v2.0.0: Supabase SSR session refresh IF NEXT_PUBLIC_SUPABASE_URL + anon key are set; otherwise falls back to legacy cookie check (auth-token / mizoki_token) whose role decode is a hardcoded stub (userRole = 'engineer', middleware.ts:254). Middleware never checks the miz_oki_session token the live login actually issues — login and route protection are entirely disconnected. - Route protection is empty unless REQUIRE_AUTH=true (middleware.ts:28-30), and the unconditional public list includes /dashboard and /boss (middleware.ts:46-56) — those routes bypass auth even with REQUIRE_AUTH on. - Comment at middleware.ts:26 (verified in source): "Auth disabled until Supabase is properly configured (currently using placeholder credentials)". - Supabase: fully implemented (SSR clients, middleware refresh, app/auth/callback code exchange) but deployed with placeholder credentials (cloudbuild.yaml:55,57) → wired, not live. Firebase AUTH: getAuth imported once, exported auth object never consumed → dead code. Firebase FIRESTORE: live (data layer AND the credential/ session store above). firebase-admin server-side only (4 API routes + a script). - Two additional unmounted auth contexts existed (lib/auth-context.tsx, lib/auth-context-backend.tsx) [RESOLVED 0A.1 — deleted]; the mounted provider is hooks/useTenantAuth.tsx. - Root CLAUDE.md (repo): Firebase env vars are baked into the Cloud Run build for the UI (NEXT_PUBLIC_FIREBASE_* build args in cloudbuild.yaml) — historical record, re-verify against current cloudbuild before deciding.

Charter guardrail: if evidence does not clearly support an irreversible migration, keep the current live IdP, remove only dead compatibility paths, and open a human decision gate. Production user impact is unknowable from this session → HD-1 in the migration ledger.

ADR-003 — Browser/backend boundary (BFF)

Status: ACCEPTED (0B, 2026-08-05).

Decision: every browser call goes Browser → same-origin Next.js route handler → authedFetch (lib/service-auth.ts OIDC, audience = service origin) → Cloud Run. The browser never holds backend URLs or mints identity tokens. One gateway module replaces the ≥7 client schemes and 5 URL resolvers; direct *.run.app fetches from client components are migrated route-by-route in Phase 1C.

Consequences: new code MUST NOT add NEXT_PUBLIC_* backend URLs or raw client-side fetch to services; CELL_URLS (lib/api/client.ts) is frozen as legacy surface to be drained, not extended further.

Direction: Browser → same-origin authenticated BFF (Next.js route handlers) → OIDC-authenticated Cloud Run service. The BFF is a security/composition boundary, not duplicate business logic.

Evidence (0A): - lib/service-auth.ts already implements the correct core: origin-scoped audience (D17b), Node format=full identity tokens (D17a-safe), Google-managed host allowlist, MIZOKI_AUTH_STRICT=1 loud-failure mode, authedFetch. - 211 API route handlers exist but are inconsistent consumers; client components make 188 raw fetch( calls across 87 files (~40% of all fetches bypass any client layer). - Generic-client sprawl: ≥7 competing HTTP client schemes and 5 URL-resolution modules (lib/base-url.ts, lib/config.ts, lib/api-config.ts, lib/api/config.ts, lib/cellBase.ts) — the gateway must replace these, not add an eighth.

ADR-004 — State ownership

Status: ACCEPTED (0B, 2026-08-05).

Decision: React Query owns server state; Zustand owns ephemeral UI state only; URL params own shareable filters; SWR stays removed. Server data currently living in Zustand stores migrates to React Query in the phase that touches each surface; live socket/EventSource handles never live inside persisted stores.

Consequences: lib/query-client.tsx is the single provider; the [not-wired]-failing hooks introduced in 0A.1 are the pattern for unbacked data (loud absence, never fabrication) until Phase 1C wires them.

Direction (charter): React Query = server state · Zustand = ephemeral UI state · URL params = shareable filters · no new SWR.

Evidence (0A): - SWR: zero usages in the entire package (exhaustive grep). swr@^2.3.6 was a dead dependency [RESOLVED 0A.1 — removed in f37b40a]. - Redux: absent (package.json + zero imports). The package CLAUDE.md claiming Redux Toolkit/Next 14/React 18 is stale and must not be trusted for current-state claims. - Zustand: 9+ store files across THREE directories (store/, stores/, lib/); ≥5 hold server data (kernels, workflows, agents, KG graphs, cell statuses); 2 are dead (store/global.ts, store/globalStore.ts); store/slices/realtimeSlice.ts keeps live WebSocket/EventSource handles inside a persisted store. - React Query: mounted provider lib/query-client.tsx (dead duplicate lib/react-query.tsx [RESOLVED 0A.1 — deleted]); 140 useQuery + 90 useMutation sites across 32 files; the two hook layers were rebuilt honest in 0A.1 (lib/api/hooks.ts + hooks/api/* now wire real endpoints or fail loudly); aggressive interval polling (2–60s) coexisting with SSE remains to rationalize in 1C.

ADR-005 — Contract strategy

Status: ACCEPTED (0B, 2026-08-05).

Decision: contract order = (1) TS types generated from each FastAPI service's OpenAPI export → (2) versioned schemas in packages/shared-contracts → (3) hand-written Zod only where no authoritative schema exists, contract-tested. First concrete step (Phase 1C): capture openapi.json from the governance services and generate adapter types; hand-written parallel interfaces in types/+lib/api/types.ts are drained as each adapter lands.

Consequences: the session-2 rule "hooks' result types describe the live API; types/ aspirational shapes do not" (applied to Causal/KG orchestrators) is the interim law until generated types replace both.

Preferred order (charter): generated TS types from authoritative OpenAPI/JSON Schema → versioned shared JSON Schema → hand-written Zod (last resort, contract-tested).

Evidence (0A): - Backend governance services are FastAPI (services/service-*/main.py) — OpenAPI is derivable from the services themselves; whether exported artifacts exist is verified in the service-contract map. - Frontend has zod@^3.23.8 installed; a local packages/shared-contracts package exists (bossChat.ts, geminiSignals.ts) but has only 2 importers and the LIVE Boss chat path does not use it — contract adoption is currently decorative. - agents/, types/, and lib/api/types.ts (20KB) hold parallel hand-written interfaces with no mechanical verification.

ADR-006 — Visual system (Operator Dossier)

Status: ACCEPTED (0B, 2026-08-05) — implementation is Phase 3A.

Decision: one versioned token set (Operator Dossier) referencing marketing brand DNA without importing the canon; define the missing shadcn CSS variables so ~576 existing utility usages resolve; set Tailwind darkMode: 'class' to match the hardcoded <html class="dark">; consolidate on ONE primitive set and retire components/command-center/primitives.tsx via adapters.

Consequences: no new raw hex in components once tokens land; the 670-literal debt is burned down surface-by-surface, never big-bang.

Direction: brand-DNA-sharing Operator Dossier system with versioned tokens; does not modify the marketing design canon.

Evidence (0A) — the measured design debt this ADR must resolve: - Four uncoordinated color systems: tailwind.config.ts (5 tokens, 4 usages), lib/design-system.ts (full scale incl. an unwired tailwindExtend export), globals.css :root hex vars, and 670 raw hex literals across 58 files. - ~576 shadcn-style utility usages (bg-card, text-muted-foreground, ring-ring, …) resolve to NO CSS — the shadcn variables were never defined; Card/Badge/Alert/ Button focus rings are effectively unstyled. - Dark mode structurally broken: <html className="dark"> hardcoded while Tailwind darkMode is unset (defaults to media) — 467 dark: usages keyed to OS preference. - A second complete primitive set lives at components/command-center/primitives.tsx (including the only Table); missing primitives: table/form/checkbox/radio/popover/ sheet/command/toast-renderer; 195 raw animate-pulse ad-hoc skeletons. - Marketing canon vocabulary (night-dossier ink/cyan, Instrument Serif/DM Sans/ JetBrains Mono) is documented in # MIZ OKI 3.5/docs/DESIGN_CANON.md §2 as brand DNA reference — to be referenced, never imported wholesale into a dense operator UI.

ADR-007 — Migration strategy

Status: ACCEPTED (0B, 2026-08-05).

Decision: additive migration only: dual-accept 307 shims for route moves (SRPVDAL precedent), feature flags for behavior changes, compatibility adapters for API-shape changes, deletions only with a 0-importer proof recorded in the migration ledger. Every push must be independently production-safe because branch automation auto-merges to main.

Consequences: the session-2 fix pass is the template: verify → fix/wire → loud-absence for the unwired → delete only proven-dead → single verified state per push.

Direction: additive migration; redirects; feature flags; compatibility adapters; no rewrite-and-replace cutover.

Evidence (0A) — why additive is mandatory here: - 155 pages / 211 API routes / 310 client files / 0 tests: no safety net exists for a cutover. - Branch automation auto-merges claude/*/cursor/* pushes to main (observed in git history) — every landed slice must be independently production-safe; flags are the only honest rollout mechanism. - Precedent in-repo: SRDAL→SRPVDAL route migrations used dual-accept 307 shims (root CLAUDE.md, Phase 16) — the same pattern applies to route consolidation here.

← All docsView source on GitHub →