Skip to content

Events

Emitted

EventWhen
booking.resource-createda bookable resource is added
booking.helda tentative hold is placed
booking.confirmeda hold becomes firm
booking.expireda lapsed hold is surfaced
booking.cancelledheld or confirmed → cancelled
booking.movedrescheduled — carries from and to
booking.startedservice begins
booking.completedthe terminal success transition
booking.no-showthe party did not turn up
booking.participant-joinedsomeone joined
booking.participant-leftsomeone left
booking.openedplaces were put on offer on an existing reservation, or withdrawn

Consumes nothing.

The PII split that shapes every payload

Three platform rules collide here, and the resolution is worth understanding before you write a consumer.

Fat events are required. A match may have four participants. And the event envelope permits exactly one subjectId, mandatory whenever piiClass !== 'none', because crypto-shredding must be able to key the erasure. A four-person roster cannot be keyed to one subject, and the envelope has no plural.

So the roster does not ride the aggregate event:

EventpiiClassCarries
participant-joined / -leftpseudonymous, subjectId = that partythe one partyRef + share
every reservation lifecycle eventnoneresource, interval, quantity, participantCount

This is better privacy design than the alternative, not a workaround:

  • The club's business record survives an erasure. "Court 1 was booked 17:00–18:30 and completed" names nobody, so it is retained as a legitimate business record while the personal link is shredded with the per-participant events.
  • Each pseudonymous fact is individually shreddable, keyed to exactly the subject it concerns — which is what the envelope was asking for.
  • partyRef is a DataSubjectId, not a free string. Participants are people; the type says so.

Reconstructing a roster

Correlate participant-joined / -left with the lifecycle event on reservationId. That is ordinary stream processing over one module's own events — not a cross-module read, so the fat-event rule is intact.

Split billing does not need this

A vertical composes invoicing's in-scope functions in the same transaction, where it already holds the roster. It never needed the event.

No booking.match-played

booking.completed is the same fact. One transition should not emit two events; a vertical wanting match vocabulary emits its own.

Evolution rules

  • Payload fields are frozen once shipped. Rename, remove or retype means a schemaVersion bump and dual-emit through a deprecation window.
  • New operation inputs are optional with behaviour-preserving defaults.
  • Permission keys are never renamed.
  • Consumers parse payloads with their own Zod schema, never the producer's types.

The hard parts, hosted.