PII Separation Architecture
Authrim stores sensitive identity values separately from the identity state used for authentication and authorization. An account can have its Core and PII data on different D1 databases; runtime code resolves the destinations for that account before reading or writing them.
This page describes the current Control Plane routing and canonical identity model. It does not imply that every field outside the PII database is anonymous, or that separation itself encrypts data.
Data boundaries
Section titled “Data boundaries”The following are logical roles, not a fixed list of three database bindings. A deployment can contain multiple Core, PII, and Lookup databases.
| Role | Contents and responsibility |
|---|---|
| Core | Identity subjects, accounts, their links, lifecycle and verification state, profile attribute metadata, and references to sensitive values. Tenant metadata also has its own resolved Core destination. |
| PII | Sensitive attribute values in identity_sensitive_values, linked identities, subject identifiers, and deletion records. Compatibility tables such as users_pii remain in the schema. |
| Lookup | Blind indexes and routing projections used to locate an account’s Core and PII destinations. This is a separate lookup role, not the authoritative store for profile values. |
| Admin / Control | Administrative state, database inventory and registry, placement and routing information, field catalogs, and operational state. These are not the account’s PII store. |
KV also carries signed runtime registry documents and caches. Protocol state and other caches have their own storage paths. Separating the D1 roles does not mean that user-related data exists only in D1.
Resolving an account’s databases
Section titled “Resolving an account’s databases”D1 placement and routing are owned by the Control Plane. Runtime profiles configure audit delivery and residency policy; they no longer select a shared-d1, tenant-d1, or external-durable storage topology.
For the account route used by runtime-data-context.ts:
- Authrim verifies the Lookup bucket assignment and HMAC key-state documents from the runtime registry.
- It builds blind indexes for an email address, external issuer/subject pair, or account ID and resolves the matching membership. The result must identify exactly one account in the requested tenant.
- It resolves the account’s
tenant_core/usersandtenant_piidestinations using signed registry information, including binding, shard, residency partition, and route generation. - It revalidates the destination against shard metadata and account state before using it. A Core-only operation can avoid reading PII values.
flowchart TB
Request["Tenant and account identifier"] --> Lookup["Lookup: blind index and account route"]
Lookup --> Verify["Verify registry and destination"]
Verify --> Core["Core: account state and value references"]
Verify --> PII["PII: sensitive values"]
The registry resolver checks physical Core/PII separation and rejects invalid or unavailable routes. Missing bindings, mismatched generations, quarantined routes, and invalid signatures are errors, not permission to use an arbitrary shared database.
This routing path currently accepts D1 providers. The presence of PostgreSQL or MySQL adapters elsewhere in Authrim does not make them interchangeable providers for these account routes.
The canonical identity model
Section titled “The canonical identity model”Runtime user operations use CanonicalRuntimeUserStore, its projection repository, and CanonicalRuntimeUserWriter. They combine a tenant-scoped Core adapter with the resolved PII adapter.
| Data | Current representation |
|---|---|
| Subject and account state | identity_subjects, identity_accounts, and subject_account_links in Core. Account state includes lifecycle, registration, directory publication, and route generation. |
| Profile attributes | profiles and profile_attribute_values in Core. An attribute can contain an inline value or a reference to a sensitive value. |
| Contact points | contact_points in Core holds contact metadata, verification state, and value references. A contact’s metadata is distinct from the email or phone value it refers to. |
| Sensitive values | identity_sensitive_values in PII stores tenant_id, owner, value key, value_json, classification, and lifecycle state. |
| External and subject identifiers | PII-side tables include linked_identities and subject_identifiers; their identifiers must not be described as universally Core-only data. |
The writer’s piiFields and sensitiveValues inputs select sensitive profile values. inlineProfileFields writes inline attributes to Core. Field classification and the calling path therefore matter: the architecture does not automatically move every potentially identifying value into PII.
Core also contains security-sensitive account metadata, including a password hash where one is stored. It still requires access control and careful logging. “Core” is a storage role, not a guarantee that its contents are public or non-personal.
Lookup, reads, and encryption
Section titled “Lookup, reads, and encryption”Lookup uses keyed HMAC blind indexes, with normalization and key-generation metadata. This lets the routing lookup find destinations without storing the original identifier as its lookup key. It does not make every hash-shaped field in the identity schema a cryptographic blind index: for example, canonical contact references and value_hash have separate purposes, and the writer can leave value_hash empty.
Operations that need a profile value still read PII. Some store methods, including email and preferred-username searches, query sensitive values. Do not assume that all authentication or search paths are PII-free.
The canonical writer serializes sensitive values into value_json; it does not encrypt each value at the application layer in that write path. Storage separation, database/platform encryption, and optional application-layer encryption are different controls. Likewise, encryption support for PII logging does not mean that all profile values are encrypted by that facility.
Account deletion and retained records
Section titled “Account deletion and retained records”The routed Admin user-deletion handler coordinates more than a lifecycle flag:
- It resolves the account and checks for an active legal hold. An active hold prevents deletion.
- It moves authentication state to
deletingand requests session revocation. - It prepares removal from the account directory and creates a deletion tombstone if one does not exist.
- It clears runtime-user sensitive
value_jsonvalues, removes relevant linked/subject identifiers, and clears raw values in identifier-change workflows covered by the deletion routine. - It marks the account and subject deleted, makes directory-removal work ready for delivery/retry, invalidates the user cache, and records the deletion event.
CanonicalRuntimeUserWriter.deleteRuntimeUser() alone marks lifecycle state; it is not a substitute for this coordinated erasure flow. Core records and retained deletion/audit records are not all physically removed by account deletion. Other copies, exports, archives, and application-owned data need their own retention handling.
Tombstones
Section titled “Tombstones”users_pii_tombstone records the original user ID, tenant, deletion time, actor/reason, retention deadline, and optional metadata and email blind index. The repository defaults to 90 days; the routed Admin deletion handler explicitly uses 90 days and leaves the email blind index empty.
Consequently, that handler’s tombstone must not be advertised as an automatic email re-registration block. A deletion record is also not necessarily anonymous simply because it omits the original email address.
Expired records are excluded from active email-tombstone checks. Physical removal is a separate cleanup operation, exposed through the Admin tombstone cleanup API; the retention timestamp is not a D1 row TTL.
Operational considerations
Section titled “Operational considerations”- Restrict access to Core, PII, Lookup, and administrative data according to their actual contents. Validate tenant ownership as well as the storage destination.
- Review field classification before storing custom attributes, labels, or metadata inline in Core.
- Keep registry signatures, route generations, and Lookup key rotation in the operational plan. Do not bypass route validation to recover a failed request.
- Review audit payloads, caches, exports, and retention settings separately. A deletion event is not evidence that every PII read is audited or every copy has been erased.
- Treat writes spanning Core, PII, and Lookup as coordinated operations with failure/retry handling, not as a single cross-database SQL transaction.
Implementation references
Section titled “Implementation references”The behavior above is implemented in these source files:
packages/ar-lib-core/src/services/runtime-data-context.tspackages/ar-lib-core/src/services/tenant-database-resolver.tspackages/ar-lib-core/src/types/runtime-profile.tspackages/ar-lib-core/src/services/lookup-directory/blind-index.tspackages/ar-lib-core/src/repositories/identity/canonical-runtime-user-writer.tspackages/ar-management/src/admin-users.tspackages/ar-management/src/account-directory-removal-producer.tspackages/ar-lib-core/src/repositories/pii/tombstone.ts