Skip to content

Latest commit

 

History

History
724 lines (637 loc) · 47.1 KB

README.md

File metadata and controls

724 lines (637 loc) · 47.1 KB

Identity — Module Specification

Global/contextual storage, common audit, seed handoff and existing-binding API admission are implemented. Derived from jointly Accepted ADR-0072 and ADR-0073, 2026-10-25. The global User/authority model, internal binding/adoption consumers and central creation owner exist. Contextual tables/commands/manifests below are implemented; the Phase 03 Status block owns current qualification and review state. Jointly Accepted ADR-0076 / ADR-0077 define P03-2 live authorization and one administrative directory GET. Its confined reader, canonical policy/evaluator, central denial evidence and HTTP directory are wired in the API composition. The additional ADR-0078 and ADR-0079 contracts are Accepted. Self-entry storage/HTTP, deliberate native account creation and context selection, bounded administration routes and native forms are implemented. Selected protected views consume immutable snapshots prepared by the native response owner; RSC rendering holds no credential or API-operation capability. The phase Status block alone owns current packet, qualification and review state. The bounded contract separates these obligations without treating acceptance as runtime proof. The ordered admission timing/observation contract additionally derives from ADR-0074, Accepted 2026-10-26. The phase Status block owns qualification and review state. The fixed-seed prerequisite source derives from ADR-0080; its acceptance does not qualify the new owner transition or seed race proofs. The separate fixed-origin completion contract derives from Accepted ADR-0081; its implementation and proofs remain governed by the phase Status block.

Overview

Identity owns global application bindings, contextual Membership/Profile/ assignments and tenant-wide Role templates. Keycloak owns credentials/provider authentication; Tenancy owns Tenant/Organization lifecycle; Audit owns audit rows. Invitations add the scoped offer and its verified acceptance contract under ADR-0093; its first writers remain subject to the phase Status block. Cross-module facts use application contracts, without an Organization FK, navigation or database join. Branch-specific Role configuration is not selected.

Global implementation and deployment

P03-1 Step 3 implements one global User/authority model and migration chain, the common prepared creation owner and eight central audit seams. API composition denies seed adoption; the ordinary seed/demo handoff is implemented in Step 4. Existing-binding API admission uses the same read-only contract. The separately classified global-self bootstrap route uses the bounded entry contract below; ordinary API admission never creates a User.

Set both Identity__AuthorityId and Authentication__Tenant__Issuer in the selected private deployment environment before make migrate installs/verifies the authority. Supply the migration connection and complete pair together, either as all three process variables or in the selected private environment file. The migration owner refuses partial input and never combines the two sources; a complete process selection takes precedence over file defaults. Schema-only process input also refuses a file that supplies an authority pair. The ID is deployment-pinned and never derived from an issuer URL. The dedicated deployment child receives the migration credential only in its environment; restore/build and API processes receive no migration credential. An exact existing registration is a no-write verification. A conflicting pair refuses without repair/rebind. With both keys absent, migrations create the schema only; missing authority cannot admit a binding. A partially configured pair refuses before migration writes. See the environment example and the canonical authority contract.

Schema-only migration does not satisfy API startup. Every API host, including one serving only anonymous reads, requires both runtime configuration keys and the matching installed authority. Startup qualification rejects missing, unavailable or mismatched registration before accepting HTTP requests. Direct dotnet run reads process/.NET configuration, not the root .env. Restore the intended configuration when it differs from the selected database; an issuer rebind remains P03-9/G13 and is not an automatic repair.

The internal binding service uses explicit startup qualification and fresh, physically read-only existing-binding transactions; it never creates a missing binding. Fixed seed adoption separately requires its confined admission port. Real provider/app-role, schema/body/ACL/caller and physical outcome controls are implemented. The directory contract appears below; closure, production first-admin and issuer rebind retain their named later owners.

Aggregate and storage contract

User's authority is ADR-0072; the canonical accepted database annex owns its exact global SQL, grants and guards.

Table Class / owner Keys, scope and lifecycle
memberships Tenant-owned, org-scoped; Membership root UUIDv7 MembershipId; referenceable (tenant_id,id); current unique (tenant_id,user_id,organization_id) NULLS NOT DISTINCT WHERE deleted_at IS NULL
membership_profiles Tenant-owned, org-scoped; contained Membership child PK (tenant_id,membership_id); one profile created with Membership; exact parent organization mirror/lifecycle
membership_role_assignments Tenant-owned, org-scoped; contained Membership child PK (tenant_id,membership_id,role_id); exact Membership organization mirror; Role capability compatibility guard
roles Tenant-owned, tenant-wide; Role root UUIDv7 RoleId; referenceable (tenant_id,id); current unique (tenant_id,permission_scope,built_in_key) WHERE deleted_at IS NULL; no organization_id
role_permission_grants Tenant-owned, tenant-wide; contained Role child PK (tenant_id,role_id,permission_scope,permission_key); exact Role capability-scope mirror; no organization_id

Use each Standard 05 table class, its EF filter and ENABLE/FORCE RLS. Org-scoped tables receive canonical restrictive write guards and immutable organization triggers. Parent references use (tenant_id,parent_id) with ON DELETE RESTRICT; index every FK. Independent INSERT/UPDATE mirror triggers compare nullable organizations with IS DISTINCT FROM. Never rely on a nullable organization column in a FK to check tenant integrity under MATCH SIMPLE.

Assignments reference Membership and Role through two tenant-composite FKs. Membership's user_id references same-module users.id with ON DELETE RESTRICT and its own supporting UserId index. The permanent User tombstone retains the key; audit actor columns still have no User FK. The BEFORE admission guard maps unknown and nonadmitting targets to the same refusal before FK diagnostics can be exposed, and its fixed diagnostic translation discloses no constraint values. Their trigger checks the exact Membership organization mirror, then requires Tenant capability iff that organization is NULL and Organization capability iff it is non-NULL. Role has no organization to match. Grant children reference Role through its tenant-composite FK and mirror capability scope. Tenant, User/parent references, Role built-in key and capability scope are immutable. Role built-in/scope CHECKs admit only the finite manifest pairs; no free-form Role key or Platform variant enters these tables. Soft-deleted Roles/Revoked or Suspended Memberships do not contribute live grants. Tenant natural-key indexes remain partial while referenceable primary-key-containing indexes remain full.

learnstack_app receives SELECT/INSERT/UPDATE on Membership/Role/Profile and SELECT/INSERT/DELETE on assignment/grant children, subject to all guards. Root revocation is soft; no app root/Profile DELETE exists. Child changes go through the owning root and tracked audit capture. No PUBLIC/Platform/dispatcher Identity privileges or new login are introduced. Global grants/functions follow ADR-0072. Each migration declares exact grants: a privilege never identifies a code path.

Permission definitions remain compiled code, without a new global Permission table. Persisted grants select (scope,key) from the registry; unknown/inactive entries never authorize. Standard 19's accepted foundation contract owns the exact registry, version-1 grants and generated kernel artifact.

Invitation contract

ADR-0093 owns the decision. The following defines the P03-4 boundary. The phase Status block owns delivery.

Subject Contract
Invitation root Tenant-owned, org-scoped typed InvitationId; immutable normalized recipient email, inviter UserId, nullable OrganizationId, one compatible RoleId and offered Role version; no invitee UserId selector
Pending uniqueness One Pending offer per tenant/org/normalized email, null-safe, including an expired Pending offer awaiting explicit reissue
Lifecycle Pending → Accepted or Revoked; expiry is evaluated from database UTC after waits; Accepted/Revoked are terminal
Accepted disposition Immutable accepted actor, original MembershipId and created/already_member; retained consumed verifier permits only own-result recovery
Generation Create stages generation 1; explicit reissue advances generation, token and job, using the current TTL; old codes are invalid immediately
Email and verifier Email is personal data; current/consumed domain-separated SHA-256 token verifiers are sensitive credential material; all raw values are excluded from audit/diagnostic sinks

CreateInvitationCommand automatically uses IInvitationEmailStager to enroll its encrypted envelope and identifier-only typed external job on the Invitation's ordinary unit of work. Invitation, envelope, job and MUST audit commit together. Reissue creates a new generation/envelope/job; it does not repin changed Role content. A lost response is resolved through the ordinary bounded receipt, without a recoverable link in replay storage. No prepare/handoff or raw-link-read API is part of this contract. The private SQL-only delivery tables are common Infrastructure custody, not Identity aggregates or a new notification module.

Administrative create/reissue use existing live Membership-admin admission, exact resource scope and current whole-Role delegation. Revocation is withdrawal: current exact-scope membership-admin, Pending target and strong root version are required; a changed/deleted Role or uncovered offered grant does not prevent withdrawal. No Role assignment or reissue authority follows. All admin readers, mutations and original receipts additionally compare tenant and exact nullable organization; an Organization context must not disclose a tenant-wide offer merely because the base EF/RLS filters admit that row. Invitable-Role summaries disclose only delegable ID/key/scope/version, without a general Role/registry reader. Own inspection and own accepted-result reading are Off; other-person offer lists/detail, invitable Roles and protected receipt reads are MUST sensitive reads. Mutations remain MUST. The matrix owns each consumer binding; the phase Status owns source-bound qualification.

ICurrentInvitationRecipientReader performs a new configured-issuer UserInfo observation for each Pending inspection/acceptance: exact sub, one valid email, Boolean verified=true, 16 KiB/five-second bound and no business locks during I/O. Email is matched under ADR-0093's explicit bounded mailbox policy; it does not identify or merge Users. A provider edit can race database commit. Keycloak owns registration and verification through the fixed same-subject VERIFY_EMAIL continuation; actual provider verification is required. No global email mirror is added to User.

The confined fn_identity_invitation_entry accepts the application-derived 32-byte verifier digest, independent host tenant and verified authority/subject key. It returns only exact offer/actor facts and the stored inviter's global admission Boolean; it does not prove delegation or expose arbitrary User state. Its disposed read owner precedes InvitationEntryScope, the eleventh logical tenant setter, whose single-use witness admits only exact inspection/acceptance/ own-result consumers. This ordinary owner is operation-bound ReadOnly or ReadWrite according to the admitted consumer, not a general scope hatch. New User bootstrap commits and disposes first. Neither owner joins or retargets the other. The database contract owns the exact closed function/setter/source ledger; Security owns host/source and native authority.

AcceptInvitationCommandHandler is the sole added two-root exception alongside provisioning: one Invitation and at most one Membership with contained Profile and assignment, one ordinary transaction, central plural MUST intents. It writes no Role, User or Tenancy root. The complete sorted invitee/stored-inviter User plan precedes the Tenant fence. Preserve the canonical strongest mode derived from actual effects: shared for nonadmin effects, exclusive for actual Tenant Admin eligibility/assignment effects, without upgrades or caller exemptions. Fresh reads after waits recheck actor, generation, database expiry, lifecycle, pinned Role and the stored inviter's complete current delegation. Standard 05 alone owns lock ordering and retained-reader details.

Exact target Membership Outcome
None Create one Active Membership with Profile and the offered assignment
Active Consume as already_member; retain that exact Membership and all Roles/profile
Suspended Conflict without consumption/reactivation
Only Revoked history Create a fresh successor ID and children; never resurrect history

The Invitation's accepted disposition is centrally captured. The unchanged Membership outcome emits its separately declared subject-only security event, without fabricated before/after or arbitrary handler metadata. A required audit failure rolls back both roots, envelope/job staging and their state changes.

The dispatcher routes an already Accepted offer to GetOwnInvitationResultQuery before declaring mutation intents. That Off read requires the same fresh Active actor, consumed verifier and original current Active Membership. It precedes Pending-only expiry/version/Role/inviter checks and does not require current email. A Pending mutation that discovers a raced acceptance returns 409 with truthful failed-attempt evidence; it cannot report a duplicate successful grant. Unknown COMMIT stays indeterminate until authoritative result recovery.

Native capture and provider sending follow the architecture journey. Revoke/reissue/accept invalidates old codes. Prepare refuses an obsolete generation before its plan commits; already-prepared mail may still arrive. Business withdrawal adds no ordinary provider-table cancellation writer. 250 is accepted submission, not inbox delivery. Queued, submission accepted, failed, uncertain, expired and revoked remain distinct product states.

Invitation storage foundation

The Invitation aggregate and its Identity migration map invitations with the canonical tenant/org filters, FORCE RLS, same-module reference keys, explicit credential redaction and guarded finite transitions. Email is normalized through InvitationMailbox; invitation authoring and recipient matching reuse that parser. SMTP receives the validated staged address and independently enforces transport/header safety. The local dot-atom is 1–64 ASCII bytes, the full mailbox at most 254, and each ASCII DNS label at most 63. No provider identity is selected by email.

The exact preliminary SQL function returns bounded offer/actor facts without email or grants. Its operation-bound HTTP/pipeline activation belongs with the three actual recipient operations; schema availability grants no public entry. The phase Status owns implementation and qualification.

Lifecycle, profile and admission

The database requires Active/Suspended Memberships to have NULL deletion fields and Revoked to carry both deletion timestamp/actor. Its INSERT/UPDATE lifecycle guard rejects direct resurrection, inconsistent status/deletion markers and transitions outside the diagram. Active/Suspended share current uniqueness; Revoked is terminal. Rejoin creates a new ID and fresh assignments after retiring the predecessor. Profile has no independent delete/status lifecycle; historical child facts cannot restore a revoked Membership's authority.

Profile carries optional contextual DisplayName (128 characters) and PreferredLocale (35-character well-formed locale tag), both marked sensitive. No unused contact field is collected. Missing values use explicit safe UI fallback, without copying global fields on reads. NULL-org profiles contain no branch-private notes; organization profiles are independent. Self/admin changes allow those fields only, never canonical email/verification, global identity, Role bindings, arbitrary settings or consent evidence. The implemented Membership contracts below carry these exact fields; P03-5/G9 owns dynamic fields/revisions. No dormant custom-fields column lands in the foundation.

Creation, Suspended→Active and all assignment additions recheck the target User fence, including additions of currently empty/inactive templates. Standard 05's accepted lock contract alone owns the algorithm, ordering, fresh reads, deadlines and database guards. Profile/nonadmin/organization mutations use its shared tenant invariant mode; first-admin and tenant-admin eligibility/assignment/kernel transitions use its exclusive mode. Actual affected rows bind the mode before acquisition; no shared-to-exclusive upgrade or caller-declared nonadmin exemption is admitted. Only the admission operations above, including invitation acceptance, automatically require the User fence; profile-only changes and assignment removals retain their existing tenant-lock requirements. Standard 05 also owns backend-heldness limits, fixed internal plan fault versus target refusal, and the identical outward refusal. No algorithm or new User reader is defined here.

Every Membership mutation, in either lock mode, leaves an Active, globally eligible, tenant-wide Tenant Admin with both kernel pairs. The first mutation therefore establishes that administrator. Role roots may exist first; completed foundation is actual eligible administration plus semantically valid required templates, without a bootstrap Boolean, receipt or table. Trial, Active and Suspended retain their admin. No Archived/termination exemption exists until separately admitted by G13/G16. Final-admin removal/suspension/revocation or kernel stripping refuses. Future global closure cannot make the final admin ineligible before its separate recovery/termination authority and guards are Accepted and implemented.

P03-1 supplies the bounded Tenancy Application.Contracts port IIdentityScopeLifecycleReader.ReadAsync(TenantId, OrganizationId?, CancellationToken). Its immutable IdentityScopeLifecycle result has four Booleans: TenantExistsAndNotDeleted, TenantAllowsOrdinaryWork, OrganizationBelongsToTenant and OrganizationAllowsOrdinaryWork. Organization facts are false when no organization is requested; it returns no row or list. Trial/Active admit ordinary work; Suspended retains an admin without ordinary-work eligibility. Organization work requires Active and nondeleted, not just belonging. Missing targets return closed false facts.

The target must match an already trusted, announced owner scope. The Tenancy implementation reads its own tables in one fresh READ COMMITTED statement on that app-role transaction; no cached lifecycle result, additional setter, cross-module SQL join or Organization FK is admitted. P03-1 Step 4 supplies the port/implementation and seed controls; live admission executes it at the declared per-act snapshot. Implemented Membership transitions and Organization lifecycle commands enforce last-admin/default-organization invariants under fresh ordered locks and are exposed through protected HTTP routes. The Phase 03 Status block owns qualification; the foundation port alone does not establish those concurrency proofs.

First-admin and seed sequence

P03-1 admits only the fixed development-seed capability. ISeedIdentityFoundationAdmission is DenyAllSeedIdentityFoundationAdmission in API composition. The external seed/demo owner supplies bounded canonical manifest facts and actual provider-adoption witnesses to the confined Seeder implementation. Email, a realm role or SystemActor alone supplies no admission. There is no HTTP entry, operator population or general bootstrap bypass.

Production first-admin/provisioning admission is P03-9/G13, before a production provisioner uses it. Its authority/recovery cannot inherit the seed capability. Tenancy's current ProvisionTenant remains the ADR-0042 Tenant/default Organization pair; no cross-module transaction or further exception is admitted.

  1. The external seed owner completes Tenancy acts and ADR-0072 trusted binding adoption, preserving all four actor/provider IDs, credentials and MFA.
  2. InitializeSeedRole writes one Role and grants per transaction from version 1. Eight templates per tenant include three Organization-capability variants; there are no per-organization Role rows or organization-initialization step.
  3. CreateFirstSeedAdminMembership atomically writes the declared admin, contained Profile/Assignment and central MUST audit, referencing an existing Tenant Admin template. Its complete root/child plan precedes mutation; User/tenant/last-admin guards apply.
  4. CreateSeedMembership writes each learner and contained state separately. SetSeedMembershipProfile changes bounded fields through Membership. No command writes another root merely because its transaction is available.
  5. Fresh acts use deterministic version-1 declarations and exact postconditions. Warm completion verifies named immutable roots and a valid installed graph; matching completion performs no writes. Valid suspension, historical revocation, profile/assignment changes and adopted grants are preserved without reset, reactivation or successor creation. Only a recognized pristine incomplete prefix can resume; a missing contained grant or assignment is not evidence that it was never installed. Recheck each possible write's current evidence under its fence/version. Edited partial, ID-conflicting or invalid/foreign state refuses before remaining writes, preserving earlier commits.

The expected fingerprint is not a new receipt table. Existing Role/Membership facts and canonical pristine-prefix evidence establish completion/resume; keep historical manifests for earlier completed acts. Registry additions do not change grants. Legitimate later adoption or removal of nonkernel profile writing is preserved, without turning seed into an upgrade reconciler. The exact evidence, per-write recheck and held-seed/admin race controls belong to Standard 05. Tenancy's separate seed-origin adoption does not grant Role authority or repair Identity children.

The four Identity seed writers use Standard 05's closed temporary prerequisite operation only after their complete ordered plan and target preparation. A separate Identity projection checks the fixed declaration's retained history across the target Tenant's branches using metadata only; ordinary seed readers keep their current-organization predicates. Tenancy prerequisites run narrow first, and the common owner restores narrow scope before returning any usable result. The Identity metadata reader does not depend on the Tenancy guard; a closed composition service joins owning contracts without a runtime dependency cycle or module request types in the common coordinator. It returns no profile values, raw editor evidence or Membership directory. Standard 05 owns source binding, state/write barriers, cleanup and the admitted-writer threat boundary.

Origin adoption has a distinct completion witness and reader, without joining the eight pristine consumers. Identity's closed owning contract checks declared Role/Membership identities, immutable scope/User associations, retained lifecycle/stamps, required children and a valid current finite grant/ assignment graph on the prepared adoption owner. The temporary hatch reveals branch facts only inside this operation and clears before admission returns. Required contained profiles exist and their owning Membership aggregate version is positive; version zero refuses. This is current retained disposition, not proof that the original profile initializer ran. Lawful empty/edited profiles, nonkernel grant removals and retained terminal nonadmin Memberships are preserved. A revoked former admin requires a qualifying local replacement; the current predicate still requires an Active undeleted NULL-org Membership assigned a live Tenant-scoped Tenant Admin Role with both kernel pairs. It neither reads global User/provider state nor freshly attests the original writers' globally eligible administrator invariant. Those writers retain their existing invariant and guards. Completion adds no login, assignment, reactivation or repair authority, and returns no profile values or raw relationship/editor evidence. Standard 05 owns the exact local predicate, marker-2 historical repeat and confinement rules.

Identity also supplies the separate closed ordinary-scope prerequisite for the twelve content seed writers: absence of all retained Tenant-wide Role history, including tombstones and undeclared IDs. Role DELETE is denied, so any retained Role proves the later v1 Identity stage began even when branch Membership facts are invisible. This contract neither borrows the temporary hatch nor exposes a Role directory.

Existing actor Membership scope Assignment
English admin (a001 suffix) English / NULL organization Tenant Admin, Tenant capability
English learner (a002) English / NULL organization Learner, Tenant capability
Yoga admin (b001) Yoga / NULL organization Tenant Admin, Tenant capability
Yoga learner (b002) Yoga / default studio Learner, Organization capability

SeedData owns the full legacy actor/tenant/organization IDs. Exact assignment declarations extend that manifest; suffixes above identify existing actors, not newly allocated IDs. The four authentication personas remain fixed. Additional cross-tenant/two-branch and same-tenant-second-person proof populations are fixture-local, outside IdentityUsers and its exact four-user realm manifest. Email never merges them.

Yoga admin b001 keeps its existing provider claim for default studio b1. Its seed Membership declaration independently selects NULL organization, and the seed transaction derives NULL SQL organization scope from that trusted target; the authentication manifest does not choose the Membership write scope. A branch session may consume eligible Tenant grants for branch operations and explicitly declared tenant-wide reads. Tenant-wide Membership/Profile/template writes require an explicitly selected orgless context through P03-3's validated native admission transition. Neither the existing branch claim nor app.scope promotes writes; no extra branch-admin Membership or provider-claim rewrite is required.

Accepted live authorization and directory contract

Live policy, central evidence and the administrative directory are implemented. The policy/evaluator consumes the confined reader/source through the shared decision port; common Application gains no Identity module dependency. The directory uses the real EF read adapter and common MUST write path. API and application checks each read fresh committed authority before dispatch. CommittedIdentityAuthorizationReader coordinates the module statements from Application; its Infrastructure session factory retains the exact Identity SQL and physical child owner. The reader consumes an exact verified actor/tenant/optional-organization candidate in an isolated child owner. It returns bounded current Membership/Role/grant facts and consumes Tenancy's lifecycle contract, without a profile/global directory, synthetic TenantContext or parent transaction reuse. The grant and lifecycle statements have separate committed snapshots; revocation committed before the grant query affects that admission, while already-admitted work can finish. Binding remains the earlier global User eligibility check.

HTTP and direct application dispatch independently classify and authorize each protected act. Explicit public, AccountSite, protected and finite seed/system policies default deny unknown/missing declarations. Resolver membership confirms scope but does not authorize a later protected act. Public requests remain free of identity/grant lookups; AccountSite keeps its separate real HostScope ceiling and Off/physical ReadOnly mode while gaining live membership/lifecycle admission.

The implemented administrative directory uses existing Tenant identity.membership.admin, without requiring the new v2 grant delta. It admits HostAndClaim, confirmed ClaimAndMembership or independently verified HostAndNativeSessionScope, never HostOnly. Native evidence retains the real-host ceiling. Its declared tenant-wide read can use an eligible NULL-org Tenant Admin Membership from a branch session. It returns current undeleted Active/Suspended Memberships and only MembershipId, UserId, OrganizationId, status, contextual DisplayName and PreferredLocale. Provider subject, email, global profile and grant graph are excluded. The API contract owns exact route, DTO/cursor/no-store rules. The native BFF's closed operation union selects the generated directory operation using its held credential and admitted context; it exposes no arbitrary method, path, headers or browser scope selector.

Its MUST identity.membership.list row commits on the ordinary ReadWrite owner before disclosure; it is not a physical ReadOnly-catalogue query. The implemented ChangeRoleGrantsCommand and AdoptRoleGrantManifestCommand each mutate one Role with contained grants, expected version, current post-lock authority and same- transaction MUST audit. Permissions owns their exact bindings and delegation ceiling; unknown manifest, self-escalation or kernel removal refuses. The accepted warm contract preserves valid differing adopted state. Protected HTTP grant-editing/adoption routes and their native forms are implemented. They retain current authorization, expected-version and protected idempotent-receipt checks; the Phase 03 Status block owns qualification.

Standard 05 owns the dedicated four/two authorization/evidence lanes, tracked job ownership, retained cleanup and required direct/PgBouncer progress proofs. Its read authority contract owns invocation/transaction lifetime and EF/RLS agreement. Standard 18 owns denial occurrences, immutable provenance and all pre-success barriers. No pool, crash, permission or disclosure proof is claimed by acceptance alone.

Bounded entry and administration contract

Accepted under ADR-0078/0079. The phase Status block records current delivery, qualification and review state for the implemented entry, administration and native presentation mechanisms. Keycloak authenticates the provider subject. Native scope evidence remains a separate immutable source; it never rewrites provider claims. The API binds the User, validates actual host and live Membership/lifecycle, then applies each ordinary operation's current capability and target rules. Global-self lookup, deliberate opt-in bootstrap and own-context discovery have only their three exact early-branch descriptors; no ordinary request allocates a User. Creating identity provides neither Membership nor a tenant session. Discovery exposes only the verified person's own eligible context choices, not another person's data.

Six application requests read existing Memberships and edit their contained profile, assignments or lifecycle. GetMembershipDetailQuery returns the target's MembershipId, UserId, OrganizationId, status, DisplayName, PreferredLocale, RoleIds and version. Its ordinary ReadWrite owner commits MUST sensitive-read audit before disclosure. Only the matched Tenant membership-admin clause may widen detail reads; the Organization clause remains in the exact current branch. GetOwnMembershipProfileQuery is Off and module-declared physically ReadOnly. It selects the actor's exact Active current Membership, without a target selector or NULL-org fallback, and returns only DisplayName, PreferredLocale and version.

SetMembershipProfileCommand requires applicable membership-admin and profile-write; SetOwnMembershipProfileCommand requires applicable profile-write and the exact actor-owned current Membership. Both replace only nullable DisplayName and an enabled PreferredLocale on an Active target. ChangeMembershipStatusCommand suspends, reactivates or revokes; ReplaceMembershipRoleAssignmentsCommand replaces at most eight distinct, same-tenant, capability-compatible Roles on an Active or Suspended target. Empty assignments remain subject to the database's total-admin invariant. Unchanged assignments retain their child instances; no-op mutations do not advance the Membership version. Suspension and prepared assignments confer no live authority; Revoked remains terminal. There is no guessed-UserId create/rejoin path; P03-4 owns invitations/successors.

All four writers retain exact context scope and expected Membership version. They recheck current authority, lifecycle, target graph and complete lock plan after waiting. Assignment additions check each Role's whole current grant set against the actor's live ceiling under retained Role-root locks; profile-only changes need no Role lock. Actual User/administrator effects select the canonical fences, with no shared-to-exclusive upgrade. The owning transaction validates the graph/last-admin invariant and commits MUST audit with the change. Module mutation results retain the exact MembershipId and version for later transport.

The closed HTTP/OpenAPI surface uses these application consumers and the current-branch directory, Role list/detail/registry, own-context and capability reads. The SDK supplies injected, bounded operation wrappers; native/browser activation and qualification remain governed by the phase Status block. Receipt disclosure uses the six closed Identity/Tenancy application reads described below.

Own-current-Tenant context navigation uses Tenancy's named IOwnTenantNavigationReader contract on the existing ordinary ReadOnly owner. Its separate IOwnTenantNavigationCoverage provider reads fresh own Membership facts before and after label selection, bound to the exact active own-context invocation. A tenant-wide own Membership permits active branch choices; otherwise only the current branch participates. This adds no general organization reader, tenant-wide scope hatch, setter or owner function. Cursors bind the actor, Tenant, organization and coverage mode. Own-capability preview projects nine finite flags without running denied operations or caching authorization. The API wire boundary owns byte-bounded page selection before continuation; ordinary short records keep the requested count and large escaped labels retain complete subsequent pages.

The existing Role grant/adoption writers use the eight finite templates. Standard 19 owns the five-plus-three registry, explicit v2 exception, monotonic marker and unchanged kernel. Standard 05 owns exact Role/Membership lock plans, adoption guards, warm witnesses and durable protected receipt metadata. Member-detail Tenant read alternatives may mint the existing read witness; exact-branch alternatives stay scoped, and writes never inherit that hatch. Receipt reads reauthorize the stored original target and commit a fresh MUST read event before retained bytes can be returned. Membership, profile, own-profile and Role reads use exact original IDs, including retained terminal targets; own-profile disclosure also requires the original current actor and scope. They neither rerun the mutation nor substitute a successor Membership. The Tenancy receipt reads cover the original Organization or Tenant default resource with their current disclosure capability. Ordinary readers keep their current-only filters; receipt reads add no general history or directory surface.

Permissions owns capability and advisory-preview bindings; audit owns actual and planned classifications. Exact HTTP/native routes and DTOs derive from the joint decision package. UI hints are not grants; actual API/application/target checks remain authoritative. Acceptance closes no runtime, race, provider, browser or accessibility proof by itself.

Entity-relationship diagram

erDiagram
    User ||--o{ Membership : "stable identity reference"
    Membership ||--|| MembershipProfile : contains
    Membership ||--o{ MembershipRoleAssignment : contains
    Role ||--o{ MembershipRoleAssignment : "tenant-composite reference"
    Role ||--o{ RolePermissionGrant : contains
    Membership {
        uuid tenant_id
        uuid organization_id "nullable; immutable"
        uuid user_id
        text status
    }
    Role {
        uuid tenant_id
        text permission_scope "capability; no organization column"
        text built_in_key
    }
Loading

Text fallback: User has contextual Memberships; each contains Profile and assignments to tenant-wide Roles. Roles contain grants. OrganizationId is a Membership value, without a cross-module FK/navigation.

State diagrams

stateDiagram-v2
    [*] --> Active: admitted creation
    Active --> Suspended
    Suspended --> Active: User admission rechecked
    Active --> Revoked: deletion markers
    Suspended --> Revoked: deletion markers
    Revoked --> [*]
Loading

Text fallback: Active and Suspended exchange; either revokes. Revoked is terminal, with a distinct successor on rejoin. Global Active/Closing/Closed admission and permanent binding follow ADR-0072, not a Membership state transition.

Sequence diagrams

sequenceDiagram
    participant Seed as External seed owner
    participant Role as InitializeSeedRole
    participant Member as CreateFirstSeedAdminMembership
    participant DB as App-role PostgreSQL
    participant Audit as Central audit path
    Seed->>Role: Exact v1 template and closed seed witness
    Role->>DB: One Role plus grants; commit
    Seed->>Member: Declared admin and existing Role
    Member->>DB: Standard 05 admission/invariant guards
    Member->>DB: Membership, Profile and Assignment
    Member->>Audit: MUST intent with captured children
    Audit->>DB: Same-transaction audit
    DB->>DB: Deferred invariant guards
    DB-->>Seed: Commit and fresh semantic verification
Loading

Text fallback: Roles commit separately; first admin/children commit with audit. Failure leaves no partial Membership; earlier Role acts are verified on resume. There is no published primary integration-event flow in this foundation; see the empty catalogue.

Component diagram

flowchart LR
    Seed[External seed/demo owner] --> Contracts[Identity Application.Contracts]
    API[API: seed admission denied] --> Contracts
    Contracts --> Application[Identity Application]
    Application --> Domain[Identity Domain]
    Application --> Tenancy[Tenancy lifecycle contract]
    Application --> Shared[SharedKernel transaction/audit ports]
    Infrastructure[Identity Infrastructure] --> Application
    Infrastructure --> PostgreSQL[PostgreSQL: learnstack_app]
    Shared --> Audit[Central Audit implementation]
Loading

Text fallback: Application uses domain, Tenancy application contracts and shared ports. Infrastructure persists through the app role. API seed admission is denied; global/provider adoption remains ADR-0072's confined owner.

Integration-event catalogue

P03-1 publishes no Identity integration event and activates no provider ingress, Invitation or closure consumer. P03-4 defines the bounded invitation writers above without an email-bearing integration event; P03-6/10 own later consent/ closure contracts. Framework domain-event dispatch does not imply a published contract.

Permission matrix

Identity permissions binds production seed commands to the exact accepted Standard 19 manifest. The seed commands are implemented in P03-1 Step 4; the separately marked P03-2 bindings are implemented. Capability metadata alone grants neither permission nor a route; canonical admission and exact endpoint declarations enforce both.

Audit coverage matrix

Identity audit declares implemented root/child classifications and personal/reference paths and the implemented authorization, Membership read/write and Role grant rows. The central writer owns durability; Identity has no audit table/direct audit SQL. Global mutation audit follows ADR-0072 alone.

Performance budget

The canonical initial performance budgets apply: API p95 below 200 ms reads / 500 ms writes, excluding separately reported provider calls. The Step 4 record owns the small local command-path measurement and its limits; it establishes no production capacity. The distinct Step 5 binding-load observation retains its own control and scope. Standard 05 owns bounded lock waits, cancellation and metrics. Timeout yields a fixed retryable failure without partial grants. Candidate-reader/authorization cost measurements belong to the Phase 03 qualification; a global permission cache/session grant cannot replace a fresh check.

Risks and open questions

Branch-specific Role configuration reopens ADR-0073. Accepted ADR-0076/0077 own live delegation, default deny/revocation, reader handoff and trusted evidence. Those mechanisms and the bounded scope transitions/DTOs are implemented; Phase 03 Status owns qualification. P03-4 invitations, P03-5 dynamic values, P03-9 production first-admin/recovery and P03-10 closure/locators remain named later writers, not implicit foundation authority.

Subject/control Current state Owner / scope
Models, commands and registry Global and contextual foundation implemented P03-1 Steps 3/4; live authorization P03-2
Canonical marker/filter/RLS/FK/unique/aggregate/audit controls Identity extensions implemented with populated positive/dirty controls P03-1 Step 4; catalogue owns the individual obligations
Permission_Definitions_DeclareScope Implemented for the finite foundation and live operation contracts IdentityFoundationConventionTests, OperationPermissionConventionTests and actual command metadata; catalogue owns live authorization controls
Mapped_Properties_Have_Explicit_Personal_Data_Classification Implemented for all current models, including contextual Identity DataProtectionConventionTests; complete census and neutral-name omission controls
Identity_Seed_Uses_Finite_Adopted_Manifests Implemented; no-write/interrupted/conflict actual seed controls IdentityFoundationConventionTests / IdentitySeederTests; Phase 03 Status owns qualification
Identity_Membership_Invariants_Use_Fresh_Ordered_Locks Implemented actual app-role races, live Membership transitions and Organization lifecycle/default invariants IdentityMembershipTests, IdentityMutationLockTests, IdentityFoundationConventionTests, MembershipAdministrationRaceTests and TenancyAdministrationRaceTests; broader closure/locators remain P03-10
Per-sink PII canaries/child audit/MUST rollback Implemented actual child snapshots/redacted diffs, MUST rollback and value-bearing diagnostic controls IdentitySeederTests, IdentityDiagnosticSinkTests, TenantExistingBindingAdmissionTests and canonical data-protection proof ownership; classification alone does not establish sink safety
Global_Identity_Storage_Is_Subject_Confined / Global_Identity_Creation_Uses_The_Common_Audit_Owner Implemented global storage/creation, exact self-entry HTTP and deliberate native activation GlobalIdentitySchemaTests, IdentityAuditConventionTests, GlobalIdentityBootstrapTests, IdentitySelfEntryHttpTests and native session-store.test.ts; Phase 03 Status owns qualification and reviews

Registered means an actual catalogue entry exists. Until then a new control is planned/unregistered; later Registered is not Implemented. No empty census, skipped suite, type declaration or diagram supplies implementation evidence.