v2.0.0
A correctness, security, and feature release following a full plugin audit. Most changes fix incorrect behavior, but several are breaking for specific integration patterns — read the migration notes first. On a UTC server with default configuration, core booking behavior is largely unchanged; the breaking items mainly affect API consumers, custom status vocabularies, partial access overrides, and non-UTC-server deployments.
⚠️ Breaking changes & migration notes
GET /api/reserve/resource-availabilitynow requires a staff/admin user. It previously responded to anyone and leaked every resource's busy calendar. Unauthenticated requests now get401, customer-collection users403. If you called this endpoint from a public/unauthenticated frontend, it will stop working — call it only from authenticated admin/staff contexts. Single-collection installs (userCollectionset) must configureresourceOwnerMode.adminRolesand/orstaffProvisioning.staffRoles, otherwise every user (including real admins) is treated as a customer and gets403.POST /api/reserve/bookno longer accepts an arbitrarycustomer. Anonymous callers may not setcustomer(→403; use the guest flow withguestdetails instead); authenticated non-staff users are always booked as themselves; staff/admin may still book for anyone. A caller-suppliedcancellationTokenin the body is now ignored (always server-generated). The samecustomer-mass-assignment guard also applies to Payload's defaultPOST /api/{reservations}REST route. Update any client that posted acustomerid for the current user — it's now inferred from the session.accessoverrides now compose per operation instead of replacing. A partial override such asaccess: { resources: { read } }previously wiped the owner-modecreate/update/deleterules; now it overlays onlyreadand keeps the rest. If you relied on a partial override to remove owner-mode rules, specify every operation explicitly.- Business timezone defaults to
'UTC'. Schedule resolution, day boundaries, exception matching, and the admin calendar now resolve in the configuredtimezone. On a UTC server this matches previous behavior; on a non-UTC server, day/slot resolution changes to the correct calendar day (it was previously buggy). Settimezoneto your business IANA zone (e.g.'America/New_York'). The unusedResource.timezonefield is deprecated. - Custom status vocabularies must set
statusMachine.confirmStatus/cancelStatus. The confirm/cancel plugin hooks, the cancellation notice-period rule, and thecancellationReasonfield now key off these (default'confirmed'/'cancelled') instead of hardcoded literals. If you renamed your statuses (e.g.booked/voided), setconfirmStatus/cancelStatusor that behavior won't fire. Both are validated againststatusesat init. - Conflict detection is stricter and corrected. Buffer times are now enforced symmetrically against neighboring bookings (the required gap between two bookings on a resource is the later one's
bufferTimeBeforeplus the earlier one'sbufferTimeAfter), so back-to-back bookings that previously slipped past a cleanup buffer are now rejected. Multi-resource bookings now block each resource only for its own item window (no more whole-span over-blocking). Service buffer fields are capped at 1439 minutes. Expect some bookings that previously passed/failed to flip to the correct outcome. - Stricter init-time validation throws on previously-silent misconfig. A missing
userCollection, a slug collision, a terminal status with outgoing transitions, a terminaldefaultStatus, or aconfirmStatus/cancelStatusnot instatusesnow throw a clear error at boot. A previously-broken-but-booting config may now fail fast — fix the config. - Lifecycle hooks fire differently.
beforeBookingCreatefires once per/api/reserve/bookbooking (was twice — double-charges/double-sends are fixed) and now runs inside the collectionbeforeChange(after field validation), so a hook stamping a required field must read the merged document.afterStatusChange/afterBookingConfirmno longer fire on plain creates.beforeBookingCancelno longer fires when the cancellation is rejected by the notice period. Review integrations that depended on the old firing. - Type-only:
StatusMachineConfiggained requiredconfirmStatus/cancelStatus. Config input isPartial, so plugin options are unaffected; only code that imported the full type and constructed it by hand needs the two fields.
Added
timezoneplugin option (IANA, default'UTC') governing all schedule/day resolution, via the built-inIntlAPI (no new dependency); exposed to admin components asconfig.admin.custom.reservationTimezone.collectionOverridesplugin option — per-collection overrides for the generated collections (Omit<Partial<CollectionConfig>, 'fields' | 'slug'> & { fields?: ({ defaultFields }) => Field[] }); resolves #4. The plugin's hooks are merged (always run, first),accesscomposes, andslugis ignored. Supersedes the deprecatedextraReservationFields.statusMachine.confirmStatus/cancelStatusfor custom status vocabularies.resourceOwnerMode.roleField(defaultstaffProvisioning.roleFieldor'role') for admin detection on collections using a custom/array role field.
Fixed
- Partial-update validation: reschedules via the REST/Local API now recompute
endTimeand re-check conflicts against the merged document; benign edits (notes, status out of a blocking status) skip re-validation so they aren't blocked by buffer/schedule changes made after booking. Flexible-duration bookings reject inverted (endTime ≤ startTime) windows. - Timezone day-resolution: the slots API and admin calendar/grid no longer resolve the wrong calendar day on non-UTC servers; exceptions and manual slots align across booking API and admin UI.
- Neighbor buffers, multi-resource over-blocking, and same-booking item conflicts (see breaking notes); per-guest capacity sums the matched item's
guestCount; an exception on any of a resource's schedules makes the whole resource unavailable that day, anddate–endDateexception ranges are honored. - Endpoint robustness: non-numeric
guestCount→400(was a silent empty list); unknown/malformedservice/resourceids →404(was500); impossible calendar dates →400. - Owner relationship (
ownedServices) now targets the resolved owner collection instead of a hardcoded customers slug; staff provisioning backfills pre-existing staff and re-creates deleted resources via the dedup-by-owner query. - Admin UI data correctness: month view fetches the full 6-week span it renders; week/day/lane views derive a shared, data-driven visible-hour window; fetches guard against stale responses; the pending badge and dashboard use count queries (no truncation), with a "showing N of M" notice on capped lists;
CustomerFieldrespects a customroutes.api/serverURL.
Changed / Deprecated
disablednow keeps the plugin's collections registered (schema-stable — no table-dropping migrations) while making behavior inert.extraReservationFieldsdeprecated in favor ofcollectionOverrides.reservations.fields(still works).Resource.timezonefield deprecated (unused) in favor of the plugin-leveltimezone.- Service/Resource
imageupload fields are added only when amediacollection exists, so media-less installs no longer hit an init error.
Internal
- Dev harness: disabled Payload's fire-and-forget
generate:typeschild in all test configs (it leaked one orphaned process per boot and pinned the CPU).