Guides
Start with one resource and one event type. Add quantities, multiple resources or custom policies when your app needs them.
Basic Booking Flow
A bookable setup needs a resource, an event type, a schedule and a link between the resource and event type. This seed creates a meeting room available on weekdays from 09:00 to 17:00 in New York, with 30-minute meetings.
Add it to a fresh development deployment, then run
npx convex run seed:seedBookingSetup once. The function is internal, so browser
clients cannot invoke it.
import { internalMutation } from "./_generated/server";
import { components } from "./_generated/api";
export const seedBookingSetup = internalMutation({
args: {},
handler: async (ctx) => {
await ctx.runMutation(components.booking.schedules.createSchedule, {
id: "business-hours",
name: "Business Hours",
timezone: "America/New_York",
organizationId: "my-org",
isDefault: true,
weeklyHours: [1, 2, 3, 4, 5].map((dayOfWeek) => ({
dayOfWeek, startTime: "09:00", endTime: "17:00",
})),
});
await ctx.runMutation(components.booking.resources.createResource, {
id: "meeting-room",
name: "Meeting Room",
type: "room",
timezone: "America/New_York",
organizationId: "my-org",
});
await ctx.runMutation(components.booking.public.createEventType, {
id: "quick-meeting",
slug: "quick-meeting",
title: "Quick Meeting",
lengthInMinutes: 30,
slotInterval: 30,
timezone: "America/New_York",
organizationId: "my-org",
scheduleId: "business-hours",
lockTimeZoneToggle: false,
locations: [],
});
await ctx.runMutation(components.booking.resource_event_types.linkResourceToEventType, {
resourceId: "meeting-room",
eventTypeId: "quick-meeting",
});
return null;
},
});Render the Booker with resourceId="meeting-room" and
eventTypeId="quick-meeting" as shown in the quickstart.
The visitor chooses a time, enters their details and confirms. The booking result
includes a management token for later cancellation or rescheduling.
To edit setup from a dashboard, expose only the host functions your dashboard needs and apply the administrator checks. Keep reset and seed operations internal.
Host Policies
The component enforces inventory consistency. Your host app decides who may book, which resources they may see, and which dates and durations you offer.
The reference gateway from the quickstart already checks:
| Policy | Reference behavior |
|---|---|
| Input | Valid timezone, booker details, positive duration and 15-minute alignment |
| Bookable setup | Active resource and event type, linked together |
| Duration | One of the event type's configured durations |
| Opening hours | Start and complete duration fit the schedule, including date overrides |
| Notice and horizon | minNoticeMinutes and maxFutureMinutes; default horizon is 60 days |
| Abuse limits | 5 booking writes per email per 10 minutes, plus 500 across the demo per hour |
Creation and token-based rescheduling both run these checks. The hourly cap is a
demo policy, not a package capacity limit. Adjust both limits for your app and
keep the bookingRateLimits schema table if you use that gateway.
For a private or multi-organization app, also scope every read and write to the resources the caller may access. The reference gateway is for a public catalog; it does not determine organization membership for you. Apply the same schedule and access checks to every resource when adding bundle booking.
bufferBefore and bufferAfter are stored configuration only. If you need gaps
between bookings, enforce them in your host's availability and write paths.
Multi-Resource Booking
Use components.booking.multi_resource.createMultiResourceBooking to reserve a
bundle in one transaction: for example, one room plus two cameras. Every requested
resource must be linked to the event type.
For a camera pool, create a resource with isFungible: true and quantity: 4.
A request for two cameras then consumes two of its four units. Exclusive resources
such as rooms always use quantity one. Quantities and capacities must be positive
safe integers; list each resource ID once.
This complete server-only recipe extends the setup above. Run
npx convex run bundles:addCameraPool once, then call internal.bundles.bookRoomWithCameras
from a host function that has checked your booking policies.
import { v } from "convex/values";
import { internalMutation } from "./_generated/server";
import { components } from "./_generated/api";
export const addCameraPool = internalMutation({
args: {},
handler: async (ctx) => {
await ctx.runMutation(components.booking.resources.createResource, {
id: "camera-pool",
name: "Cameras",
type: "equipment",
timezone: "America/New_York",
organizationId: "my-org",
isFungible: true,
quantity: 4,
});
await ctx.runMutation(components.booking.resource_event_types.linkResourceToEventType, {
resourceId: "camera-pool",
eventTypeId: "quick-meeting",
});
},
});
export const bookRoomWithCameras = internalMutation({
args: {
start: v.number(),
end: v.number(),
booker: v.object({ name: v.string(), email: v.string() }),
},
handler: (ctx, args) => ctx.runMutation(
components.booking.multi_resource.createMultiResourceBooking,
{
...args,
eventTypeId: "quick-meeting",
timezone: "America/New_York",
resources: [
{ resourceId: "meeting-room", quantity: 1 },
{ resourceId: "camera-pool", quantity: 2 },
],
},
),
});Check bundle inventory with checkMultiResourceAvailability. Use
getBookingWithItems to read its resource quantities from an authorized host
function. Cancellation releases every item; rescheduling moves every item
atomically, and a conflicting destination leaves the original booking intact.
The bundled Booker is a single-resource flow. Build your own resource and quantity selection for bundles or pools. Ordinary and provisional single-resource creation reject pools, and their slot queries return no pool slots.
Reschedule with excludeBookingUid
When a customer chooses a new time, their existing booking should not make its
own interval appear unavailable. The day and month availability queries accept
excludeBookingUid for this purpose.
In a custom calendar, pass the booking's uid to getDaySlots and
getMonthAvailability alongside your normal query arguments. This changes the
availability preview; it does not authorize a move. Your host must verify the
management token or the caller's right to manage that booking.
The component's rescheduleBooking and rescheduleBookingByToken move pending or
confirmed bookings. They create a replacement booking with a new uid, preserve
the status, management token and resource quantities, and cancel the old record.
Use the returned booking for subsequent navigation and management calls.
The reference gateway also rejects moves after the booking starts, identical times, and dates outside the host policy. It checks the proposed slot again inside the mutation before committing the move.
Durations and Timezones
Set lengthInMinutes to the default duration and, optionally,
lengthInMinutesOptions to choices such as [30, 60, 90]. Set slotInterval to
control the spacing between offered starts. Use multiples of 15 minutes.
Store booking start and end as Unix timestamps in milliseconds. Keep the
IANA timezone, such as Europe/Berlin, for display and schedule interpretation.
A schedule's date is its local calendar date; derive it in that timezone rather
than taking the date from a UTC ISO string.
For API arguments and hook return values, see the API reference.