State Derivation

How consumers replay events to derive relationship state.

Event Replay

Consumers derive relationship state by replaying all verified events in ascending sequence order through a deterministic reducer. The replay produces a map of relationship states keyed by relationship_id.

Each event has a sequence number (a positive integer starting at 1). Events must be processed in strict ascending order.

Sequence Integrity

The event feed must form a contiguous, gap-free sequence:

  • Duplicate sequences — if two events share the same sequence number, the feed is invalid. Consumers must reject the entire feed.
  • Gaps in sequences — if any sequence number is missing (e.g., the feed contains sequence 1, 2, 4), the feed is treated as incomplete. Consumers should not trust derived state from an incomplete feed.

Reducer Operations

The reducer processes two event types. Unknown event types are silently ignored for forward-safe evolution with future protocol versions.

relationship.upsert

Creates or replaces a relationship record:

  1. Look up the relationship_id in the state map.
  2. Create or replace the entry with the event’s fields: issuer, subject, relationship_type, roles, valid_from, valid_until, and visibility.
  3. Set the relationship status to active.
  4. Record the event’s sequence as the last_sequence for this relationship.

If a relationship.upsert is applied to an existing relationship (same relationship_id), the previous state is fully replaced. This allows issuers to update roles, types, or other fields by appending a new upsert event.

relationship.revoke

Marks an existing relationship as revoked:

  1. Look up the relationship_id in the state map.
  2. Set the relationship status to revoked.
  3. Record the effective_at timestamp from the event (the moment the revocation takes effect).
  4. Record the reason_code from the event (e.g., employment_ended, contract_complete, voluntary).
  5. Update last_sequence.

If no matching relationship exists for the relationship_id, the revocation event is still recorded. This ensures that a revocation is never lost even if replay starts from a partial feed.

Derived Statuses

After replay, each relationship has one of three statuses:

StatusCondition
activeThe most recent event for this relationship is a relationship.upsert and valid_until (if set) has not passed.
revokedA relationship.revoke event was applied after the most recent relationship.upsert.
expiredThe relationship has a valid_until timestamp that is in the past and no relationship.revoke has been applied.

Status precedence: revoked takes priority over expired. A revoked relationship remains revoked regardless of whether valid_until has passed.

Authorization Decisions

Consumers can make authorization decisions against derived state. Given a subject DID and a set of required predicates (e.g., relationship_type=employee), the decision procedure returns an exit code:

Exit CodeMeaning
0Allow — the subject has an active relationship matching all required predicates.
1Deny — no active relationship matches, or the relationship is revoked/expired.
2Error — the feed could not be fetched, verified, or processed.

Decision Logic

  1. Fetch and verify the event feed (all 9 verification steps per event).
  2. Replay events through the reducer to build the state map.
  3. Find all relationships where subject matches the queried DID.
  4. Filter to relationships with status active.
  5. Check that at least one active relationship satisfies all required predicates (type, roles, etc.).
  6. Return 0 if a match is found, 1 otherwise.

If any step fails (network error, invalid signature, malformed feed), return 2.

Forward Evolution

Unknown event_type values are ignored during replay. This allows the protocol to introduce new event types in future versions without breaking existing consumers. Consumers should log unknown event types for observability but must not treat them as errors.