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.