PasskeyBridge —

Engineering · 2026-04-29

Event Sourcing for Identity State: CRUD and Trust Decisions

By J. W. Bouckaert

Event Sourcing for Identity State: CRUD and Trust Decisions

Overwritten state

Many identity databases keep a users table with columns such as risk_score, last_verified_at and status. Each verification overwrites the previous values, and each risk recalculation replaces the previous score. The table answers "what do we believe about this identity now?" and keeps little else.

A fraud investigation usually needs a different answer: what should we have believed at the time of a specific decision, given what has surfaced since? That depends on the order in which signals arrived and on the inputs each decision used. An overwritten row no longer holds either.

An example sequence

Consider a hypothetical sequence:

  1. A user completes a passkey sign-in. A carrier lookup shows no recent SIM change. The session is granted access to a high-value endpoint.
  2. Twenty minutes later, a signal arrives about the user's secondary phone line. On its own it does not affect the active session.
  3. An hour after that, a number recycling signal arrives from another carrier that explains the earlier one.
  4. The fraud team now asks whether the original sign-in should have been trusted, and which later actions that session authorized.

If the store overwrites the score and discards the inputs, the team cannot reconstruct the original decision. If the store keeps every signal and decision as an immutable event, the team can replay the log up to the time of the sign-in, add the late signal at its source timestamp, and compare the result with the original decision. The original events stay unchanged.

Three limits of overwritten identity state

Lost inputs. When a row is updated, the previous state is gone unless an audit table captured it. Audit tables often record outputs such as status and risk_score and omit the inputs behind them. They can show what was decided but not what was known.

Concurrent signals. Identity decisions combine carrier signals, device attestations, behavioral telemetry and threat feeds, which arrive at different times. Writing them into one row means either accepting lost updates when the last writer wins, or serializing writes to that row.

No re-scoring. When a late signal arrives, such as a delayed SIM swap detection, backlogged events after a carrier API outage, or a CAEP event from another identity provider, the security team needs to know which recent decisions it affects. Without the original inputs, answering that means running each verification again.

CapabilityOverwritten stateEvent-sourced state
Current trust scoreRow lookupProjection read
Score at a past timeOnly if an audit table captured itReplay to that time
Score with a late signal addedRequires running the verification againReplay with the signal at its source timestamp
Reason for a decisionLimited to audited fieldsFull input set in the log
Concurrent signal writesContend on the same rowAppended independently

Event sourcing

In strict event sourcing:

  1. State is derived. The source of truth is an append-only log of events. Current state is a projection built by folding the log.
  2. Events are immutable. An event is never modified, deleted or reordered. A correction is a new event.
  3. Projections are independent. One log can feed a current-state projection, an audit projection and on-demand replays, each rebuildable from the log.

For identity, a small set of event types covers most of the work:

  • signal.received: a signal arrived from a carrier, device attestation, behavioral telemetry or threat feed, with its source, type, payload digest and timestamp.
  • signal.classified: the signal was classified as hard, soft or unknown.
  • verification.attempted and verification.completed: a verification began and finished, with the inputs and score at decision time.
  • attestation.bound: a DPoP key was bound to an issued token.
  • token.issued and token.revoked: token lifecycle events.
  • policy.evaluated: a policy decision against a specific set of inputs at a specific time.

Each event carries a sequence number, a causation ID linking it to the event that triggered it, and a correlation ID grouping related events.

CQRS

Replaying a user's whole history on every request is too slow for a sign-in path. CQRS separates the two sides:

  • The command side appends to the event log.
  • The query side reads materialized projections, each built for one question: current trust state, active sessions, an audit timeline of decisions with their inputs, and temporary projections built for a specific replay.

When a late event invalidates part of a projection, that part is rebuilt from the log. The log is not changed, and a new policy.evaluated event records the re-scored decision with a causation link to the late signal.

Re-scoring after a late signal

In the example above, a handler would:

  1. Append the late signal.received event with the carrier's timestamp, which may be earlier than its arrival.
  2. Find verification.completed and policy.evaluated events made after that timestamp whose inputs did not include the signal.
  3. Replay each affected decision with the signal included.
  4. If the new score differs from the original by more than a set threshold, append a policy.evaluated event with reason: "retroactive_rescoring".
  5. Let the revocation engine act on that event for any session still active.

The original verification event stays in the log next to the correction.

Schema changes

When a column is added to a mutable table, older rows cannot be filled with the values they would have had, because the inputs are gone. With versioned event types, an old event keeps its original schema and projections handle each version. A new annotation, such as a data residency field, is applied by projection code with a documented default for older events. The change is made by rebuilding projections, and stored history is left as it was.

Costs

Operational work. Projections, late events and long-lived event schemas need maintenance. The pattern works best with a small set of well-defined event types.

Read-side lag. Projections trail the log. Paths that need the latest state, such as token validation and revocation checks, need a projection with an explicit consistency guarantee or a direct read of the log.

Storage. Every event is kept. Older events can be moved to cheaper storage, at the cost of slower replay for audits and investigations that reach them.

PasskeyBridge implementation

PasskeyBridge writes an event row when it ingests a signal, and audit and event records are appended to hash chains with signed checkpoints every five minutes. A late carrier signal is handled when it arrives. PasskeyBridge does not rebuild earlier decisions or re-score the sessions they authorized; the re-scoring path in this article is a design.

The revocation cascade acts on the signal it receives, and DPoP shadow telemetry records whether a proof was present. Neither replays an event log to produce a new trust score.

Summary

An identity store that overwrites state keeps the latest answer and loses the inputs behind earlier decisions. An event log keeps those inputs, which makes it possible to replay a decision and to re-score it when a late signal arrives. The costs are projection maintenance, read-side lag and storage.

---

Related guides: SSF/CAEP shared signals and DPoP proof of possession.

Start free · Test the API