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.

typescriptconvex/seed.ts
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:

PolicyReference behavior
InputValid timezone, booker details, positive duration and 15-minute alignment
Bookable setupActive resource and event type, linked together
DurationOne of the event type's configured durations
Opening hoursStart and complete duration fit the schedule, including date overrides
Notice and horizonminNoticeMinutes and maxFutureMinutes; default horizon is 60 days
Abuse limits5 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.

typescriptconvex/bundles.ts
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.