Skip to content

Operations, functions & permissions

Operations

Registered bindings, each one a permission check plus a call into the in-scope function below.

OperationPermissionDoes
booking/create-resourcebooking:manage-resourcesadd a bookable resource
booking/set-resource-activebooking:manage-resourcestake one out of service
booking/list-resourcesbooking:readlist resources
booking/holdbooking:holdtentative hold; throws SlotUnavailable
booking/confirmbooking:confirm (per reservation)held → confirmed, re-checking capacity
booking/expirebooking:confirmsurface a lapsed hold as expired
booking/joinbooking:create (per reservation)add a participant; auto-confirms at fillTarget
booking/leavebooking:cancel (per reservation)soft-leave
booking/cancelbooking:cancel (per reservation)held/confirmed → cancelled
booking/movebooking:move (per reservation)reschedule; throws SlotUnavailable
booking/openbooking:confirm (per reservation)put places on offer on an existing reservation, or close it again
booking/start · complete · no-showbooking:completeservice transitions
booking/get · list · availabilitybooking:readreads

Checks marked (per reservation) pass an EntityRef, so a consumer holding an entity-narrowed grant reaches their own booking and no one else's.

In-scope functions

The composable surface. A vertical calls these inside its own operation and its own permission check, in one transaction — this is how you extend the engine without forking it.

ts
createResource(ctx, { kind, name, capacity? })            → Resource
holdReservation(ctx, { resourceId, startsAt, endsAt,
                       expiresAt, quantity?, fillTarget? }) → Reservation  // throws SlotUnavailable
confirmReservation(ctx, { reservationId })                → Reservation
expireReservation(ctx, { reservationId })                 → Reservation
joinReservation(ctx, { reservationId, partyRef, share? }) → { participant, reservation }
leaveReservation(ctx, { reservationId, participantId })   → Reservation
moveReservation(ctx, { reservationId, resourceId?,
                       startsAt?, endsAt? })              → Reservation  // throws SlotUnavailable
openReservation(ctx, { reservationId, fillTarget })       → Reservation  // null closes it again
cancelReservation · startReservation · completeReservation · markNoShow
getReservation(ctx, id)      → { reservation, participants }
listReservations(ctx, { resourceId?, from?, to? }) → Reservation[]
availability(ctx, { resourceId, from, to })        → FreeInterval[]
effectiveStateOf(state, expiresAt, now)            → ReservationState

move, not update

moveReservation changes resource and/or interval, re-runs the allocation check excluding itself (so nudging a booking that overlaps its own old slot is legal), and keeps the reservation's identity, roster and payments. Passing startsAt alone shifts it, preserving duration — what dragging a calendar cell means.

It is explicitly not cancel-then-rebook, which would lose all three. And there is no generic updateReservation: engines model named transitions, participants are an append-only log rather than a patchable field, and booking.moved carrying from/to is worth more to a consumer than a permanent diff blob.

open — a reservation can be put on offer after the fact

fillTarget is engine state: it drives the auto-confirm in joinReservation. So a booking cannot be opened to others by a vertical keeping its own counter beside the engine's and hoping the two stay in step — openReservation sets it, and null closes it again into a private booking.

Additive: reservations made without a target are unaffected. A target below the people already on the reservation is refused rather than silently stranding one of them.

This is what lets the three shapes of "open game" share one mechanism: a club opening a court with nobody on it, a player opening one they are on, and a player opening a booking they already hold.

availability returns intervals, not slots

FreeInterval[]{ startsAt, endsAt, available }, merged, with available a number because capacity may exceed 1. It reports raw gaps between reservations and knows nothing of opening hours: left alone it will happily call 03:00 free. Intersecting with the venue's bookable window is the vertical's job.

This shape is deliberate. With mixed durations there is no canonical slot list — "does 90 fit at 19:00? does 120?" is a question about gaps, and a fixed slot list cannot answer it.

Permissions

KeyGrants
booking:createadd participants to a reservation
booking:readresources, reservations, availability
booking:holdplace a tentative hold
booking:confirmconfirm a held reservation
booking:cancelcancel a reservation, or leave one
booking:movereschedule to another slot or resource
booking:completestart service, complete, or mark a no-show
booking:manage-resourcescreate, edit and deactivate resources

booking:move is separate from booking:cancel on purpose: staff who may reschedule a customer are not necessarily staff who may cancel and refund one. booking/open is guarded by booking:confirm rather than a key of its own: whoever may confirm a reservation may decide whether it is on offer.

Entitlement

entitlementKey: 'booking'. Default-deny — grant it to a tenant before its operations resolve.

The hard parts, hosted.