0011. The principal and directory data model
Context
Vör verifies tokens and Modgud issues them, but neither says what a principal is — the subject the warden is bound to — nor where the directory lives. This is the first substantive identity-data decision, and it is constrained from two directions at once. The constitution keeps the strongly-consistent core minimal (ADR-0001 rule 2: only issuance, uniqueness, and freshness-critical revocation belong there), so a rich user directory cannot all be strong state. And the scale targets are large: a workforce tenant of 500K+ principals and a CIAM realm of millions of users, served by the same system. The model must hold both without putting directory size anywhere near the verification path.
Wyrd anticipated exactly this workload: "principals, credentials, attributes, and relationship tuples are all kilobytes; they never chunk or erasure-code" (Wyrd ADR-0018), and reserved metadata-op-rate as a first-class scaling dimension (Wyrd ADR-0018 §4) and namespace partitioning per tenant (Wyrd ADR-0020). The directory is a consumer of that metadata tier, not a new store.
Decision
-
A principal is identified by an immutable, opaque internal id, assigned at creation and never reused, distinct from every mutable external identifier (username, email, employee number, federated subject). Tokens and relationship tuples (ADR-0006) reference the internal id; humans and external systems reference the mutable identifiers, which are resolved to the internal id through an index. Renaming or re-emailing a principal is an index update, never an identity change.
-
Account, identity, and credential are separate records. The principal is the subject; credentials (ADR-0015) are the means of proving it and live apart, so a principal may hold several (password, passkey, federated link) without schema churn; profile/attributes are eventually-consistent, cacheable, edge-distributable data that are not in the strong core. At creation, every principal is also assigned its per-principal data-key reference (ADR-0034) — the envelope key its PII is encrypted under — because retrofitting per-principal encryption is a full-directory migration.
-
The directory lives on Wyrd's metadata tier as small, schema-versioned records, written through the consistency contract (ADR-0004). The only facts held strongly-consistent are the identity-defining and uniqueness-constraining ones: the principal record's existence and the unique identifier index entries, written atomically with the principal — exactly the irreducible writes rule 2 permits. Everything else about a principal is eventually-consistent.
Two properties of the index are fixed now, because the index key shape is the hardest thing in the directory to migrate:
- Uniqueness is realm-scoped: one principal per
(realm, identifier). A workforce tenant's default realm spans the tenant, so the familiar "one principal per(tenant, email)" holds there unchanged; a CIAM tenant running a realm per application or per end-user population (ADR-0012) gets separate principal pools per realm — registrations in one app neither collide with nor leak into another. A tenant that wants one pool shared across its apps models them as clients of a single realm: the sharing becomes an explicit modelling decision, not an accident of the index. - The index key is a keyed blind index, not the identifier in clear. The
entry is keyed by
HMAC(realm-index-key, normalized-identifier), so the strongly-consistent core never holds an email or username in clear — lookup still works (compute the HMAC, fetch), uniqueness still works (the HMAC collides exactly when the identifier does), and crypto-shredding (ADR-0034) can genuinely cover identifiers: a per-principal envelope key cannot protect a lookup index that must be searchable before the principal is known, but a blind index plus index-entry deletion on erasure can.
- Uniqueness is realm-scoped: one principal per
-
Designed for cardinality, not just correctness. The identifier index and the per-tenant directory are structured to address millions of principals per tenant with lookups whose cost is independent of tenant size, and the scaling dimension is per-tenant metadata-op-rate (Wyrd ADR-0018 §4), not dataset size. Crucially, directory size never reaches the verification hot path: Vör reads the token, not the directory (ADR-0010), so a 10-million-user realm verifies exactly as fast as a 10-user one.
-
One principal model, realm-typed. Workforce and CIAM share one schema; their differences (self-service registration, social linkage, progressive profiling) are realm policy (ADR-0012), not a second principal type. Where a CIAM-specific shape proves genuinely irreducible to policy, it is added as an optional, versioned extension — never a fork of the core record. (Whether any such shape exists is the open question flagged for resolution before this model is stamped stable.)
Consequences
- The strong core stays small even as the directory grows huge: uniqueness and existence are strong; profile and attributes are not. The "search all users globally" convenience the constitution forbids (ADR-0001) is served from per-tenant projections, never a cross-tenant strong scan (rule 3).
- The immutable-id / mutable-identifier split makes rename, re-email, and identifier
reassignment safe and makes the token's
substable for the life of the principal — a property relying parties depend on. - The model is the foundation the OAuth surface (ADR-0014), authentication methods (ADR-0015), provisioning (ADR-0018), and federation (ADR-0019) all build on; its schema version travels with each record (the ADR-0003 versioning rule applied to directory data).
- [OPEN] the exact set of CIAM extensions that policy cannot express and that therefore warrant a versioned extension — resolved before the directory schema is stamped stable. OAuth consent state is no longer part of this item: ADR-0037 decides it as a versioned directory record. What remains open is marketing-preference state and anonymous→known promotion (in particular whether promotion is id-preserving under decision 1).