Cal.com Architecture: How It Actually Works
How to read this page
Symvanta parsed this repository into a code graph: every function, class, and method is a node, and every call or import between them is an edge. Everything on this page is computed from that graph at the commit shown above. The terms:
- Module (or cluster)
- A group of symbols that call each other far more than they call anything else. An algorithm called Louvain community detection finds these groups from the call traffic alone; nobody draws them by hand.
- Modularity (the Q number)
- A 0-to-1 score of how cleanly those groups separate. Higher means more call traffic stays inside its own group; scores around 0.7 and above read as clean boundaries.
- Hub
- The most depended-upon symbol inside one module.
- Load-bearing symbols
- PageRank, the algorithm Google originally used to rank web pages, run over the call graph instead: it surfaces the functions the rest of the codebase leans on hardest.
- Arrows and their numbers
- How many calls cross from one module into another. A heavier arrow means tighter coupling between those two parts.
- Dependency cycle
- File A imports B, which imports A again, sometimes through a longer loop. Cycles are not bugs, but a change inside one tends to ripple around the whole loop.
- Mutually recursive symbols
- Functions that call each other, usually the natural shape of parsers and tree-walking code.
Cal.com is an open-source scheduling platform built on Next.js, tRPC, and a NestJS public API (apps/api/v2): a monorepo covering the booking web app, a versioned REST API for platform integrations, the embeddable Booker other sites drop onto their own pages, and the app-store adapters that connect a user's calendars, video providers, and payment processors. Symvanta's Louvain community detection organized the indexed symbols into 350 functional clusters (modularity Q=0.96), which for a monorepo this size means almost every call stays inside the cluster it starts in. The largest cluster is the web app's server-side request path around buildLegacyRequest (837 symbols): the bridge that rebuilds a legacy request out of App Router headers and cookies, the session resolver behind it, and the repositories a page loads through. Behind it sit the tRPC server layer around TrpcSessionUser (783 symbols) and the app-store integration layer around getAppKeysFromSlug (744 symbols), then the design system (classNames, 724 symbols), the webhook pipeline (BaseEventDTO, 643 symbols), and the booking audit trail (557 symbols). Louvain labels a cluster after the directory its members share whenever no symbol dominates it, and names a few after a fraction of what they hold, so two of the ten module names below were re-derived from the packages and symbols the cluster actually holds, checked one by one with find_node against the graph. Cal.com is released under the MIT License, with no separate commercial directory in the current codebase.
Module map
The diagram below shows the 10 largest of the 350 detected modules, sized by symbol count, with arrows weighted by how many calls cross between them. Two clusters sit at the center: buildLegacyRequest, the server request path every page renders through, and safeStringify, the log-safe serialization layer. The two point at each other, 14 calls one way and 7 back, and every other drawn module except the design system has an edge into one of them. Those 14 calls are the heaviest edge on the map, followed by a pair of 11s out of the app-store layer, one into the request path and one into the serializer, which is what a codebase looks like when every third-party integration logs its credential exchange through one PII-stripping serializer. The design system is the one box with no line attached: at this clustering its only outgoing edge lands in the avatar helpers, a cluster too small to draw.
Where to start reading
These are the 12 most depended-upon symbols in the codebase, blended from the global PageRank ranking and the hub of each of the 10 diagrammed modules, so a product-shaped hub like BookingOutput is guaranteed a seat next to the infrastructure symbols PageRank favors: start here to understand how the pieces connect.
buildLegacyRequestTrpcSessionUsergetAppKeysFromSlugclassNamesBaseEventDTODataRequirementssafeStringifyBookingOutputBaseEmailuseAtomsContextrenderEmailcreateNextApiHandler
Five raw PageRank entries lose their seat to a module hub, and one of them is a warning about the ranking itself. hasPermission ranks tenth globally and is not one function: packages/platform/enums and packages/platform/utils each define a permission-bitmask helper of that name, and the graph records a separate PermissionCheckService.hasPermission node in every file that calls that service, so the rank counts a name rather than a definition. The other four are single definitions that a hub outranked: useAppContextWithSchema, the ErrorWithCode constructor, getPlaceholderAvatar, and useDataTable. At this clustering no test helper reaches the raw ranking at all: all twelve entries are product code.
A few of the twelve are worth calling out individually. buildLegacyRequest rebuilds a legacy request object out of App Router headers and cookies, and buildLegacyCtx beside it does the same for a whole getServerSideProps context, which is how pages that have not migrated keep working. getAppKeysFromSlug reads the stored credentials for one installed app by slug, the first thing every calendar, video, CRM, and payment integration does. safeStringify is the serializer the whole monorepo logs through, and getPiiFreeCredential beside it is why: a credential reaches a log line with its tokens removed. TrpcSessionUser is the type every signed-in tRPC procedure receives, resolved by createContext and enforced by authedProcedure. DataRequirements is the hub of the booking audit trail, the interface that declares which related records the enrichment store has to load before a stored audit row can be rendered as text. BookingOutput types one row of the web app's bookings list. renderEmail and BaseEmail are the two halves of notification delivery, one turning a React template into HTML and one holding the send logic every template class inherits.
Key subsystems
The web app's server request path
buildLegacyRequest, 837 symbols, is the largest cluster on the map and the only one of the ten that spans several packages, which is why Louvain named it after its hub. It covers what happens before an apps/web page renders: buildLegacyRequest and buildLegacyCtx reconstruct a legacy request and context from App Router headers, cookies, params, and search params; getServerSession resolves who the request runs as; UserRepository and FeaturesRepository are the two repositories most pages load through; getTranslate loads the request's translations; and HttpError with getServerErrorFromUnknown shape what a failed server call returns. Its heaviest outgoing edge is 14 calls into safeStringify, and it calls the app-store layer 8 times.
The tRPC server layer
TrpcSessionUser, 783 symbols, is packages/trpc/server: the request context and the procedures built on it. createContext resolves the session user a request runs as, authedProcedure is the base every signed-in procedure extends, createNextApiHandler mounts the router onto Next.js, and onErrorHandler normalizes what a failed procedure returns to the client. Most of the rest of the cluster is the Zod input schema that each router declares for each of its procedures. Its heaviest edge on the map points into the request path 4 times, the same request resolving its session and locale on the way out.
The app-store integration layer
getAppKeysFromSlug, 744 symbols, is packages/app-store, where every calendar, video, CRM, and payment app is registered and configured. It holds per-app credential lookup (getAppKeysFromSlug, getParsedAppKeysFromSlug), the install path each app returns to (getInstalledAppPath), and the OAuth callback state that survives the round trip to a provider (IntegrationOAuthCallbackState, encodeOAuthState, decodeOAuthState). Its two heaviest edges tie at 11, one into the request path and one into safeStringify, and the named integrations under them are HubSpot 7, Salesforce 3, and Stripe 2.
The shared UI surface
The design system in packages/ui, 724 symbols, holds the classNames merge function alongside Tooltip, Avatar, buttonClasses, and the IconName union that types every icon in the product. It is the most self-contained module drawn: one outgoing edge of 4 calls, into the avatar helpers, and nothing on the map calls back into it. Most of what a component here calls is another component in the same cluster.
Webhook delivery
BaseEventDTO, 643 symbols, is packages/features/webhooks: the pipeline that tells a customer's endpoint a booking changed. It holds the DTOs an event is serialized into (BaseEventDTO, WebhookPayload), the queued task shape and its schema (WebhookTaskPayload, webhookTaskPayloadSchema), and the subscriber lookup that decides who receives it (WebhookSubscriber). Its outgoing edges are all small, 2 into the request path and 1 into safeStringify, the shape of a subsystem that is handed a payload instead of going looking for data.
The booking audit trail
booking-audit, 557 symbols, is packages/features/booking-audit, the record of who changed a booking and what changed. BookingAuditContextSchema and BaseStoredAuditData define what gets written, AuditActorType records who wrote it, and DataRequirements drives the enrichment store that turns a stored row back into readable text through TranslationWithParams. Its one edge on the map is 5 calls into safeStringify. Louvain labeled this cluster "Data Auditing Components"; the name here comes from the feature package its members all live in.
Log-safe serialization and the video adapters
safeStringify, 545 symbols, is the cluster the rest of the map leans on hardest: six of the other nine drawn modules have an edge into it. Alongside the serializer itself sit getPiiFreeCredential, which strips personal data out of a credential before it reaches a log line, getUid from the calendar-event parser, findValidApiKey, and getVideoAdapters, the lookup that turns a stored credential into a working video-provider client. It calls back into the request path 7 times and into the tRPC layer once.
The bookings list
BookingOutput, 512 symbols, is the bookings screen in apps/web. The hub, BookingOutput, is one booking as the viewer.bookings.get tRPC router returns it, BookingRowData wraps that booking in the row state the table needs, BookingListingStatus is the status filter the listing runs under, taken straight off the router's input type, and BookingActionContext carries the booking plus the flags every row action reads (isUpcoming, isCancelled, isPending, isTabRecurring). The platform atoms call into it 4 times and it calls back twice, the heaviest traffic between any two UI clusters on the map.
Notification delivery
BaseEmail, 499 symbols, is packages/emails: BaseEmail is the class every template extends, renderEmail turns a React template into the HTML that ships, and SMSManager covers the text-message half of the same job. Its heaviest edge, 13 calls, goes to a second and smaller emails cluster around BaseScheduledEmail (165 symbols) that holds the layout primitives every template renders inside. Louvain named this cluster after the templates directory its members share; the name here is its hub. On the map it calls the request path twice and safeStringify twice.
The platform atoms and the Booker
useAtomsContext, 425 symbols, is packages/platform/atoms, the React components Cal.com ships to platform customers, plus the Booker state they drive. useAtomsContext and useIsPlatform tell a component whether it is rendering inside a customer's embed or inside Cal.com's own web app, and useBookerStoreContext exposes the Booker's in-progress selections to both. Its heaviest edge is 4 calls into the bookings list, and it calls safeStringify twice.
Canonical request flow
Cal.com's public API (apps/api/v2) exposes a versioned booking-creation endpoint at POST /v2/bookings (selected with the cal-api-version: 2024-08-13 request header); tracing relate(kind:chain) downstream from its handler gives the following flow for creating a booking.
BookingsController_2024_08_13.createBooking(apps/api/v2/src/platform/bookings/2024-08-13/controllers/bookings.controller.ts:149) is the entry point, guarded byOptionalApiAuthGuardsince booking creation does not require the caller to be signed in. It hands the parsed body, the raw request, and the optional user straight to the service layer.BookingsService_2024_08_13.createBooking(services/bookings.service.ts:112) runs the validation every booking has to pass regardless of type:getBookedEventTyperesolves the event type from an id, a username and slug, or a team slug and slug;EventTypeAccessService.userIsEventTypeAdminOrOwnerdecides whether the caller counts as an owner or host;checkBookingRequiresAuthenticationSettingenforces the event type's own authentication rule with that answer; a managed parent event type is rejected outright; collective and round-robin event types go throughcheckEventTypeHasHosts; andhasRequiredBookingFieldsResponsesconfirms the submitted form answers cover every required field.- Two flags off the resolved event type,
recurringEventandseatsPerTimeSlot, then pick exactly one of four sibling methods:createRegularBookingfor a plain single booking,createSeatedBookingfor a shared-seat event, andcreateRecurringBooking/createRecurringSeatedBookingfor their recurring counterparts. - All four branches translate the API request into the internal booking shape through
InputBookingsService_2024_08_13(createBookingRequestfor the two single-booking branches,createRecurringBookingRequestfor the recurring pair), then hand that shape to the shared booking engine inpackages/features:RegularBookingService.createBookingfor the single branches,RecurringBookingService.createBookingfor the recurring ones. This is the same engine the web app books through, so the REST API adds a translation layer on top of it rather than a second implementation. - Each branch shapes its response through a matching
OutputBookingsService_2024_08_13method, and the two non-recurring branches re-read the row they just wrote throughBookingsRepository_2024_08_13first (getByUidWithAttendeesAndUserAndEventfor a regular booking) because the engine returns less than the API promises. If anything along the way throws,ErrorsBookingsService_2024_08_13.handleBookingError, orhandleEventTypeToBeBookedNotFoundfor the specific case of a missing event type, converts the failure into the API's standard error shape before it reaches the caller.
The four creation branches are siblings: a single request runs exactly one of them, and all four converge on the same input and output services, which is why InputBookingsService_2024_08_13 and OutputBookingsService_2024_08_13 receive calls from all four while BookingsRepository_2024_08_13 receives calls from only the two non-recurring ones.
Health signals
Symvanta detected 25 dependency cycles across 350 modules (modularity Q=0.96). The largest spans 227 files and ties the notification code to the code that triggers it: the webhook notifier (WebhookNotifier.ts), the webhook output mapper, the email templates (OrganizerRequestEmail.tsx is one of several in the loop), the booking handlers that send them, and the app-store credential helpers those handlers call all sit in one import loop. The second largest spans 68 files of React components: the app-store apps' own settings components, the event-type and calendar screens in apps/web, and the platform atoms' wrappers around them.
The other 23 are small and land in five places. Five are in the platform API and the types it shares: the calendars controller with its Outlook service, the conferencing controller with its Zoom, Office 365, and shared conferencing services, the throttler decorator with its guard, an event-type input with the validator it declares, and the bookings service with its own input service, which sits directly on the request path traced above. Eight are in apps/web, among the app shell, the settings, event-type, form-builder, navigation, OAuth-client, and app-installation screens, the largest of them the six-file installation wizard. Five are in packages/features: availability, feature opt-in, no-show handling, the form builder, and the embed tab UI. Three are in shared packages: the 16-file embed-core bundle, the calendar and video-adapter type declarations, and a text field that imports its own types file. The last two are test scaffolding, the Playwright fixtures and the booking-scenario mock. 15 sets of mutually recursive symbols were also detected, the largest being availability (12 symbols): UserAvailabilityService and the input and result types around it (GetAvailabilityUser, CurrentSeats, GetUserAvailabilityInitialData) reference each other while resolving when a user is free. A modularity Q of 0.96 says the 350 modules are cleanly separated: nearly all call traffic stays inside the module it starts in, and only a thin layer of edges crosses between them.
Auto-generated by Symvanta from the public repo calcom/cal.com at commit 176037d , licensed MIT .
Machine-readable companion: data.json (module counts, subsystems, load-bearing symbols, health signals).