payload-reserve Plugin Guide
Overview
payload-reserve is a Payload CMS 3.x plugin that injects a complete reservation/booking system:
- •4 collections: Services, Resources, Schedules, Reservations
- •User extension: Appends customer fields (
name,phone,notes,bookingsjoin) to an existing auth collection - •4 beforeChange hooks: Auto endTime calculation, conflict detection, status state machine, cancellation policy
- •Admin components: Dashboard widget (RSC), Calendar view (client), Customer picker (client), Availability grid (client)
- •Custom endpoint:
/api/reservation-customer-searchfor multi-field customer search
Plugin pattern: Higher-order function (pluginOptions) => (config) => modifiedConfig.
Three export paths:
- •
payload-reserve— server-side plugin function, types, and utility functions - •
payload-reserve/client— CalendarView, AvailabilityOverview, CustomerField - •
payload-reserve/rsc— DashboardWidgetServer
Quick Start
import { buildConfig } from 'payload'
import { payloadReserve } from 'payload-reserve'
export default buildConfig({
collections: [/* must include a 'users' auth collection */],
plugins: [payloadReserve()],
})
Gotcha: The users collection (or whichever userCollection you specify) must be defined in config.collections before the plugin runs. The plugin finds it and appends fields.
Peer dependency: payload ^3.76.1
Configuration
All options are optional — the plugin works out of the box.
payloadReserve({
disabled: false, // disable plugin (collections still registered)
slugs: {
services: 'services', // override collection slugs
resources: 'resources',
schedules: 'schedules',
reservations: 'reservations',
media: 'media', // media collection for Resources image field
},
userCollection: 'users', // existing auth collection to extend
adminGroup: 'Reservations', // admin panel group name
defaultBufferTime: 0, // minutes between reservations (fallback)
cancellationNoticePeriod: 24, // minimum hours notice for cancellation
customerRole: false, // filter customers by role (string or false)
access: { // per-collection access control overrides
services: { read: () => true },
resources: { /* ... */ },
schedules: { /* ... */ },
reservations: { /* ... */ },
},
})
| Option | Default | Description |
|---|---|---|
disabled | false | Disable plugin functionality |
slugs.* | services, resources, schedules, reservations, media | Collection slugs |
userCollection | 'users' | Auth collection to extend with customer fields |
adminGroup | 'Reservations' | Admin panel group |
defaultBufferTime | 0 | Default buffer minutes between bookings |
cancellationNoticePeriod | 24 | Minimum hours notice for cancellation |
customerRole | false | Filter customers by role in reservation form |
access | {} | Per-collection access control overrides |
Collection Relationships
Services <--many-to-many-- Resources
|
has schedule
|
Schedules
Reservations --> Service
--> Resource
--> Customer (User)
- •Resources reference Services (hasMany) — which services a resource can perform
- •Schedules belong to a Resource — when the resource is available
- •Reservations reference a Service, Resource, and User (customer)
For full field schemas, see references/collections.md.
Status State Machine
+-> confirmed --+-> completed
| |
pending ------+ +-> cancelled
| |
+-> cancelled +-> no-show
- •On create (public): Must be
pending - •On create (admin): Can be
pendingorconfirmed(walk-in support) - •Terminal states (
completed,cancelled,no-show): No further transitions
For hook details, conflict detection algorithm, cancellation policy, and escape hatch, see references/hooks-and-status.md.
Customization Patterns
Adding Fields After Plugin
Add fields to plugin collections after the plugin runs using another plugin or config manipulation:
export default buildConfig({
plugins: [
payloadReserve(),
// Add fields to reservations after the plugin
(config) => {
const reservations = config.collections?.find(c => c.slug === 'reservations')
if (reservations) {
reservations.fields.push({ name: 'internalNotes', type: 'textarea' })
}
return config
},
],
})
Custom afterChange Hooks
Add hooks to plugin collections for notifications, integrations, etc.:
// In a plugin that runs after payloadReserve()
const reservations = config.collections?.find(c => c.slug === 'reservations')
if (reservations) {
if (!reservations.hooks) reservations.hooks = {}
if (!reservations.hooks.afterChange) reservations.hooks.afterChange = []
reservations.hooks.afterChange.push(myNotificationHook)
}
Access Control for Public Booking
Pass access overrides in plugin config to enable public booking. See references/frontend-booking.md for a complete step-by-step guide.
Custom Slug Example
payloadReserve({
slugs: {
services: 'salon-services',
resources: 'stylists',
schedules: 'stylist-schedules',
reservations: 'appointments',
},
})
Components access slugs via config.admin.custom.reservationSlugs.
Admin Components
- •DashboardWidget (RSC): Today's stats (total, upcoming, completed, cancelled, next appointment). Registered as modular dashboard widget with slug
reservation-todays-reservations. - •CalendarView (Client): Month/week/day CSS Grid calendar replacing Reservations list view. Color-coded by status. Click-to-create, event tooltips, current time indicator.
- •CustomerField (Client): Rich customer picker with multi-field search (name, phone, email), inline create/edit via document drawer, optional role filtering.
- •AvailabilityOverview (Client): Weekly resource availability grid at
/admin/reservation-availability. Green=available, blue=booked, gray=exception.
Utility Exports
Available from payload-reserve (server-side):
| Function | Purpose |
|---|---|
addMinutes(date, minutes) | Add minutes to a Date |
doRangesOverlap(startA, endA, startB, endB) | Check time range overlap |
computeBlockedWindow(start, end, bufferBefore, bufferAfter) | Compute blocked window with buffers |
hoursUntil(futureDate, now?) | Hours between now and future date |
resolveScheduleForDate(schedule, date) | Resolve concrete time ranges for a date |
combineDateAndTime(date, time) | Merge date + "HH:mm" string |
isExceptionDate(date, exceptions) | Check if date is an exception |
Troubleshooting
"Could not find collection 'users' to extend": The users collection must be defined in config.collections before the plugin runs. Ensure your Users collection is listed before calling payloadReserve().
Conflict detection not catching overlaps: Check that bufferTimeBefore/bufferTimeAfter are set on the Service. The plugin falls back to defaultBufferTime (default: 0) if not set.
Status transition errors: Only valid transitions are allowed. Check the state machine diagram above. Use context: { skipReservationHooks: true } for migrations/seeding.
Cancellation rejected: The cancellationNoticePeriod (default: 24h) blocks late cancellations. Use the escape hatch for automated cleanup.
Customer picker not filtering by role: Set customerRole: 'your-role' in plugin config. Your user collection must have a role field — the plugin doesn't add it.
endTime not updating: endTime is auto-calculated from startTime + service.duration. Ensure the service has a duration value. The field is read-only.
Deep Dives
- •Full collection schemas: references/collections.md
- •Hook details, state machine, escape hatch: references/hooks-and-status.md
- •Frontend booking integration guide: references/frontend-booking.md
- •Stripe, notifications, scheduled cleanup: references/integration-patterns.md