Skip to content

EventStreamEntry

Defined in: packages/contracts/src/event-stream.ts:66

A registered Event stream: its payload contract and its claim→identity stamping rule.

payload is a strict zod schema, and strictness is ENFORCED, not merely advertised: an object-rooted payload (including every object inside a union or discriminated union) must carry a never catchall — .strict() or z.strictObject() — or defineSyncRegistry rejects the registration. A stripping object would let a misspelled or newly-added key vanish between the caller and the consumer with no verdict anywhere, which is exactly the silence the Event lane’s “everything in the Outbox is well-formed” invariant exists to prevent. A NON-object root (a string, array, record, a transform pipeline, …) is accepted unchanged and follows ordinary parse-contract semantics: what the schema ACCEPTS is what appendEvent validates, and the JSON-NORMALIZED form of what the schema PRODUCES is what the consumer receives.

The schema EXECUTES at both boundaries; only ONE execution’s output is taken. appendEvent runs it as pure validation — the result is discarded and the Outbox stores the value the caller supplied — and the ingestion endpoint runs it again as the one AUTHORITATIVE parse, whose output is what is enqueued and what the consumer callback receives. A transform callback therefore runs twice, so transforms must be pure and deterministic: an effectful or environment-dependent one is unsupported, because the server’s run is the one that counts and nothing reconciles it against the client’s. Parse output the JSON value domain cannot carry (a BigInt, undefined) is a terminal per-event rejected at ingest, never a batch fault.

What is enqueued is the JSON-NORMALIZED parse output, not the parse output itself. Ingest serializes the authoritative parse’s result and enqueues the round-trip, so every queue backend — the in-memory fake and real pgmq’s jsonb alike — delivers the identical value. The consequence is worth designing against: a transform producing a Date reaches the consumer as that date’s ISO STRING, and a nested undefined property is dropped (an undefined array member becomes null). A transform that must round-trip as a rich type has to encode it itself.

Schema evolution is compatibility-bound (ADR-0053 decision 1): a payload schema may evolve only backward-compatibly — it must keep accepting every previously-valid payload, because events written offline under the old schema are still in flight. An incompatible change requires a NEW Event-stream name. The registry lock records a hash of this entry so a change is a reviewable risky diff; the hash DETECTS change, it cannot judge compatibility (see registryEventStreams).

TPayload extends z.ZodType = z.ZodType

identity: Record<string, EventStreamIdentityField>

Defined in: packages/contracts/src/event-stream.ts:72

The identity fields stamped server-side from verified claims, keyed by field name. The stamped record (field name → value) is what the consumer callback receives on every event.


payload: TPayload

Defined in: packages/contracts/src/event-stream.ts:67


optional revision?: number

Defined in: packages/contracts/src/event-stream.ts:85

An opaque version counter for the part of this payload contract the lock’s hash cannot see — the same role RowFilterSpec.revision plays for a customWhere closure. The lock hashes the payload schema as a JSON Schema, and refinements/transforms are simply not representable there: a reviewer demonstrated that z.string().refine(v => v.length >= 3) and the INCOMPATIBLE >= 10 version produce the identical stream hash, so the risky diff the ADR relies on never fires.

So: whenever you change acceptance logic a JSON Schema cannot express (any .refine/.superRefine threshold, a cross-field check, a transform), you MUST bump revision (positive integer). That is what surfaces the change in the lock diff, where the compatibility rule can actually be reviewed. Leaving it unchanged after a refinement change silently bypasses the gate.