One event envelope for gateway, REST catch-up and webhooks¶
Context and Problem Statement¶
Events reach their receivers on four paths: as dispatch on the WebSocket gateway, through
GET /api/v1/events (REST catch-up), through the REST consumer of the agents and as an outgoing
webhook, also as a transport for agents (04-sync-protocol.md,
sections 2, 7 and 15). Agents additionally need details that apply to only one delivery:
a freshly issued invocation token and the flag of a redelivery. Does every event have the same
form on every path, and where are the delivery details?
Decision Drivers¶
- SDKs, adapters and apps decode events with one type, no matter on which path they arrive (ADR 0026, R-API-06 in 06-api.md).
- A receiver that switches from the gateway to webhooks does not change its processing.
- Invocation tokens are created anew per delivery and must never be stored (I3, ADR 0023).
- Fields have the same name everywhere, in snake_case (06, section 2).
- Webhooks follow Standard Webhooks for headers and signature (ADR 0016).
Considered Options¶
- One envelope for all paths; delivery details as separate fields in the envelope, never in the log
- Own format per path (gateway frame, REST page, webhook body following the recommendation of
Standard Webhooks with
type,timestamp,data) - CloudEvents 1.0 in the JSON format as the envelope
- One envelope for the event plus a separate wrapper for delivery details
Decision Outcome¶
Chosen option: "One envelope for all paths, delivery details as fields in the envelope", because it gives one form, one schema and one decoding for all receivers. The fields are normative in 04-sync-protocol.md, section 2.2 (R-SY-17).
- Fields of the envelope:
id,seq(durable only),type,schema_version,occurred_at,actor,scope,resource,audience,payload,redacted,truncated,correlation_id,trace_context(only to agent adapters),client_msg_id,permission_version(durable only). Ephemeral events use the same envelope withoutseq. - Delivery fields:
invocation_token(invocation token for events that carry an invocation) andredelivered(redelivery afteragent.ack_timeout). They are created per delivery and never stand inevent_log(03-data-model.md,event_log). For webhookssync_epochis added (ADR 0059). - On the gateway the envelope is the body of
{"op": "dispatch", "d": ...}; inGET /api/v1/eventsan element ofevents; in the webhook the whole body, signed byte for byte.webhook-idis the event ID (webhooks.md, section 2). - Schema:
EventEnvelopeindocs/api/openapi.yaml, per typeallOfof the envelope and the type part; the AsyncAPI only references it (06, section 11).
Consequences¶
- Good, because one type per event is enough in all generated clients and webhook receivers use the same fixtures as gateway clients.
- Good, because delivery fields are clearly marked and the storage rule stays simple: the log never contains a token.
- Bad, because the webhook body deviates from the recommendation of Standard Webhooks
(
occurred_atinstead oftimestamp, nodata). The recommendation is not mandatory [Q1, verified]; headers and signature follow the standard. - Neutral, because the schema contains fields that are never set for some receivers (delivery fields to people); there they are missing (ADR 0053).
- Neutral, because a change to the envelope affects all paths; changes are therefore additive only (04, section 6.1).
Confirmation¶
- Gate G9 (06-api.md, section 15): every sent gateway frame matches its schema.
- Gate G10: webhook conformance with test vectors.
- Contract test: the same event through the gateway,
GET /api/v1/eventsand a webhook yields the same envelope; differences only in delivery fields. - Test:
event_loghas no column and no payload withinvocation_tokenorredelivered; a delivery to a person contains neither of the two fields. - Build ticket M1-16 in PLAN.md (envelopes without JSON
null,correlation_idequal to the trace ID). - Built in M1-16 (
services/server/internal/sync):TestEnvelope_FieldsMatchOpenAPIholds that the JSON members ofsync.Envelopeand its nested types equal the properties ofEventEnvelope,EventActor,EventScopeandEventResourceplustypeandpayload;TestEnvelope_DeliveryFieldsOnlyWhenSetholds that delivery members appear only fromDeliveryFields, andTestMigration_EventLogMatchesSpecthatevent_loghas no column for them.
Pros and Cons of the Options¶
One envelope with delivery fields¶
- Good, because one form everywhere.
- Bad, because the boundary between stored and delivery-related fields is held only by rule and test, not by the form.
Own format per path¶
- Good, because every path can follow its own convention.
- Bad, because three schemas, three decoders and three test suites are created and a change of the path changes code.
CloudEvents 1.0¶
- Good, because a widespread standard with the required fields
id,source,specversion,type[Q2, verified]. - Bad, because names of context attributes may contain only lowercase letters and digits
[Q2, verified];
permission_version,client_msg_idandcorrelation_idwould have to be named differently than in the API and the data model. - Bad, because
seq,scope,resourceandaudiencewould be extensions that no standard tool evaluates; the benefit of the standard would stay small.
Separate wrapper for delivery details¶
- Good, because the form itself shows what is stored.
- Bad, because agent consumers would get a second form and the webhook body would no longer be
the event; signature and duplicate detection through
webhook-idwould become more complicated.
More Information¶
- Sources (retrieved 2026-10-10): Q1 Standard Webhooks, section "Payload structure", https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md [verified]; Q2 CloudEvents 1.0, sections "Naming Conventions" and "REQUIRED Attributes", https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md [verified].
- Related: ADR 0009 (event log), ADR 0013 (spec-first), ADR 0016 (webhooks), ADR 0023
(invocation token), ADR 0053 (no JSON
null).