Architecture Decision Record

0018. SCIM 2.0 provisioning and lifecycle

Context

Workforce identity is not only authentication; it is lifecycle — users and groups created, updated, and deprovisioned as people join, change roles, and leave, driven from an external IdP or HR system. SCIM 2.0 is the standard for that, and parity with Entra/Okta requires it: inbound (an external source of truth provisions principals into Vord) and outbound (Vord provisions principals into downstream applications). The decision that earns an ADR here is not the REST plumbing; it is how deprovisioning relates to the revocation model (ADR-0005), because the gap between "removed from the directory" and "can no longer authenticate at the edge" is a security-critical one, and because SCIM's query surface brushes directly against a convenience the constitution forbids.

Decision

  1. SCIM endpoints are a management-plane surface (ADR-0021) that writes the directory (ADR-0011) on the strongly-consistent core. Provisioning is issuance-side: creating a principal and its unique identifier index entry (realm-scoped, ADR-0011) is exactly the irreducible core write rule 2 permits. A provisioning call returns the consistency token of ADR-0004, so a just-provisioned principal is immediately usable read-your-writes (the New-Enemy-free guarantee applied to "the user I just created can log in now"). Because stock provisioners (Entra, Okta) speak plain SCIM and cannot carry a consistency-token extension, the SCIM endpoints also pin reads internally: a query on a resource this endpoint recently wrote is served at-or-after that write's version, so the standard create-then-filter pattern never sees its own write missing and never duplicate-creates. The token is for Vord-aware automation; the internal pinning is for everyone else.

  2. Deprovisioning is the join with revocation. A SCIM DELETE or deactivate is one durable act with a bounded edge effect: the handler writes the directory (the principal is gone/inactive in the strong core) and emits the deprovision event in the same commit; the revocation path of ADR-0005 — the signed status list plus the NATS urgent push, both projections of that event (ADR-0022) — then kills the principal's already-issued, still-unexpired tokens at the edge within the freshness budget (ADR-0039). There is no second, separate "remember to revoke" write to forget: the event is the trigger, recorded atomically with the directory write. What remains between removal and edge-death is the propagation window itself — bounded, published, and instrumented, never a dual-write race.

  3. SCIM queries are served from projections, within a tenant — never a cross-tenant strong scan. SCIM's list/filter/PATCH semantics are exactly the "search all users" convenience the constitution refuses on the hot path (ADR-0001, rule 3). Filtered reads are answered from per-tenant projections of the directory; there is no cross-tenant strongly-consistent enumeration.

  4. Inbound and outbound share the directory model. Inbound SCIM writes Vord's directory; outbound SCIM reads it (from projections) and pushes to downstream apps. Both are tenant/realm-scoped (ADR-0012) and respect the same uniqueness constraints (ADR-0011).

  5. SCIM endpoint authentication is OAuth, not a standing bearer secret. The provisioner authenticates as a confidential client (ADR-0038) via client credentials (ADR-0014) with a dedicated provisioning scope, so SCIM access is rotatable, revocable, and audited like every other client. A compatibility mode — a long-lived static bearer token, which is what most incumbent provisioner UIs expect to paste — is supported per realm but modeled as a client credential under the hood (hashed at rest, rotatable, revocable via ADR-0038's kill path) and flagged in the admin surface as the weaker posture.

Consequences

  • Vord matches the workforce-lifecycle table stakes (joiner/mover/leaver automation) while keeping provisioning a clean strong-core write and queries off the strong core — the constitution honored on both the write and the read side.
  • The deprovision→revocation join makes "offboard this employee and kill their live sessions" a single, bounded-freshness operation, instrumented by the same revocation-lag signal as all revocation (ADR-0005) — a security property with a measurable SLA rather than a hopeful eventual cleanup.
  • Returning a consistency token on provisioning means downstream automation can chain "create then immediately use" correctly across replicas — the SDK responsibility of ADR-0004 applied to the provisioning path.
  • This is a roadmap (seat-reserved) capability: the EventSink emit points (ADR-0022) exist from M0 and the revocation propagation path lands at M2, so by the time SCIM ships (M7) both of its dependencies are long-standing — it bolts on without re-architecting. (Corrected from an earlier draft that overstated the M0 surface: at M0 only the emit seam exists, not the projections.)