Person-rooted identity: every external identifier is a binding
Status: accepted
Three concurrent pressures converged on one architectural point: the OneRoster
projection (ADR-0012) needs SIS sourcedIds attached to persons; research governance
(ADR-0008) needs guardians who may never log in; and CLR 2.0 / Open Badges 3.0 export
needs credential-subject identifiers that outlive this instance. Meanwhile person.id
is today a hard FK to Supabase auth.users with cascade delete — a person cannot
exist before an auth account does, and deleting an auth account destroys the person
and all learning history, contradicting the no-destruction lifecycle principle.
Decision
Section titled “Decision”- The Person row (instance-local UUID) is the root of identity. No external identifier is ever the primary key.
- Every external identifier is an
identity_binding(provider / issuer / subject, effective window): Supabase auth subjects, LTIsubs, OneRostersourcedIds, CLR credential-subject URIs, and DIDs alike. Auth is demoted from identity root to one binding kind among peers; a Person may have zero auth bindings. - Guardians are Persons. Guardianship is a person-to-person relationship (kind +
effective window), modeled with the consent work (ADR-0008). OneRoster
agentsmap onto it at the boundary. - Credential subject identifiers are pairwise per-issuance by default
(platform-minted URI + hashed-email IdentityObject) to limit cross-verifier
correlatability. A stable, learner-owned DID is an opt-in binding, never the
default. Note: CLR/OB do not require DIDs (
credentialSubject.idis optional); DIDs are staged by choice, not dependency. - Learner key custody is a per-binding policy, staged as a ladder: (1) keyless
subjects — no learner keys at all; (2) custodial
did:keyin platform KMS when holder binding is wanted; (3) wallet-held keys via an export path. Each rung is a new binding row, not a migration. - Issuer keys are the immediate key-management scope: platform and per-org issuer
Profiles with KMS-held signing keys (HTTPS JWKS or
did:web), required for any signed CLR/OB export. Learner keys are deferred to rung 2.
Considered and rejected
Section titled “Considered and rejected”- DID as the person root.
did:keycannot rotate (key loss = identity loss),did:webdies with the domain, and custody migration can legitimately change a learner’s DID; each ordinary event would be a primary-key crisis. DIDs enter as bindings — the self-sovereign ceiling without the fragility floor. - Canonical platform HTTPS URIs as subject ids. Instance-bound: identity dies with the domain, the exact failure the portability goal exists to avoid. Used only as the default pairwise per-issuance identifier, where instance-boundness is acceptable.
- Keeping
person.id = auth.users.id. Convenient for RLS (subis the person id) but blocks rostered/invited/guardian persons without accounts and couples learning history to auth-account lifetime. - A single stable DID per learner from day one. Maximally correlatable across verifiers; the wrong privacy default under GDPR.
Consequences
Section titled “Consequences”- A real migration:
person.iddecouples fromauth.users(the cascade FK is removed; auth becomes a binding). Every RLS policy and pgxsinkit row filter that assumessub == person.id(claim-aware policies, ownership filters) must resolve person via the auth binding instead — its own design item, required before any SIS consumer lands. - Cross-instance import is identity correlation, not transfer: the receiving instance mints a new Person and binds the imported identifiers.
- Custodial key destruction becomes an erasure primitive (crypto-shredding) for the GDPR deletion path.
identity_bindinggrows a binding-kind/custody dimension; guardianship needs a person-to-person relationship table.