Cal.diy Onboarding LabChapter I — End to end: A visitor submits the public booking form to schedule a meeting0/4Contents
Chapter I

End to end: A visitor submits the public booking form to schedule a meeting

This module follows one concrete request — a visitor submitting the public booking form — through every layer of the write path: the Booker UI's createBookingMutation (a TanStack Query useMutation), the typed createBooking() fetch helper, the /api/book/event Next.js Pages API route and its request-level middleware, RegularBookingService's orchestration, the Prisma persistence of the Booking row, EventManager.create()'s calendar/video sync, and finally handleWebhookTrigger()'s BOOKING_CREATED notification. By the end you can recognize and navigate this same UI-mutation → API-route → service → persistence → sync → notification shape anywhere else it recurs in the codebase.


walkthrough

Entry point: the Booker's createBookingMutation

apps/web/modules/bookings/hooks/useBookings.ts · lines 111138
lines 111–114

This is where the visitor's action first meets code: useBookings is the hook the Booker form calls. It pulls live selection state — eventSlug, eventTypeId — out of a shared client-side store via useBookerStoreContext. Think of that store as the form's shared whiteboard: every piece of the Booker UI (calendar, timeslot picker, form fields) reads and writes the same in-memory state instead of passing props down through a dozen components.

lines 136–138

createBookingMutation wraps @tanstack/react-query's useMutation. mutationFn: createBooking tells React Query: when someone calls createBookingMutation.mutate(data), invoke the imported createBooking function with data and track its pending/success/error lifecycle for you (loading spinners, error state, retries) — that's the whole point of a mutation hook instead of calling fetch directly. The actual trigger lives further down this same file, where handleBookEvent is built with handleBooking: createBookingMutation.mutate — that's the function the form's submit handler ultimately calls once the visitor clicks "Confirm".

export const useBookings = ({ event, hashedLink, bookingForm, metadata, isBookingDryRun }: IUseBookings) => {  const router = useRouter();  const eventSlug = useBookerStoreContext((state) => state.eventSlug);  const eventTypeId = useBookerStoreContext((state) => state.eventId);  const [rescheduleUid, setRescheduleUid] = useBookerStoreContext(    (state) => [state.rescheduleUid, state.setRescheduleUid],    shallow  );  const rescheduledBy = useBookerStoreContext((state) => state.rescheduledBy);  const [bookingData, setBookingData] = useBookerStoreContext(    (state) => [state.bookingData, state.setBookingData],    shallow  );  const timeslot = useBookerStoreContext((state) => state.selectedTimeslot);  const { t } = useLocale();  const bookingSuccessRedirect = useBookingSuccessRedirect();  const bookerFormErrorRef = useRef<HTMLDivElement>(null);  const duration = useBookerStoreContext((state) => state.selectedDuration);  const isRescheduling = !!rescheduleUid && !!bookingData;  const bookingUid = getQueryParam("bookingUid") ?? "";  const createBookingMutation = useMutation({    mutationFn: createBooking,    onSuccess: (booking) => {
step 1 of 2
import { post } from "@calcom/lib/fetch-wrapper";import type { BookingCreateBody, BookingResponse } from "../types";export const createBooking = async (data: BookingCreateBody) => {  const response = await post<    BookingCreateBody,    // fetch response can't have a Date type, it must be a string    Omit<BookingResponse, "startTime" | "endTime"> & {      startTime: string;      endTime: string;    }  >("/api/book/event", data);  return response;};
packages/features/bookings/lib/create-booking.ts · lines 114
Figure I · This is the typed fetch helper the mutation wraps. post<BookingCreateBody, ...>("/api/book/event", data) calls into the shared fetch-wrapper package's generic post(), which JSON-serializes the body, sets Content-Type: application/json, and throws an HttpError if the response isn't ok — so createBooking never has to touch fetch directly. The two type parameters mean the caller gets back a typed BookingResponse shape (with startTime/endTime as strings, since JSON has no Date type) instead of an untyped blob — this is what "typed fetch helper" means concretely in this codebase.
walkthrough

The Pages API route: middleware, then delegation

apps/web/pages/api/book/event.ts · lines 163
lines 20–25

The route this action lands on is apps/web/pages/api/book/event.ts — a Next.js Pages API route, meaning handler is the entire HTTP contract: it gets a raw NextApiRequest and must produce a response. Before anything else, if Cloudflare Turnstile is enabled for the Booker, checkCfTurnstileToken() verifies the invisible CAPTCHA token the client attached — reject bad traffic before it costs a database round trip.

lines 27–35

Next comes bot detection: a FeaturesRepository/EventTypeRepository pair backs a BotDetectionService, and botDetectionService.checkBotDetection() inspects the request's headers and target eventTypeId (bot detection can be toggled per event type via a feature flag) and throws before a suspicious request reaches any booking logic.

lines 37–40

checkRateLimitAndThrowError() enforces a per-visitor limit keyed by createBooking:<hashed IP>piiHasher.hash(userIp) means the rate-limit key is never the visitor's raw IP address. This is request-level middleware in the literal sense used elsewhere in this lab: code that runs on every hit to this route regardless of what's inside the booking payload.

lines 42–47

getServerSession() checks whether the visitor is a logged-in Cal.diy user — most public bookings have no session, hence the -1 fallback used for userId a few lines below. The handler then stamps creationSource: CreationSource.WEBAPP onto the request body so every downstream layer can tell this booking arrived through the web app rather than the public API or a platform integration.

lines 49–58

Only after all three checks pass does the route touch the domain. getRegularBookingService() resolves a RegularBookingService instance out of a dependency-injection container, and regularBookingService.createBooking() is handed the raw bookingData plus a bookingMeta bag (who's asking, from where, forced slug, trace context). This is the handoff from transport layer to orchestrating service layer: the route's job ends here, and everything about validating and actually creating the booking now belongs to RegularBookingService.

import process from "node:process";import { getServerSession } from "@calcom/features/auth/lib/getServerSession";import { getRegularBookingService } from "@calcom/features/bookings/di/RegularBookingService.container";import { BotDetectionService } from "@calcom/features/bot-detection";import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";import { FeaturesRepository } from "@calcom/features/flags/features.repository";import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";import getIP from "@calcom/lib/getIP";import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken";import { defaultResponder } from "@calcom/lib/server/defaultResponder";import { piiHasher } from "@calcom/lib/server/PiiHasher";import type { TraceContext } from "@calcom/lib/tracing";import { prisma } from "@calcom/prisma";import { CreationSource } from "@calcom/prisma/enums";import type { NextApiRequest } from "next";async function handler(req: NextApiRequest & { userId?: number; traceContext: TraceContext }) {  const userIp = getIP(req);  if (process.env.NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER === "1") {    await checkCfTurnstileToken({      token: req.body["cfToken"] as string,      remoteIp: userIp,    });  }  // Check for bot detection using feature flag  const featuresRepository = new FeaturesRepository(prisma);  const eventTypeRepository = new EventTypeRepository(prisma);  const botDetectionService = new BotDetectionService(featuresRepository, eventTypeRepository);  await botDetectionService.checkBotDetection({    eventTypeId: req.body.eventTypeId,    headers: req.headers,  });  await checkRateLimitAndThrowError({    rateLimitingType: "core",    identifier: `createBooking:${piiHasher.hash(userIp)}`,  });  const session = await getServerSession({ req });  /* To mimic API behavior and comply with types */  req.body = {    ...req.body,    creationSource: CreationSource.WEBAPP,  };  const regularBookingService = getRegularBookingService();  const booking = await regularBookingService.createBooking({    bookingData: req.body,    bookingMeta: {      userId: session?.user?.id || -1,      hostname: req.headers.host || "",      forcedSlug: req.headers["x-cal-force-slug"] as string | undefined,      traceContext: req.traceContext,    },  });  return booking;}export default defaultResponder(handler, "/api/book/event");
step 1 of 5
  try {    if (!isDryRun) {      booking = await createBooking({        uid,        rescheduledBy: reqBody.rescheduledBy,        reqBody: {          user: reqBody.user,          metadata: reqBody.metadata,          recurringEventId: reqBody.recurringEventId,        },        eventType: {          eventTypeData: eventType,          id: eventTypeId,          slug: eventTypeSlug,          organizerUser,          isConfirmedByDefault,          paymentAppData,        },        input: {          bookerEmail,          rescheduleReason,          smsReminderNumber,          responses,        },        evt,        originalRescheduledBooking,        creationSource: input.bookingData.creationSource,        tracking: reqBody.tracking,      });
packages/features/bookings/lib/service/RegularBookingService.ts · lines 17051733
Figure II · By this point RegularBookingService's handler has already validated the event type, resolved which hosts are available, and built a full CalendarEvent (evt) — but it hasn't written anything yet. This createBooking() call (imported from packages/features/bookings/lib/handleNewBooking/createBooking.ts, a different function from the route-level orchestration) is the first place anything touches the database: it hands over the built evt, the resolved eventType/organizerUser, and the confirmation/payment flags, and returns the persisted Booking row. Underneath, that function wraps a prisma.$transaction() that calls tx.booking.create() — the authoritative write. Once this resolves, the Booking exists regardless of whether the calendar sync or webhook that follow it succeed or fail.
checkpoint

Checkpoint: the booking write path

Answered in place — nothing is graded, everything is explained. 0 / 2 passed

Inside RegularBookingService's handler, which of these happens first for a newly confirmed booking?

Which layer runs bot detection, rate limiting, and session lookup before a booking request is handed to any domain logic?