Engineering · 2026-04-29
Event Sourcing for Identity State: CRUD and Trust Decisions
By J. W. Bouckaert
The mistake hidden inside every identity database
Open the schema of almost any identity platform shipped between 2010 and 2024 and you will find a users table with a risk_score column, a last_verified_at timestamp, a status enum, and a row-level updated_at. Every successful verification overwrites the previous state. Every risk recalculation replaces the previous score. The database holds the latest answer to the question "what do we believe about this identity right now?" and almost nothing else.
That schema is the single most expensive design decision in the identity industry, and almost nobody calls it out. It is wrong because trust is not a state. Trust is a function of time, evaluated against a sequence of events that happened in a specific order, and that ordering is the only thing that lets you answer the question your fraud team actually has, which is not "what do we believe right now?" but "what should we have believed at 14:07 UTC, given everything that has surfaced since?"
CRUD destroys the answer to that question on the way in.
A trust decision sequence
Consider a concrete sequence, drawn from a real (anonymized) post-incident review:
- 14:03:11—User completes WebAuthn passkey ceremony. Result: success. Confidence: high. Token issued, 1-hour TTL.
- 14:03:11—Carrier signal lookup confirms SIM has been on the same handset for 412 days. Confidence: high.
- 14:03:11—Behavioral telemetry shows typing cadence within 1.2 standard deviations of historical baseline. Confidence: high.
- 14:03:12—Trust decision recorded:
trust_score = 0.94. Authorization granted to high-value endpoint. - 14:31:48—A different carrier (the user's secondary line, on a different MNO) emits an SS7 anomaly signal. The user is not currently using that line. The signal does not change the trust score for the active session.
- 15:48:09—A silent number recycling event surfaces from a third carrier, retroactively explaining why the secondary-line SS7 signal looked anomalous. The recycled number was the user's old contact number.
- 15:48:09—The fraud team needs to know: was the 14:03 verification actually trustworthy in light of what we now know at 15:48? And if not, every downstream decision authorized by that token over the last 105 minutes is suspect.
In a CRUD-shaped store, the answer to step 7 is: we cannot tell you. The 14:03 trust score was overwritten by subsequent recalculations. The signals that fed it were summarized into the score and discarded. The question "what would the trust score have been at 14:03 if the 15:48 signal had been available?" is unanswerable because the 14:03 input vector no longer exists in the database.
In an event-sourced store, the answer is: we replay the event log up to 14:03:11, inject the 15:48 signal into the projection at 14:03:11, and compute the counterfactual trust score. The original event log remains immutable. The new projection is one of many possible projections built from the same source of truth.
This distinction is the entire argument.
CRUD's three failure modes for identity
CRUD-shaped identity stores fail in three specific, predictable ways. Each is a direct consequence of the schema choice, and no patch removes it.
Mode 1: Lost temporal context. When a row is updated, the previous state is gone unless an explicit audit table captured it. Most teams ship audit tables that capture some fields—usually the obvious ones like status and risk_score—and miss the inputs that actually fed those fields. The audit table answers "what did we say?" but not "what did we know?" Six months later, when a regulator asks why a specific decision was made, the audit table cannot reconstruct the input vector.
Mode 2: Race-y multi-signal aggregation. Identity decisions are almost never single-signal. They aggregate carrier signals, device attestations, behavioral telemetry, threat intelligence feeds, and historical baselines—each arriving at different latencies, from different sources, with different freshness windows. CRUD updates serialize these into a single row mutation, which means either you accept lost updates (the last writer wins, signals get silently dropped), or you wrap every update in a transaction and serialize your write throughput at the row level. Neither is acceptable at carrier-signal volume.
Mode 3: Retroactive re-scoring is impossible. This is the one that matters most for fraud teams. When a new threat signal surfaces—a delayed SIM swap detection, a carrier API circuit breaker recovering from degradation with a backlog of late events, a shared-signal CAEP event propagating from a peer IdP—the security team needs to ask "what other recent decisions are now suspect?" In a CRUD store, the answer requires re-running every verification from scratch against new inputs, which the system was never designed to do because it does not retain the original inputs.
| Capability | CRUD Identity Store | Event-Sourced Identity Store |
|---|---|---|
| Latest trust score for a user | O(1) row lookup | O(1) projection read |
| Trust score at a specific historical timestamp | Often impossible | O(log n) replay to that timestamp |
| Counterfactual: "what would the score have been with this new signal" | Impossible without re-verification | Replay log + inject signal at correct position |
| Audit answer to "why did we authorize this?" | Limited to fields the audit table captured | Full input vector replayable from log |
| Concurrent multi-signal updates | Row-level lock contention | Append-only, lock-free |
| Schema evolution | Migrations risk losing historical context | Old events keep their schema; projections evolve |
| Retroactive risk re-scoring across all sessions | Requires re-running every verification | Replay affected event slices |
Event sourcing, strictly
Event sourcing, in its strict form, has three rules:
- State is derived, never primary. The source of truth is an append-only log of immutable events. Any "current state" is a projection built by folding the log.
- Events are immutable. Once written, an event is never modified, never deleted, never reordered. Corrections are themselves new events that supersede earlier ones in the projection logic.
- Multiple projections are first-class. The same event log can be folded into a "current trust score" projection, a "historical decisions" projection, a "regulator audit" projection, and an arbitrary number of counterfactual projections—each living independently and rebuildable from the log at any time.
For an identity stack, this maps to a small, disciplined set of event types:
signal.received—A raw signal arrived from a source (carrier, device attestation, behavioral telemetry, threat feed). Includes source, signal type, payload hash, timestamp, and a provenance reference.signal.classified—A signal was deterministically classified as hard, soft, or unknown per the PBCASCADE classifier rules.verification.attempted—An identity verification ceremony began. Includes the requested assurance level and the policy bundle in effect.verification.completed—A ceremony finished, with its result, the input signal vector at decision time, and the computed score.attestation.bound—A DPoP key was bound to an issued token, with its JWK Thumbprint and the device-attestation chain it inherits.token.issued/token.revoked—Token lifecycle events. Revocations cascade through the revocation engine without mutating the original issuance event.policy.evaluated—A policy decision was made against a specific input vector at a specific timestamp.
Each event carries a monotonic sequence number, a causation ID linking it to the event that triggered it, and a correlation ID grouping it with related events in the same logical operation. None of them ever update. None of them ever delete.
The CQRS half that makes it practical
Event sourcing without CQRS is a tar pit. Reading the current trust score for a user by replaying their entire event log from epoch on every request is not viable at login-path latency.
The split:
- Command side writes to the event log. Append-only, ordered per aggregate, contention-free across aggregates.
- Query side reads from materialized projections. Each projection is a denormalized read model optimized for one specific question.
The projections we maintain in production:
- Current trust state: latest computed score per identity, refreshed on every relevant event. This is what the 50ms verification path reads.
- Active sessions: open token bindings, refreshed on
token.issued/token.revoked/attestation.bound. This is what the revocation cascade iterates. - Audit timeline: full input vector for each decision, partitioned by time, optimized for regulator and incident-response replay.
- Counterfactual workspaces: ephemeral projections built on demand for "what would have happened if" analysis.
When a new event arrives that invalidates a projection's assumptions—for example, a late-arriving carrier signal whose timestamp falls before the most recent decision—the affected slice of the projection is rebuilt from the log. The original log is not touched. The original decision events remain. A new policy.evaluated event records the re-scored decision with a causation link to the late signal. Anyone reading the audit timeline sees both the original decision and the re-scored one, with their full causal context.
Retroactive re-scoring: The concrete mechanism
Return to the post-incident sequence at the top. At 15:48:09, the silent-number-recycling event arrives. In an event-sourced store, the handler does the following:
- Append the new
signal.receivedevent to the log with its actual carrier-emitted timestamp (which may be earlier than the wall-clock arrival). - Identify every
verification.completedandpolicy.evaluatedevent whose decision timestamp falls after the signal's emission timestamp but whose input vector did not include this signal. - For each affected decision, build a counterfactual projection: replay the event log up to the original decision timestamp, but include the late signal in the input vector at its actual emission position.
- Compute the counterfactual score. If it differs from the original score by more than a configured threshold, emit a
policy.evaluatedevent withreason: "retroactive_rescoring"and the causal link to the late signal. - The revocation engine subscribes to retroactive re-scoring events. If any active session was authorized on a now-invalidated decision, it triggers a revocation cascade and a forced re-authentication.
The original 14:03 verification event is unchanged. The audit log shows the original decision and the retroactive correction, with the causal chain that connects them. The fraud team's question—"was the 14:03 verification actually trustworthy in light of what we now know at 15:48?"—has a precise, reproducible answer.
Performance
Append-only logs sound expensive. They are not, when designed properly. The performance characteristics that matter:
| Operation | Latency target | Implementation |
|---|---|---|
| Append event to log | < 5 ms p99 | Single sequential write to a partitioned log; no locks across aggregates |
| Read current trust state | < 2 ms p99 | Materialized projection with index lookup |
| Read decision audit for one user | < 20 ms p99 | Time-partitioned projection scan |
| Counterfactual re-scoring for one decision | tens of ms p99 | Replay bounded by aggregate's event count, typically < 1000 events |
| Bulk retroactive re-scoring after late signal | < 5 s for 10⁴ affected decisions | Parallel projection rebuilds across affected aggregates |
Append-only writes are faster than CRUD updates at scale because they avoid row-level locking, avoid index maintenance overhead on hot rows, and partition cleanly across the write log. The cost shows up in storage, which is cheap and asymmetric—you pay for retention but you only pay once per event, regardless of how many projections derive from it.
The verification latency budget is preserved because the read path never replays the log. It reads a precomputed projection. The replay path runs out-of-band, asynchronously, on the small subset of decisions actually affected by a late signal.
Schema evolution without migration pain
CRUD identity stores fight schema evolution because every column added or changed potentially invalidates the historical interpretation of existing rows. A column added in 2024 cannot be retroactively populated with the value it would have had in 2022, because the inputs are gone.
Event-sourced stores handle schema evolution by versioning the event types. An event written in 2022 retains its 2022 schema forever. Projections are responsible for handling multiple event versions. When a new event type is introduced—say, Schrems III residency annotations added in 2026—the projection logic recognizes that pre-2026 events have no residency annotation and applies a documented default. The historical event log is untouched. The migration is a projection rebuild, not a schema change.
This is the property that makes event-sourced identity stores survive multi-year regulatory shifts. The 2018 events that fed a GDPR-era decision can be replayed against a 2026 projection that applies post-Schrems-III rules, producing a clear answer to "would this 2018 decision still be compliant under 2026 rules?" CRUD stores cannot answer that question because they no longer have the 2018 inputs.
Costs of the pattern
Event sourcing is not free, and pretending otherwise has burned plenty of teams. Three real costs:
Operational complexity. Maintaining projections, handling late events, designing event schemas that survive a decade—these are skills most teams do not yet have on staff. The pattern rewards small, disciplined, well-scoped event types and punishes the natural temptation to log "everything that happened."
Eventual consistency on the read side. Projections lag behind the log by milliseconds in the steady state and by seconds during recovery from a failure. Code paths that must see the absolute latest state need to read directly from the log or from a projection with explicit consistency guarantees. We solve this by separating the high-stakes read paths (token validation, revocation lookups) from the analytics read paths (audit trails, counterfactual replay), with different consistency guarantees for each.
Storage growth. A high-traffic identity stack at 10⁵ events per second produces a lot of log. We retain hot events (last 90 days) on fast storage for fast replay and tier older events to cold storage for compliance and historical replay. The cold tier has higher replay latency but the use cases that touch it (regulator audits, post-incident analysis) tolerate seconds.
These are real costs. They are smaller than the cost of not being able to answer the question your fraud team will eventually ask.
Event sourcing in PasskeyBridge
Every signal that flows through PasskeyBridge—from carrier webhook through classification through cascade dispatch through token revocation—is an event in our log. The revocation cascade engine reads from the active-sessions projection. The DPoP shadow telemetry pipeline writes signal.received events without coupling to the verification path. The DSAR engine reads the audit timeline projection to satisfy data-subject access requests inside its 1-hour SLA. The retroactive re-scoring path is what lets us tell a tenant, three hours after a delayed carrier signal surfaces, exactly which of their authorized sessions were affected and which were not.
The pattern is documented in the webhook configuration guide and in the security and cryptography reference, both of which depend on the event log as their durable causal record.
The prerequisite decision
If you are designing an identity stack today and you write the line UPDATE users SET risk_score = ? WHERE id = ? in your verification path, you are committing to a future in which your fraud team cannot answer the questions they are going to ask you in 18 months. That commitment compounds. Every additional CRUD column is one more piece of context the system cannot replay. Every audit table you bolt on captures a small fraction of what an event log would have captured for free.
The case for event sourcing in identity has little to do with elegance. The alternative—CRUD with bolt-on audit—loses the temporal context that every meaningful trust decision depends on, and there is no migration that recovers it after the fact.
The teams that ship event-sourced identity stores in 2026 are the teams that will be able to answer regulator questions, fraud-team questions, and incident-response questions in 2028 with reproducible, replayable, causally-grounded evidence. The teams that ship CRUD will be apologizing for what they no longer remember.
Trust is a function of time. Build the database that knows that.
---
Curious how this maps onto a working revocation pipeline? Read the SSE/CAEP shared signals guide and the DPoP proof-of-possession spec for the two event consumers most likely to act on a retroactive re-scoring signal.