Skip to content

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.

  • 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, LTI subs, OneRoster sourcedIds, 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 agents map 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.id is 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:key in 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.
  • DID as the person root. did:key cannot rotate (key loss = identity loss), did:web dies 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 (sub is 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.
  • A real migration: person.id decouples from auth.users (the cascade FK is removed; auth becomes a binding). Every RLS policy and pgxsinkit row filter that assumes sub == 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_binding grows a binding-kind/custody dimension; guardianship needs a person-to-person relationship table.