Skip to content

IMM-01-Phase-3d-PREFLIGHT-PARK-DECISION-01

Brief: IDENTITY-MODEL-MIGRATION-01 Phase 3d execute (apps/site identity direct-bind retirement) — Gate 1 pre-flight (V1–V5). Predecessor: Gate 1 substrate audit — docs/docs/ops/audits/IMM-01-Phase-3d-Gate-1-audit.md (committed bcc678db). Filed: 2026-05-29 by Alex. Status: Pre-flight complete. 3d PARKED into the first-client-onboarding arc by Tim's Gate-1 fork call (Path C). No execute work follows from this brief; this doc is the durable map the onboarding arc inherits. Scope: Read-only substrate verification (V1–V5) + park decision. No code written, no substrate touched; every DB read was a read-only MCP d1_database_query against prod. Anchors against: ADR-024 (canonical user-identity schema), ADR-025 §888 (customer-facing workers route identity via internal-api), ADR-026 (cross-DB relations are application-layer), ADR-027 (data parity NOT committed), CANONICAL-IDENTITY-VIA-UI-ONLY, MANDARIN. Related: audits/AUTH-BRIDGE-AUDIT-01.md — reaches the same conclusion from the auth-bridge direction (the canonical scope half is unbuilt onboarding foundation); shares the §2 substrate counts.


1. Headline

The original Gate-1 audit overturned 3d's session-rename framing and re-cast it as identity direct-bind retirement (route apps/site's 14 identity reads/writes off direct WORKSPACE_DB binds and through INTERNAL_API to canonical destinations). This pre-flight overturns the next assumption: the canonical destinations are not ready, and one of them does not exist.

  • The canonical identity store (rto-identity-db) is empty by design. Per CANONICAL-IDENTITY-VIA-UI-ONLY + ADR-027 (data parity not committed), it holds no customer rows because no customer has onboarded through the UI yet.
  • clients exists nowhere canonical. The tenants → clients canonicalisation named in ADR-024/ADR-008 was never run. There is no clients table in any D1. The only org-like entity is the pre-canonical ops-db orgs table.
  • Therefore 3d's canonical routing is coupled to first-client onboarding — it was never a standalone pre-launch refactor. Routing apps/site to canonical destinations today would point live flows at empty/absent tables and at write endpoints that don't exist.

2. The three-substrate picture (V2 bytes — prod, read-only MCP)

Three parallel identity/membership substrates are live. apps/site, the canonical target, and the existing /org service surface each use a different one.

Substrate D1 (prod) Tables Prod row counts
WORKSPACE_DB — what apps/site reads/writes today engine-db-oc 81e2919a-6587-40a1-b749-0a65103d95f0 tenants, users, user_tenant_roles, portal_invites, groups, user_groups tenants 1, users 4, user_tenant_roles 1, portal_invites 0, groups 0, user_groups 0
IDENTITY_DB — ADR-024 canonical target rto-identity-db 8f0ba518-8194-407e-87a6-f16ee62ce30e credentials, id_migration_map, impersonation_tokens, magic_link_allowlist, portal_invites, tier_grants, users — no clients users 1, tier_grants 1 (client-scoped 0, T4/T4A 0 — the lone grant is the T3 operator), portal_invites 0
OPS_DB — existing /org surface (orgs.ts, ROLES-01 era) ops-db 0692049c-1bf1-49e7-9229-3773eeba1a45 orgs, org_memberships, org_scope, access_allowlist — orgs has no slug column orgs 2 (the known UCCA duplicate, ORGS-DUPLICATE-UCCA-RECORD-CLEANUP-01), org_memberships 1

The membership/identity data that apps/site actually serves lives in WORKSPACE_DB (1 tenant / 4 users / 1 role) and partially in OPS_DB (2 orgs / 1 membership). The canonical store is effectively empty. The three were never reconciled because the reconciliation is the onboarding work.

3. Endpoint inventory (V1 — workers/internal-api/src/identity.ts, orgs.ts, index.ts dispatch)

Source-header enforcement already accepts site-worker (index.ts:1415–1416). Ten /identity/* endpoints are wired (1673–1693); the /org/* surface is wired (1602–1636). Mapping the brief's §4 reads / §5 writes:

§ Need (site) Canonical endpoint Status
user-by-email (sites 3, 4, 12) GET /identity/user-by-email ✅ exists (reads identity-db users, 1 row)
user-by-id (site 6) GET /identity/user-by-id ✅ exists — returns client_id, not legacy org_id
portal-invite-by-token (sites 10, 11) GET /identity/portal-invite ✅ exists; tenant_name resolves via a separate /org call per ADR-026
member-list + invites-by-client (sites 9, 13, 14) GET /identity/members ✅ exists — reads tier_grants (empty) + pending portal_invites (empty)
client-by-slug (site 2) missing — no clients table; orgs has no slug; /org/:id is by-id only
client-by-id (site 8) /org/:id (getOrg) ⚠️ exists but resolves to ops-db orgs (pre-canonical), not canonical clients
user-create/upsert (§5) POST /identity/user-upsert ✅ exists
tier-grant-create (§5) missing
portal-invite-create (§5) missing (getPortalInvite is read-only)
client/org provision (§5) /org/provision (provisionOrg) ⚠️ exists but writes ops-db orgs + org_memberships, not canonical clients + tier_grants

The read endpoints for users/members/invites exist but return empty against the canonical store. The canonical write side does not exist (tier-grant-create, portal-invite-create, canonical client-provision), and there is no canonical clients entity to write to.

4. V3 / V5 — schema expressible, data absent

  • V3 (compliance gate, apps/site/app/api/mode/route.js:16–46). Effective logic: users.org_id present → access; else groups/user_groups LIKE %compliance%. groups/user_groups are empty in prod (0 rows) — that branch is dead; the gate is really the org-owner check. Canonical re-expression is schema-expressible (a tier check via GET /identity/tier, which exists) but tier_grants holds 0 client/T4/T4A grants, so a re-expressed gate returns false for everyone. Schema: expressible. Data: absent.
  • V5 (tier_grants expressibility). Schema (id, user_id, tier, client_id, granted_by, granted_at, revoked_at, notes) can express membership (client_id + user_id) + role (tier), and getMembers already implements exactly the sites-9/13 read shape. But 0 client-scoped grants exist — the membership data is unmigrated in workspace-db user_tenant_roles / ops-db org_memberships. Schema: yes. Data: absent.

Both V3 and V5 resolve to park-and-file on the data axis regardless of routing path — neither can route against an empty canonical store.

5. Conclusion

3d's canonical routing is coupled to first-client onboarding, not a standalone pre-launch refactor. The missing pieces — the clients entity and the canonical write endpoints (tier-grant-create, portal-invite-create, client-provision) — are UI-coupled: they exist to serve the onboarding UI, which is parked. Building them now is building against an unbuilt interface; they get built with it, not ahead of it.

6. Decisions (Tim, Gate-1 fork call, 2026-05-29)

  1. Path C — 3d parked. No conversion executed. 3d parks into the first-client-onboarding arc, alongside the identity-management-UI and test-candidate-RTO work already queued there.
  2. Foundation deferred to build-with-the-UI. The clients entity and the three write endpoints are not built ahead of the onboarding UI. They are built with it.
  3. Data stays UI-only. No operator hand-editing of canonical identity; no seed migration of the workspace-db residue into the canonical store. The empty canonical store is the intended clean slate per CANONICAL-IDENTITY-VIA-UI-ONLY / ADR-027.
  4. Data line, for the record: No customer identity enters the canonical store except through the UI path. None hand-granted.

7. V4 loose end — third write surface

The drift re-grep confirmed all 14 read sites hold (minor line drift only) and surfaced one inventory gap the original audit missed: apps/site/app/api/members/route.js is a third identity-write surfaceINSERT INTO portal_invites (line 53) and user_tenant_roles writes (lines 105/117/119) — beyond the provision + invite writers named in the execute brief's §5. When 3d eventually runs inside the onboarding arc, members/route.js writes fold into the write conversion per MIGRATION-COMPLETION-DISCIPLINE, or it is decommission-and-leave.

8. Forward placement

  • 3d (apps/site identity direct-bind retirement) → first-client-onboarding arc, gated behind the canonical foundation being built with the onboarding UI.
  • Canonical foundation (clients entity + tier-grant-create / portal-invite-create / canonical client-provision endpoints) → build-with-the-UI, same arc.
  • tenants/org_memberships → clients/tier_grants data canonicalisation → resolved by onboarding (UI-only), not by migration.
  • apps/site retains its WORKSPACE_DB (and ops_db) bindings until the arc runs — consistent with the audit's separate APPS-SITE-DB-BINDING-RETIREMENT-01 / ops-db retirement forward briefs; nothing in 3d's parked scope removes them.

The map is captured so the onboarding arc inherits it rather than re-deriving it. 3d was a real finding, cleanly parked — not abandoned.