Data Model

Core types and structures used in the SIG protocol.

This page defines the core types and structures that make up the SIG protocol. All JSON field names use snake_case.

OreMetadata

The issuer’s metadata object, served at /.well-known/sig-metadata.json. This is the entry point for discovery.

FieldTypeRequiredDescription
spec_versionstringYesProtocol version. Must be "sig/0.1".
issuerstringYesThe issuer’s canonical DID, e.g. "did:web:acme.example".
jwks_uristringYesAbsolute URL to the issuer’s JWKS endpoint.
events_uristringYesAbsolute URL to the NDJSON event feed.
public_onlybooleanYesIf true, all events in the feed are public. If false, the feed may contain events with "private" visibility.
algorithms_supportedstring[]YesList of supported JWS algorithms. Must include "EdDSA".
event_serializationstringNoSerialization format for events. Defaults to "jws-flattened" if omitted.
Example:
{
  "spec_version": "sig/0.1",
  "issuer": "did:web:acme.example",
  "jwks_uri": "https://acme.example/.well-known/jwks.json",
  "events_uri": "https://acme.example/.well-known/sig-events.ndjson",
  "public_only": true,
  "algorithms_supported": ["EdDSA"]
}

CommonEventFields

Every event in the SIG protocol contains these fields in its payload.

FieldTypeRequiredDescription
spec_versionstringYesMust be "sig/0.1".
event_idstringYesUnique identifier for this event. ULID or UUIDv7 recommended for natural ordering.
event_typestringYesThe event type: "relationship.upsert" or "relationship.revoke".
issuerstringYesThe issuer’s did:web identifier. Must match the DID in the metadata.
issued_atstringYesTimestamp of when the event was created. RFC 3339 format, UTC (e.g. "2026-02-27T15:30:00Z").
sequenceintegerYesMonotonically increasing sequence number. Starts at 1 for the first event.
relationship_idstringYesIdentifier for the relationship this event pertains to. Stable across upserts and revocations of the same relationship.
subjectstringYesThe subject’s did:web identifier (the entity the relationship is about).
visibilitystringYesEither "public" or "private". Determines whether this event is intended for public consumption.

JWS Flattened Envelope

Each event is wrapped in a JWS JSON Flattened Serialization envelope. This is the on-wire format: each line in the NDJSON feed is one of these objects.

FieldTypeDescription
protectedstringBase64url-encoded protected header (JSON object).
payloadstringBase64url-encoded event payload (JSON object containing CommonEventFields + type-specific fields).
signaturestringBase64url-encoded Ed25519 signature computed over ASCII(protected).ASCII(payload).

Example:

{
  "protected": "eyJhbGciOiJFZERTQSIsImtpZCI6ImtleS0xIiwidHlwIjoib3JlLWV2ZW50K2p3cyJ9",
  "payload": "eyJzcGVjX3ZlcnNpb24iOiJvcmUvMC4xIiwiZXZlbnRfaWQiOiIwMUo...",
  "signature": "5Oj2eA3J7JzG..."
}

To verify, the consumer:

  1. Base64url-decodes protected and payload.
  2. Looks up the signing key using the kid from the decoded protected header.
  3. Verifies the signature over the concatenation ASCII(protected) || '.' || ASCII(payload).

Protected Header

The decoded protected header contains:

FieldTypeRequiredDescription
algstringYesSigning algorithm. Must be "EdDSA".
kidstringYesKey ID referencing a key in the issuer’s JWKS.
typstringYesMedia type. Must be "sig-event+jws".

Example (decoded):

{
  "alg": "EdDSA",
  "kid": "key-1",
  "typ": "sig-event+jws"
}

DerivedRelationshipState

This is not a wire type. It is the computed state of a relationship after replaying the event log. Verifiers and consumers build this structure locally.

FieldTypeDescription
issuerstringThe issuer’s did:web identifier.
relationship_idstringThe stable identifier for this relationship.
subjectstringThe subject’s did:web identifier.
relationship_typestringThe type of relationship (e.g. "employee", "contractor").
rolesstring[]Current roles assigned to the subject.
valid_fromstring | nullStart of the validity window (RFC 3339 UTC), or null if unbounded.
valid_untilstring | nullEnd of the validity window (RFC 3339 UTC), or null if unbounded.
statusstringOne of "active", "revoked", or "expired".
revoked_reason_codestring | nullIf revoked, the reason code from the revocation event.
revoked_effective_atstring | nullIf revoked, the effective timestamp of the revocation (RFC 3339 UTC).
last_sequenceintegerThe sequence number of the most recent event affecting this relationship.

Status is derived as follows:

  • active — The most recent event is a relationship.upsert and the current time is within the validity window (or the window is unbounded).
  • expired — The most recent event is a relationship.upsert but the current time is past valid_until.
  • revoked — The most recent event affecting this relationship is a relationship.revoke.

FeedState

The aggregate state derived from replaying an entire event feed.

FieldTypeDescription
by_relationship_idHashMap<string, DerivedRelationshipState>Map from relationship_id to the derived state for that relationship.
last_sequenceintegerThe highest sequence number seen in the feed.
Consumers build a FeedState by iterating through the NDJSON feed in order, verifying each event’s signature, and applying each event to the appropriate entry in by_relationship_id. After processing, the map contains the current state of every relationship the issuer has attested to.