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
sequencenumber, the feed is invalid. Consumers must reject the entire feed. - Gaps in sequences — if any
sequencenumber 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:
- Look up the
relationship_idin the state map. - Create or replace the entry with the event’s fields:
issuer,subject,relationship_type,roles,valid_from,valid_until, andvisibility. - Set the relationship status to
active. - Record the event’s
sequenceas thelast_sequencefor 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:
- Look up the
relationship_idin the state map. - Set the relationship status to
revoked. - Record the
effective_attimestamp from the event (the moment the revocation takes effect). - Record the
reason_codefrom the event (e.g.,employment_ended,contract_complete,voluntary). - 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:
| Status | Condition |
|---|---|
active | The most recent event for this relationship is a relationship.upsert and valid_until (if set) has not passed. |
revoked | A relationship.revoke event was applied after the most recent relationship.upsert. |
expired | The 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 Code | Meaning |
|---|---|
0 | Allow — the subject has an active relationship matching all required predicates. |
1 | Deny — no active relationship matches, or the relationship is revoked/expired. |
2 | Error — the feed could not be fetched, verified, or processed. |
Decision Logic
- Fetch and verify the event feed (all 9 verification steps per event).
- Replay events through the reducer to build the state map.
- Find all relationships where
subjectmatches the queried DID. - Filter to relationships with status
active. - Check that at least one active relationship satisfies all required predicates (type, roles, etc.).
- Return
0if a match is found,1otherwise.
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.