docs(adr-0026): person + user portal-side data model (proposed) #213
Reference in New Issue
Block a user
Delete Branch "docs/adr-0026-person-user-schema-proposed"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
ADR-0025 shipped the authorization model with three stubs explicitly deferred to a follow-up ADR:
Principal.user.idandPrincipal.user.personIdcarry the Entraoidas a placeholder —apps/portal-bff/src/auth/principal-builder.tsdocuments the seam.StubScopeResolverreturns[{ kind: 'unrestricted' }]for every signed-in user; the per-persona scope values documented innotes/test-tenant-role-assignments.mdhave nowhere to live.@RequireScope'sScopableResourceshape references anEtablissement/Delegation/Regionchain that has no Prisma table.ADR-0026 specifies the portal-side data model that closes those stubs. Decision-only, status
proposed— implementation lands in two PRs once accepted.What lands
docs/decisions/0026-person-user-portal-data-model.mddata,backend,security. ~310 lines.docs/decisions/README.mdproposed, 2026-05-24).ADR scope
The ADR commits the schema for:
Persongolden record — stable identity, can exist without a portal account (Pléiades pre-provisioning, dossier bénéficiaires, alumni).sourcefield tracks provenance (self-signinin v1;pleiades/acteurs-plusjoin the catalogue with ADR-0027).Userportal-account overlay — one-to-zero-or-one with Person, lazy-created on first OIDC callback. Portal-only state (lastSignInAt, future a11y preferences) rides here, not on the shared Person row.UserScope— confirms the migration whose shape ADR-0025 §"Sources of truth — apf_portal-sideuser_scopestable" already specified.Region/Delegation/Etablissement— organisational hierarchy with externally-meaningful codes (INSEE / French dept / FINESS) as primary keys, matching the on-the-wire shape of the scope literals (etablissement:0330800013,delegation:33).The ADR explicitly defers:
Salarie,Adherent,Benevole,Elu,Beneficiaire,PartenaireExterne) — they track upstream-system shape and ride alongside their producing sync.Both deferrals point at ADR-0027 (Pléiades + Acteurs+ syncs + facet schemas) as the next-up ADR.
Notes for the reviewer
Why
Person+Usersplit and not a single table. The "Considered Options" section walks through this. Option A (User-only) breaks down the moment a dossier holder who never signs in needs a stable identifier. Option D (Person with embedded facet columns) forces every Pléiades or Acteurs+ schema change through a table that both shapes share, which is exactly the kind of coupling that ADR-0027 needs the freedom to design out.Why externally-meaningful primary keys for the hierarchy.
Region.code/Delegation.code/Etablissement.finessare INSEE / FINESS codes — stable across reorgs and already on every URL the portal will mint. Adding a separate UUID would force every consumer to indirect through it for no gain. The trade-off (no in-place rename — if a délégation merges, delete + re-insert with the new code) is bounded by how rarely INSEE rebases.Lazy User creation at first sign-in. v1 has no Pléiades data; the OIDC callback creates the Person + User pair the first time it sees a new
oid. When Pléiades sync ships (ADR-0027), the same provisioner extends with aPerson.externalIdlookup before falling back toemail. The schema does not change between the two regimes — only the provisioner's lookup order does.Person.sourceandEtablissement.kindas enum-as-string + drift-gate extension. The catalogue-drift gate (ADR-0025 §"Confirmation" andscripts/check-catalogue-drift.mjs) was built precisely to constrain hand-edited string columns. Extending it to assert everyPerson.sourcewrite is in the closed catalogue closes one of the soft spots in the v1 schema without standing up a Postgres ENUM.PII posture.
Person.firstName/lastName/emailare flagged in the ADR's Decision Drivers. The Pino redact list (ADR-0012) and the audit-log salt (ADR-0013) already cover Entraoid; the new PII fields ride the same posture (caller-redacted at the audit module, redacted at the Pino logger before the line ships).What "two PRs" means in the More Information section. PR 1: Prisma schema migration +
PersonAndUserProvisionercalled fromSessionEstablisher+ drift-gate extension. PR 2:PrismaScopeResolverreplacingStubScopeResolver+/admin/users/:id/scopesscreen +prisma/seed.tspopulating the 19 test-tenant personas'user_scopesrows.Backward compatibility for in-flight Redis sessions. Sessions minted before the schema migration carry a Principal whose
user.idis the Entraoidplaceholder. The legacy-session bridge inapps/portal-bff/src/auth/principal-extractor.ts(added in #208) already handles this — when the migration deploys, those sessions continue to work for the 12 h absolute-TTL window, after which every session in Redis has the realpersonId.Open questions to resolve before acceptance
These were flagged in the ADR text and are worth surfacing here for the review pass:
Person.emailas the v1 dedup key. The ADR's "Bad, because" item — two Pléiades records sharing an email would crash the unique constraint. ADR-0027 will add theexternalIdprecedence, but in the interim the v1 lazy-creator either trusts emails-are-unique (acceptable for the test tenant where every persona has a distinct one) or skips the dedup attempt and creates a fresh Person peroid. The ADR currently picks the dedup-by-email path; flag if the safer choice is "always fresh, reconcile later".Region/Delegation/Etablissementmigrations be seeded as part of the schema PR, or kept as a separate dataset import? The ADR is silent on this; my default is "seed the geographic codes APF actually operates in" (a one-off SQL fixture in the migration directory). Worth confirming.Test plan
pnpm exec prettier --check docs/decisions/0026-person-user-portal-data-model.md docs/decisions/README.md— clean.Person.sourcecatalogue, theEtablissement.kindcatalogue, and the two open questions above.§More Informationopens (2 PRs).What's next
proposed.proposed → accepted(same cadence as #201 → #205 for ADR-0025).PrismaScopeResolver+ admin-UI scope-seeding + test-tenant seed.