---
id: DOM-PFM-SEC-003
title: User Account
realises: CON-PFM-SEC-003
product_area: platform
module: security
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
owned_by: SVC-PFM-SEC-security
readable_by:
- SVC-PFM-SEC-tenants
- SVC-PFM-SEC-customers
lifecycle: STATE-PFM-SEC-004
custom_fields: none
identity: Each membership receives a generated user identifier when invitation, confirmed portal registration
  founding-staff reconciliation or the retained direct-create route creates it. A membership belongs to one
  customer console or one workspace portal. An address may recur across these boundaries, but not within one
  boundary. Portal pool usernames use the member identifier. Sign-in identities use issuer and subject; definitions,
  assignments, grants and service principals retain their own stable identities.
attributes:
- name: wall and access type
  kind: enum
  required: true
  computed: false
  notes: 'A permanent membership boundary: one customer console or one workspace portal.'
- name: e-mail address and profile
  kind: collection
  required: true
  computed: false
  notes: Canonical sign-in address plus names, picture, time zone and locale; routine edits preserve the address.
- name: membership status
  kind: enum
  required: true
  computed: false
  notes: Membership state and the recorded times of invitation, activation, suspension and revocation.
- name: last authentication
  kind: date
  required: false
  computed: true
  notes: Interactive context reads update this timestamp no more than once per minute.
- name: invitations
  kind: collection
  required: false
  computed: false
  notes: Invitation records hold a token digest when issued, seven-day validity, delivery results and attempts;
    only one outstanding invitation is allowed.
- name: sign-in identities
  kind: collection
  required: false
  computed: false
  notes: Pool accounts, linked Google identities and short-lived sessions carrying the evidence for provider
    linking.
- name: linked contact
  kind: reference
  required: false
  computed: false
  notes: An optional CRM contact reference for a portal member, without a database foreign key to CRM.
- name: lifecycle and password events
  kind: collection
  required: true
  computed: true
  notes: Append-only records of membership lifecycle and password actions.
- name: former-user record
  kind: collection
  required: false
  computed: true
  notes: Retained identity and name after deletion, allowing earlier records to identify their actor.
- name: pool-account removals
  kind: collection
  required: false
  computed: true
  notes: Durable requests to disable or delete provider accounts after membership removal commits.
- name: sign-in pools
  kind: collection
  required: true
  computed: false
  notes: 'Console and workspace-portal pool metadata: issuer, clients, addresses and declared status.'
- name: portal contact challenges
  kind: collection
  required: false
  computed: false
  notes: Ten-minute candidate-contact challenges that retain a code digest and attempt count.
- name: permission catalog
  kind: collection
  required: true
  computed: true
  notes: Release-declared permission keys, classification, sensitivity, staff restrictions and catalog history.
- name: policies
  kind: collection
  required: true
  computed: false
  notes: Named allow/deny rule sets with optional conditions. Release-controlled built-ins coexist with customer-owned,
    versioned custom policies that can be archived.
- name: roles
  kind: collection
  required: true
  computed: false
  notes: 'Policy compositions and inline rules: five customer built-ins, three staff built-ins and custom customer
    roles. Custom roles can bind an eligibility rule and refresh interval.'
- name: role assignments
  kind: collection
  required: true
  computed: false
  notes: Roles or inline rules assigned to a member or CRM contact, with scope, optional validity and conditions,
    and derivation metadata when source-managed.
- name: staff access grants
  kind: collection
  required: false
  computed: false
  notes: Recorded approved, emergency or standing staff entry, including target, reason, ticket, decision,
    validity and the materialized assignment.
- name: service principals
  kind: collection
  required: true
  computed: false
  notes: Registered machine callers, classified by kind and operating status.
- name: security history
  kind: collection
  required: true
  computed: true
  notes: Security changes and classified refusals carry actor, scope, entity and impact metadata. The ordinary
    writer appends, scoped purge can remove events, and general database immutability or credential-payload
    rejection is not established.
- name: security context
  kind: computed
  required: true
  computed: true
  notes: An evaluated response containing reachable workspaces, applications, scoped permissions and staff
    HQ keys; computed on demand rather than persisted.
relationships:
- to: DOM-PFM-SEC-001
  cardinality: many console memberships and custom definitions to one customer
  ownership: Console memberships and custom definitions store their owning customer reference.
  cross_service: false
- to: DOM-PFM-SEC-002
  cardinality: many portal memberships, assignments and staff access grants to one workspace
  ownership: Portal memberships, scoped assignments and grants refer to a workspace. Workspace purge removes
    the portal memberships and their dependent identities and assignments.
  cross_service: false
invariants:
- id: INV-DOM-PFM-SEC-003-01
  statement: A membership has one console or portal boundary; an e-mail address is unique within its customer
    console or workspace portal.
  enforced_at:
  - service
  - database
  provenance: observed
  source: services-platform:modules/security/db/users.table.ts:users (users_console_email_address_uidx, users_portal_email_address_uidx)
- id: INV-DOM-PFM-SEC-003-02
  statement: Fresh authorization refuses invited, suspended and revoked memberships. An earlier cached decision
    can remain usable until its applicable expiry.
  enforced_at:
  - service
  provenance: observed
  source: services-platform:modules/security/authorizer/authorize.ts:authorize (step 9)
- id: INV-DOM-PFM-SEC-003-03
  statement: Only one invitation may remain outstanding for a membership; resend supersedes the earlier row
    before creating its replacement.
  enforced_at:
  - service
  - database
  provenance: observed
  source: services-platform:modules/security/db/user-invitations.table.ts (user_invitations_live_uidx)
- id: INV-DOM-PFM-SEC-003-04
  statement: Invitation tokens are retained as hashes, expire after 168 elapsed hours and are consumed by a
    conditional update that admits only a usable invitation.
  enforced_at:
  - service
  - database
  provenance: observed
  source: services-platform:modules/security/services/persistence/invitations.ts:acceptInvitation
- id: INV-DOM-PFM-SEC-003-05
  statement: Authorization resolves existing sign-in identities without creating them; a provider identity
    can link to only one account within a pool.
  enforced_at:
  - service
  - database
  provenance: observed
  source: services-platform:modules/security/db/external-identities.table.ts (external_identities_pool_provider_uidx);
    modules/security/authorizer/authorize.ts:authorize (step 8)
- id: INV-DOM-PFM-SEC-003-06
  statement: 'An assignment names exactly one subject: a membership or a CRM contact.'
  enforced_at:
  - database
  provenance: observed
  source: services-platform:modules/security/db/security-assignments.table.ts (security_assignments_subject_check)
- id: INV-DOM-PFM-SEC-003-07
  statement: Assignment scope must remain inside the subject’s boundary. Platform scope requires a console
    membership in Rhythm’s customer; contacts are limited to their workspace and its record-type scopes.
  enforced_at:
  - service
  - database
  provenance: observed
  source: services-platform:modules/security/services/persistence/assignments.ts:assertScopeInsideWall
- id: INV-DOM-PFM-SEC-003-08
  statement: Evaluation uses active assignments within their validity windows. Pending assignments grant no
    access and revoked rows are terminal.
  enforced_at:
  - service
  - database
  provenance: observed
  source: services-platform:modules/security/db/security-assignments.table.ts:ASSIGNMENT_STATUSES
- id: INV-DOM-PFM-SEC-003-09
  statement: Uniqueness prevents duplicate non-revoked role grants for a subject, scope and window, and duplicate
    live derived rows for a role and resource key.
  enforced_at:
  - database
  provenance: observed
  source: services-platform:modules/security/db/security-assignments.table.ts (security_assignments_live_grant_uidx,
    security_assignments_derivation_uidx)
- id: INV-DOM-PFM-SEC-003-10
  statement: Derived rows carry source and key metadata. Source synchronization controls them; manual amendment
    and withdrawal are refused.
  enforced_at:
  - service
  - database
  provenance: observed
  source: services-platform:modules/security/services/persistence/assignments.ts:assertAssignmentNotDerived
- id: INV-DOM-PFM-SEC-003-11
  statement: Delegation checks require unconditional possession at the target scope and refuse grants to the
    caller’s own membership, subject to the documented founding-administrator exception.
  enforced_at:
  - service
  provenance: observed
  source: services-platform:modules/security/services/invitations/widening.ts:assertMayGrant
- id: INV-DOM-PFM-SEC-003-12
  statement: Guarded changes roll back if no qualifying customer security administrator or no active Rhythm
    Administrator would remain.
  enforced_at:
  - service
  provenance: observed
  source: services-platform:modules/security/services/membership/lockout.ts:guardedAgainstLockout; modules/security/services/membership/last-administrator.ts:assertRhythmAdministratorRemains
- id: INV-DOM-PFM-SEC-003-13
  statement: Release reconciliation is the permitted writer for built-in definitions, rules and policy attachments;
    other writers are rejected.
  enforced_at:
  - service
  - database
  provenance: observed
  source: services-platform:modules/security/services/catalog/reconcile-catalog.ts:reconcileCatalog
- id: INV-DOM-PFM-SEC-003-14
  statement: Custom definitions belong to a customer. Role composition refuses another customer’s policy, archived
    policies and staff-only definitions.
  enforced_at:
  - service
  - database
  provenance: test
  source: services-platform:modules/security/tests/integration/db/security-definition-integrity.integration.spec.js#refuses
    another customer's policy, the one way authority could cross a wall through a definition
- id: INV-DOM-PFM-SEC-003-15
  statement: Custom definitions are retained by archiving. Existing role assignments and policy references
    prevent archive.
  enforced_at:
  - service
  provenance: observed
  source: services-platform:modules/security/services/definitions/manage-roles.ts:archiveCustomRole
- id: INV-DOM-PFM-SEC-003-16
  statement: Reconciliation increments a built-in definition’s version only when its materialized content changes.
  enforced_at:
  - service
  provenance: observed
  source: services-platform:modules/security/services/catalog/catalog-plan.ts:planBuiltInPolicy
- id: INV-DOM-PFM-SEC-003-17
  statement: Cross-customer staff assignments require a matching active grant for member, role, scope and validity
    window.
  enforced_at:
  - service
  - database
  provenance: observed
  source: services-platform:modules/security/services/staff-access/materialize-grant.ts:materializeStaffAccessGrant
- id: INV-DOM-PFM-SEC-003-18
  statement: Active-grant unique indexes protect staff target uniqueness. Open requests use a sequential precheck,
    with concurrent exclusion unknown. Approved entry lasts one to thirty days; emergency entry is at most
    240 minutes and needs a reason of twenty characters or more.
  enforced_at:
  - service
  - database
  provenance: unknown
  source: services-platform:modules/security/db/staff-access-grants.table.ts (staff_access_grants_active_uidx,
    EMERGENCY_MAX_MINUTES, EMERGENCY_REASON_MIN_LENGTH); modules/security/services/staff-access/request-tenant-access.ts:requestTenantAccess
- id: INV-DOM-PFM-SEC-003-19
  statement: Reconciliation preserves a manually revoked standing grant rather than recreating access at that
    target.
  enforced_at:
  - service
  provenance: observed
  source: services-platform:modules/security/services/staff-access/ensure-standing-access.ts:ensureStandingAccess
- id: INV-DOM-PFM-SEC-003-20
  statement: Ordinary security writers append events and classified refusals follow rollback. Workspace purge
    can remove scoped events; general database append-only and credential-payload rejection guarantees are
    unknown.
  enforced_at:
  - service
  provenance: unknown
  source: services-platform:modules/security/services/audit/refused-attempts.ts:auditedAttempt; modules/security/services/persistence/audit-events.ts:recordSecurityAuditEvent
- id: INV-DOM-PFM-SEC-003-21
  statement: Membership deletion retains a former-user identity and name and refuses unresolved references
    from other records.
  enforced_at:
  - service
  - database
  provenance: observed
  source: services-platform:modules/security/services/invitations/user-deletion.ts:deleteUser
- id: INV-DOM-PFM-SEC-003-22
  statement: Challenges use digests, ten-minute expiry and five-attempt accounting. Sequential replacement
    supersedes earlier rows and link writes check existing claims, but concurrent single-challenge and single-contact
    exclusion are unknown.
  enforced_at:
  - service
  - database
  provenance: unknown
  source: services-platform:modules/security/services/portal/contact-match.ts:startContactChallenge; modules/security/services/persistence/users.ts:linkPortalMemberContact;
    modules/security/services/persistence/portal-contact-challenges.ts:startPortalContactChallenge
physical:
- platform.users
- platform.user_invitations
- platform.user_lifecycle_events
- platform.user_password_events
- platform.authentication_identities
- platform.external_identities
- platform.identity_link_sessions
- platform.former_users
- platform.pool_account_removals
- platform.identity_pools
- platform.portal_contact_challenges
- platform.security_catalog_releases
- platform.security_permissions
- platform.security_policies
- platform.security_rules
- platform.security_roles
- platform.security_role_policies
- platform.security_assignments
- platform.derived_assignment_reconciles
- platform.derived_assignment_reconcile_jobs
- platform.derived_assignment_reconcile_job_steps
- platform.derived_assignment_reconcile_job_events
- platform.staff_access_grants
- platform.service_principals
- platform.security_audit_events
- platform.authorizer_flush_jobs
- platform.authorizer_flush_job_steps
- platform.authorizer_flush_job_events
- platform.tenant_portal_onboarding_jobs
- platform.tenant_portal_onboarding_job_steps
- platform.tenant_portal_onboarding_job_events
evidence:
- 'services-platform at ad3f6a65: modules/security/db/*.table.ts; services/persistence/assignments.ts, invitations.ts;
  services/invitations/*; services/membership/*; services/staff-access/*; services/catalog/*; services/definitions/*;
  services/portal/contact-match.ts; services/audit/refused-attempts.ts; authorizer/authorize.ts; docs/row-level-security.md
  (security assignments and audit events are exempt).'
- 'react-console at baf6dae: lib/security/api.ts, lib/security-context (what the console reads of the account).'
examined_at: '2026-10-23'
examiner: codex
---

# User Account

## What It Models

User Account is the planning concept for a person’s membership and authority. Its design groups four aggregates: membership and sign-in identities; roles, policies and permission definitions; scoped assignments; and staff access grants. Each aggregate retains its own lifecycle and transaction boundary. Service principals and security history are adjacent records owned by Security.

## Identity and Attributes

Each membership receives a generated user identifier when invitation, confirmed portal registration founding-staff reconciliation or the retained direct-create route creates it. A membership belongs to one customer console or one workspace portal. An address may recur across these boundaries, but not within one boundary. Portal pool usernames use the member identifier. Sign-in identities use issuer and subject; definitions, assignments, grants and service principals retain their own stable identities.

The attribute rows distinguish stored fields, computed answers and supporting records.

## Relationships

Console memberships and custom definitions store their owning customer reference. Portal memberships, scoped assignments and grants refer to a workspace. Workspace purge removes the portal memberships and their dependent identities and assignments.

## Invariants

The structured invariant rows identify each protection, its enforcement layer and its evidence. Read their qualifications with the statement rather than treating a service check as a database constraint.

## How It Is Stored and Shared

A customer-console membership and a workspace-portal membership are distinct even when the same person uses both. Definitions describe permissions; assignments apply them to subjects and scopes. The evaluator also supplies baseline self-service permissions. A linked CRM contact allows contact-derived assignments to contribute to a portal member’s access.

## Open Design Questions

Security owns these records and exposes administration operations and evaluated context. Other services synchronize derived assignments through the shared capability in their own transactions. Workspace provisioning invokes Security to invite a first administrator. Assignments and security history do not use the ordinary per-workspace row policy because their access boundaries differ.

The database table inventory excludes the separate gateway route registry. These records have no custom-field enabling declaration. Fresh permission evaluation and cached permission use have different timing; a gateway decision can remain cached for sixty seconds and its signed context can remain usable for thirty more. Provider synchronization is separately best effort or queued, with documented failure and recovery limits.

The existing planning grouping is retained. Product still needs to settle contact-derived subject semantics and the desired consistency between suspension and durable account removal. The design records those disagreements rather than changing the planning concepts.
