Cal.diy Onboarding LabChapter III — Before the submit: how the Booker knows which times are open0/4Contents
Chapter III

Before the submit: how the Booker knows which times are open

Before the Booker can let a visitor submit the booking form traced in module 1, it has to know which times are even clickable. This module follows useSchedule as it calls the slots.getSchedule tRPC procedure — a second client/server transport alongside Pages API routes — into getScheduleHandler and the AvailableSlotsService, which computes open slots on every request from a host's schedule, existing bookings, and buffer settings rather than reading them off a stored table.


Tracing a read: from the Booker grid to computed slots
AvailableTimeSlots (Booker UI)…components/AvailableTimeSlots.tsx
useScheduleForEvent…dules/schedules/hooks/useEvent.ts
useSchedule…es/schedules/hooks/useSchedule.ts
trpc.viewer.slots.getSchedule.useQuery…es/schedules/hooks/useSchedule.ts
slotsRouter.getSchedule…/routers/viewer/slots/_router.tsx
getScheduleHandler…ewer/slots/getSchedule.handler.ts
AvailableSlotsService…rver/routers/viewer/slots/util.ts
Computed slots response…rver/routers/viewer/slots/util.ts
Figure I · Click a node to see what it does, where it lives in the code, and its labeled connections — the annotation opens beside it.
export const slotsRouter = router({  getSchedule: publicProcedure.input(ZGetScheduleInputSchema).query(async ({ input, ctx }) => {    const { getScheduleHandler } = await import("./getSchedule.handler");    return getScheduleHandler({      ctx,      input,    });  }),
packages/trpc/server/routers/viewer/slots/_router.tsx · lines 1725
Figure II · getSchedule is declared as a publicProcedure — no session required, matching the public booking page — with a Zod input schema (ZGetScheduleInputSchema) and a .query(...) resolver. Contrast this with module 1's createBooking, which is a handler function exported from a Next.js Pages API route file and matched by URL path. Same client-to-server boundary, second transport: tRPC procedures are named and typed end-to-end (viewer.slots.getSchedule) instead of parsed out of a request body by hand.
import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots";import type { GetScheduleOptions } from "./types";export const getScheduleHandler = async ({ ctx, input }: GetScheduleOptions) => {  const availableSlotsService = getAvailableSlotsService();  return await availableSlotsService.getAvailableSlots({ ctx, input });};
packages/trpc/server/routers/viewer/slots/getSchedule.handler.ts · lines 18
Figure III · Compare this to module 1's API route handler, which itself orchestrated validation, persistence, and notifications inline. getScheduleHandler does none of that — it resolves an AvailableSlotsService via getAvailableSlotsService() (a dependency-injection container, not a direct class import) and immediately returns availableSlotsService.getAvailableSlots({ ctx, input }). All the real logic for computing openings lives in that service, not in the handler.
    function _enrichUsersWithData() {      return usersWithCredentials.map((currentUser) => {        return {          ...currentUser,          currentBookings: currentBookingsAllUsers            .filter(              (b) => b.userId === currentUser.id || b.attendees?.some((a) => a.email === currentUser.email)            )            .map((bookings) => {              const { attendees: _attendees, ...bookingWithoutAttendees } = bookings;              return bookingWithoutAttendees;            }),          outOfOfficeDays: outOfOfficeDaysAllUsers.filter((o) => o.user.id === currentUser.id),        };      });    }    const enrichUsersWithData = withReporting(_enrichUsersWithData.bind(this), "enrichUsersWithData");    const users = enrichUsersWithData();    const premappedUsersAvailability = await this.dependencies.userAvailabilityService.getUsersAvailability({      users,      query: {        dateFrom: startTime.format(),        dateTo: endTime.format(),        eventTypeId: eventType.id,        afterEventBuffer: eventType.afterEventBuffer,        beforeEventBuffer: eventType.beforeEventBuffer,        duration: input.duration || 0,        returnDateOverrides: false,        bypassBusyCalendarTimes,        silentlyHandleCalendarFailures: silentCalendarFailures,        mode,      },
packages/trpc/server/routers/viewer/slots/util.ts · lines 788820
Figure IV · Two of the three ingredients meet here. currentBookings is sliced per host out of currentBookingsAllUsers — bookings already sitting in the database, matched by userId or attendee email — while eventType.afterEventBuffer/beforeEventBuffer are handed straight into getUsersAvailability alongside the date range. The host's own schedule (schedule.availability) is read further inside that same service call. Nothing here is a precomputed 'availability' record; it's schedule, bookings, and buffers combined fresh for this one request.
      aggregate.push({        start: dayjs(startTime).subtract(minutesToBlockBeforeEvent, "minute").toDate(),        end: dayjs(endTime).add(minutesToBlockAfterEvent, "minute").toDate(),        title,        source: `eventType-${eventType?.id}-booking-${id}`,      });      return aggregate;
packages/features/busyTimes/services/getBusyTimes.ts · lines 176182
Figure V · Think of a buffer like the extra few minutes a meeting room stays marked 'occupied' before and after a booking, so the next group isn't walking in while chairs are still being pushed back. Mechanically: every existing booking is turned into a busy interval that starts minutesToBlockBeforeEvent earlier and ends minutesToBlockAfterEvent later than the booking's own start/end. That widened interval — not the raw booking time — is what candidate slots get checked against, so a buffer never needs to be written anywhere; it's applied at read time, on top of whatever bookings already exist.
checkpoint

Checkpoint: fetching available slots

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

Which pairing plays the same client/server-boundary role for reading slots that `createBooking`'s POST handler played for submitting a booking?

When does a host's `beforeEventBuffer`/`afterEventBuffer` actually get turned into blocked time?

How do the Booker's slot-fetching and booking-submission requests relate to each other?