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.