Skip to content

Envelopes

The event store stores every event in an EventEnvelope: the name of the event, its serialized payload, and its metadata. The store returns envelopes from appendEvents, getEnvelope(s) and readAll, and the event publishers and subscribers receive them.

const [envelope] = await eventStore.appendEvents(stream, [new AccountOpenedEvent()], {
expectedVersion: ExpectedVersion.NoStream,
});
envelope.event; // 'account-opened'
envelope.payload; // the serialized event
envelope.metadata.version; // 1
envelope.metadata.globalPosition; // 42n
Field Type What it is
eventId EventId The unique id of the event, a ULID generated when it was appended. The ids of one store’s appends are generated in order.
aggregateId string The id of the aggregate the stream belongs to.
version number The position of the event in its stream: 1, 2, 3, …
occurredOn Date When the event occurred: the time of its id, unless a pre-built envelope carried its own.
correlationId string, optional The id of the request, message or event the append originates from.
causationId string, optional The id of the message or event that directly caused the append.
headers EventHeaders, optional Key-value metadata stored with the event. See Headers.
eventVersion number, optional The version of the event’s schema. Only set when a pre-built envelope carried it; reserved for upcasting.
globalPosition bigint, optional The position of the event in its pool, across streams. Set once the event is stored. See Reading all events.

Events stored by 3.x have no headers or event version; the data migration gives them their global positions.

An append sets the metadata of all its events:

await eventStore.appendEvents(stream, events, {
expectedVersion: account.committedVersion,
metadata: {
correlationId: request.id,
causationId: command.id,
headers: { tenant: 'acme', userId: 'u-42', retried: false },
},
});
  • correlationId and causationId are strings of at most 255 characters.
  • headers is an object of at most 8 KiB of JSON. Its keys are not empty and don’t start with $, which is reserved for the library (such as a future $traceparent). Its values are strings, finite numbers, booleans or null.
  • Invalid metadata is rejected with an InvalidEventMetadataException before anything is written.
  • A store without the headers capability rejects headers with an UnsupportedOperationException, rather than dropping them. Every built-in store supports headers.
  • Events appended without headers read back without the headers field, and empty headers ({}) are not stored.

Pre-built envelopes keep the metadata they carry, including $ headers. The metadata of the append only fills the fields they lack; headers counts as one field, so the headers of the append are not merged into those of an envelope.

JSON.stringify(envelope) writes the envelope through envelope.toJSON(). It renders the eventId as its value, a string, and the globalPosition (a bigint) as a decimal string instead of throwing. Dates, such as the occurredOn, become ISO 8601 strings:

JSON.stringify(envelope);
// {"event":"account-opened","payload":{...},"metadata":{"eventId":"01JA50F56AM0CCDBNVQW3TTWNY","occurredOn":"2024-10-14T07:56:45.642Z", ..., "globalPosition":"42"}}

Parse it back with EventId.from(json.metadata.eventId), new Date(json.metadata.occurredOn) and BigInt(json.metadata.globalPosition).

The payload is rendered the way the SQL stores write it: a value object that the serializer left in it, such as an Id, stays { props: { value } }. Value objects themselves have no toJSON, so JSON.stringify(id) and what the stores write for them are the same as in 3.x.