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. clientsexists nowhere canonical. Thetenants → clientscanonicalisation named in ADR-024/ADR-008 was never run. There is noclientstable in any D1. The only org-like entity is the pre-canonical ops-dborgstable.- 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_idpresent → access; elsegroups/user_groupsLIKE%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 viaGET /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), andgetMembersalready implements exactly the sites-9/13 read shape. But 0 client-scoped grants exist — the membership data is unmigrated in workspace-dbuser_tenant_roles/ ops-dborg_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)¶
- 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.
- Foundation deferred to build-with-the-UI. The
clientsentity and the three write endpoints are not built ahead of the onboarding UI. They are built with it. - 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.
- 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 surface — INSERT 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 (
clientsentity +tier-grant-create/portal-invite-create/ canonical client-provision endpoints) → build-with-the-UI, same arc. tenants/org_memberships → clients/tier_grantsdata canonicalisation → resolved by onboarding (UI-only), not by migration.- apps/site retains its
WORKSPACE_DB(andops_db) bindings until the arc runs — consistent with the audit's separateAPPS-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.