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.
Entry point: the Booker's createBookingMutation
apps/web/modules/bookings/hooks/useBookings.ts · lines 111–138This 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.
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) => {
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 1–14post<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.The Pages API route: middleware, then delegation
apps/web/pages/api/book/event.ts · lines 1–63The 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.
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.
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.
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.
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");
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 1705–1733RegularBookingService'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: 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?