This guide is the operational and architectural reference for Cal.diy — the community-driven, fully open-source (MIT) fork of Cal.com with all enterprise/commercial code removed. It is written for the people who run, deploy, secure, and extend a Cal.diy instance: administrators, DevOps engineers, and developers integrating against the Platform API.
Where the User Guide shows what you can do with Cal.diy, this guide explains how the system is built and operated: the monorepo architecture, deployment topologies, the complete configuration surface, the data model, the Platform API internals, security posture, and day-2 operations.
Table of Contents
- Executive Summary
- Architecture Overview
- Technology Stack
- System Requirements
- Installation & Deployment
- Configuration Reference
- Database & Data Model
- The Platform API v2 (Internals)
- Integrations & the App Store
- Calendar Cache & Sync
- Security
- Operations
- Performance & Scaling
- Internationalization
- Testing Strategy
- Repository Structure (for Developers)
- Operational Troubleshooting
- Potential Improvements & Tech Debt
- Quick Reference
1. Executive Summary
Cal.diy is a monorepo that ships four products from one scheduling backend:
- A self-hostable Next.js scheduling web application (
apps/web) — the flagship. - A NestJS Platform API v2 (
apps/api/v2) — a versioned REST API for programmatic scheduling. - A React component SDK (
@calcom/atoms) — native scheduling components for your own app. - An embed SDK (
@calcom/embed-core/embed-react/embed-snippet) — drop the booking page into any website.
It is built on Next.js + React + tRPC + Prisma/PostgreSQL + Redis, styled with Tailwind CSS, linted/formatted with Biome, and tested with Vitest + Playwright. The build orchestrator is Turborepo over Yarn 4 workspaces.
| Property | Value |
|---|---|
| License | MIT (no Open Core, no license key) |
| Primary language | TypeScript |
| Package manager | Yarn 4 (nodeLinker: node-modules) |
| Build system | Turborepo |
| Database | PostgreSQL ≥ 13 (Prisma ORM) |
| Cache / queue / rate limiting | Redis (BullMQ, throttler storage) |
| Default video | Cal Video (Daily.co) |
| Deployment targets | Docker, Vercel, Railway, Northflank, Render, Heroku |
| Error monitoring | Sentry |
| Background jobs | Tasker + trigger.dev |
Important
Cal.diy is a fork with enterprise features (Teams, Organizations, Insights, Workflows, SSO/SAML) removed. The codebase still contains scaffolding (e.g.
Team/Membership/OAuthClienttables, organization references in the Platform API), but the supported, license-free surface is individual scheduling, the embed SDKs, atoms, and the Platform API.
2. Architecture Overview
2.1 Monorepo layout
cal.diy/
├── apps/
│ ├── web/ Next.js scheduling web app (flagship, port 3000)
│ ├── api/v2/ NestJS Platform API (port 5555, /v2 prefix)
│ └── docs/ Nextra docs site (@calcom/docs)
├── packages/
│ ├── prisma/ Schema, migrations, seed, generated client
│ ├── trpc/ Internal type-safe API (web app ↔ server)
│ ├── features/ Domain logic: bookings, availability, event-types,
│ │ auth, webhooks, schedules, calendars, etc.
│ ├── app-store/ 100+ integrations (calendar, video, payment, CRM…)
│ ├── platform/
│ │ ├── atoms/ @calcom/atoms — React component SDK
│ │ ├── types/ @calcom/platform-types (API DTOs)
│ │ ├── constants/ @calcom/platform-constants
│ │ ├── enums/ @calcom/platform-enums
│ │ ├── libraries/ @calcom/platform-libraries
│ │ └── examples/ Example apps consuming atoms
│ ├── embeds/
│ │ ├── embed-core/ Embed engine (vanilla)
│ │ ├── embed-react/ React <Cal> + getCalApi()
│ │ └── embed-snippet/ Loader snippet
│ ├── lib/ Shared utilities (largest package, no upward deps)
│ ├── ui/ Design system components
│ ├── emails/ Email templates (React Email)
│ ├── i18n/ Locale JSON (38 target languages)
│ └── kysely/, dayjs/, sms/, coss-ui/, … supporting packages
└── turbo.json, biome.json, playwright.config.ts, vitest.workspace.ts
2.2 Module boundary enforcement
Cal.diy enforces a strict dependency hierarchy through Biome noRestrictedImports rules to prevent circular dependencies. The key constraints:
apps/web ──▶ trpc ──▶ features ──▶ app-store ──▶ lib
└────▶ prisma
Rules (enforced at lint time):
• lib MUST NOT import from app-store, features, trpc, @trpc/server
• app-store MUST NOT import from features, trpc
• features MUST NOT import from trpc, apps/web
• trpc MUST NOT import from apps/web
• atoms MUST NOT import from trpc (it uses the Platform API instead)
• api/v2 MAY import ONLY from @calcom/platform-* and @calcom/prisma
Note
The
libpackage is the dependency floor (152 files, ~410 KB) and may only depend onprisma. This makes it the safe place for shared logic. The API v2 is deliberately isolated — it can only touch theplatform-*packages andprisma, never the web app'sfeatures/trpccode.
2.3 Request flow — web app
Browser
│ HTTP
▼
Next.js (apps/web) ── SSR pages + App Router ──┐
│ tRPC call │ NextAuth (cookies)
▼ ▼
packages/trpc (server routers) packages/features/auth
│ │
▼ ▼
packages/features (booking, availability) Prisma ◀── PostgreSQL
│ ▲
▼ │
packages/app-store (calendar/video/payment) ────┘ external APIs (Google, Stripe…)
2.4 Request flow — Platform API
Client (API key / OAuth client / OAuth2 token)
│ HTTPS, header: cal-api-version
▼
NestJS (apps/api/v2)
├─ Middleware: RawBody/Json/Urlencoded body, RequestId, Logger, Redirects, Rewriter
├─ Guards: CustomThrottlerGuard (Redis rate limit), Auth guards
├─ ValidationPipe (whitelist + transform, class-validator DTOs)
├─ Versioning: custom header extractor (default 2024-04-15)
├─ Controllers → Services → Repositories → Prisma → PostgreSQL
├─ Queue: BullMQ on Redis
└─ Filters: Prisma / Zod / Http / TRPC / CalendarService exceptions → Sentry
Tip
The API can run two ways from one codebase: as a long-lived local server (
main.tsrun(), listens onAPI_PORT) or as a Vercel serverless function (a cached singletonNestServerExpress instance). The samebootstrap()configures pipes, filters, CORS, and helmet in both modes.
3. Technology Stack
| Layer | Technology | Notes |
|---|---|---|
| Language | TypeScript | Strict in most packages |
| Web framework | Next.js 15/16, React 19 | App Router + legacy pages |
| Internal API | tRPC | Type-safe web ↔ server |
| Platform API | NestJS 10 | REST, header versioning, Swagger |
| ORM | Prisma 6.16 | Custom generated client + extensions |
| Database | PostgreSQL ≥ 13 (Postgres 18 in test compose) | Read/write URL split supported |
| DB adapter | @prisma/adapter-pg + pg Pool |
Optional connection pooling |
| Cache/queue | Redis | BullMQ jobs, throttler storage |
| Styling | Tailwind CSS 4 | Scoped for atoms via postcss-prefixwrap |
| Lint/format | Biome 2.3 | Replaces ESLint + Prettier |
| Unit tests | Vitest 4 | Multi-project workspace |
| E2E tests | Playwright | Web + embed projects, multi-browser |
| Perf tests | Grafana k6 | Booking flow load/stress/spike |
| Background jobs | Tasker + trigger.dev | Async tasks, cron |
| Monitoring | Sentry | Errors + profiling |
| Synthetic checks | Checkly | __checks__/**/*.check.ts |
| Validation | Zod + class-validator | Web (Zod) / API (class-validator) |
| Date/time | Day.js + Luxon | |
| i18n | i18next + Lingo.dev | 38 locales |
| React Email + SMTP/SendGrid/Resend | MailHog in dev |
4. System Requirements
| Component | Minimum | Notes |
|---|---|---|
| Node.js | ≥ 18.x | nvm use respects the pinned version |
| PostgreSQL | ≥ 13.x | Postgres 18 used in the integration-test compose |
| Yarn | 4.12.0 (bundled) | .yarnrc.yml, nodeLinker: node-modules |
| Redis | required for the Platform API | rate limiting, queue, throttler storage |
| RAM (build) | 4 GB min (MAX_OLD_SPACE_SIZE=4096) |
16 GB recommended for NODE_OPTIONS=--max-old-space-size=16384 |
| Docker + Compose | for yarn dx / Docker deploy |
Caution
The Docker build currently requires a reachable database at build time (application configuration is resolved during build). Either point
DATABASE_URLat an existing DB or start a temporary one (docker compose up -d database). Also noteDOCKER_BUILDKIT=0is required when building from source to allow a network bridge.
5. Installation & Deployment
5.1 Local development
git clone https://github.com/calcom/cal.diy.git
cd cal.diy
yarn
cp .env.example .env
echo "NEXTAUTH_SECRET=$(openssl rand -base64 32)" >> .env
echo "CALENDSO_ENCRYPTION_KEY=$(openssl rand -base64 24)" >> .env
# Set DATABASE_URL, then:
yarn workspace @calcom/prisma db-migrate
yarn dev
yarn dx automates the above with a Dockerized Postgres and seeded users (see the User Guide). The Turbo dx/dev tasks depend on env-check:common and env-check:app-store, which compare your .env/.env.appStore against the example files.
5.2 Docker Compose (production)
git clone --recursive https://github.com/calcom/cal.diy.git
cd cal.diy
cp .env.example .env
# Generate and set NEXTAUTH_SECRET (rand 32) and CALENDSO_ENCRYPTION_KEY (rand 24)
docker compose up -d # full stack (db + calcom + studio)
docker compose up -d calcom # web app only, against external DATABASE_URL
Build from source:
docker compose up -d database # temp DB for build
DOCKER_BUILDKIT=0 docker compose build calcom
docker compose up -d
Important
NEXTAUTH_SECRETandCALENDSO_ENCRYPTION_KEYmust be identical at build time and run time. IfNEXT_PUBLIC_WEBAPP_URLdiffers between build and run, the container performs a brief static-file rewrite on start.
5.3 Build-time vs run-time variables
| Variable | Build-time | Run-time | Default |
|---|---|---|---|
DATABASE_URL |
✅ required | ✅ required | postgresql://unicorn_user:magical_password@database:5432/calendso |
NEXTAUTH_SECRET |
✅ required | ✅ required | secret (change it!) |
CALENDSO_ENCRYPTION_KEY |
✅ required | ✅ required | secret (32 bytes) |
MAX_OLD_SPACE_SIZE |
✅ required | — | 4096 |
NEXT_PUBLIC_WEBAPP_URL |
optional | optional | http://localhost:3000 |
NEXTAUTH_URL |
— | optional | {WEBAPP_URL}/api/auth |
CALCOM_TELEMETRY_DISABLED |
optional | optional | (set 1 to disable telemetry) |
5.4 Cloud platforms
| Platform | Mechanism |
|---|---|
| Vercel | Pro plan required (serverless function count). API v2 runs as a serverless handler. postdeploy runs prisma migrate deploy. |
| Railway | One-click template + Postgres add-on |
| Northflank | One-click stack |
| Render | Deploy from calcom/docker |
| Heroku | app.json provisions heroku-postgresql:mini, postdeploy migrates |
| Gitpod | .gitpod.yml runs yarn, generates secrets, yarn dx; ports 3000/5420/1025/8025 |
5.5 Deploy flow & migrations
git pull
yarn
yarn workspace @calcom/prisma db-deploy # production: apply committed migrations
yarn predev # detect .env changes
yarn build && yarn start
Warning
Use
db-deploy(applies only committed migrations) in production.db-migrateis for development and can reset the database. SetSKIP_DB_MIGRATIONS=1to skip the auto-migration step (auto-migrations.ts) — it also skips automatically ifDATABASE_URL/DATABASE_DIRECT_URLare unset or Prisma can't connect.
6. Configuration Reference
Cal.diy is configured almost entirely through environment variables. The authoritative list is turbo.json's globalEnv. They are grouped below.
6.1 Core (required)
| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL connection (point at a pooler if used) |
DATABASE_DIRECT_URL |
Direct DB URL (for migrations when pooling) |
NEXT_PUBLIC_WEBAPP_URL |
Public base URL of the instance |
NEXTAUTH_URL |
Auth server URL |
NEXTAUTH_SECRET |
Cookie/JWT encryption (openssl rand -base64 32) |
CALENDSO_ENCRYPTION_KEY |
AES-256 app encryption key (32 bytes) |
CALCOM_ENCRYPTION_KEY / CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY |
Credential encryption |
NEXT_PUBLIC_VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY |
Web Push (generate via npx web-push generate-vapid-keys) |
6.2 Database & caching
| Variable | Purpose |
|---|---|
DATABASE_READ_URL / DATABASE_WRITE_URL |
Read/write split (API v2) |
DATABASE_READ_POOL_MAX / DATABASE_WRITE_POOL_MAX |
Pool sizes |
USE_POOL |
Enable pg connection pooling (true/1) |
DATABASE_CHUNK_SIZE |
Batch sizing |
REDIS_URL |
Redis (rate limit, queue, throttler) |
UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN |
Serverless Redis |
INSIGHTS_DATABASE_URL |
Read-only replica for analytics (optional; defaults to primary) |
SLOTS_CACHE_TTL |
Slot cache lifetime |
6.3 Email
| Variable | Purpose |
|---|---|
EMAIL_FROM / EMAIL_FROM_NAME |
Global From header |
EMAIL_SERVER_HOST / _PORT / _USER / _PASSWORD |
SMTP server |
SENDGRID_API_KEY / SENDGRID_EMAIL / SENDGRID_SYNC_API_KEY |
SendGrid (workflow reminders) |
RESEND_API_KEY |
Resend |
SEND_FEEDBACK_EMAIL |
Feedback routing |
6.4 Auth, security & rate limiting
| Variable | Purpose |
|---|---|
JWT_SECRET |
API v2 JWT signing |
NEXTAUTH_COOKIE_DOMAIN |
Cookie scope |
NEXTAUTH_SECRET_BACKUP |
Rotation support |
CSP_POLICY |
Content Security Policy mode (non-strict) |
CRON_API_KEY / CRON_SECRET |
Authenticate cron endpoints |
IP_BANLIST / BLACKLISTED_GUEST_EMAILS |
Block lists |
CLOUDFLARE_TURNSTILE_SECRET / NEXT_PUBLIC_CLOUDFLARE_SITEKEY |
Bot protection (Turnstile) |
UNKEY_ROOT_KEY |
Optional Unkey rate limiting |
RATE_LIMIT_DEFAULT_TTL_MS, RATE_LIMIT_DEFAULT_LIMIT, _LIMIT_API_KEY, _LIMIT_OAUTH_CLIENT, _LIMIT_ACCESS_TOKEN |
API v2 rate limits |
NODE_TLS_REJECT_UNAUTHORIZED |
Set 0 only behind a trusted SSL-terminating LB |
6.5 Platform API (apps/api/v2)
| Variable | Purpose |
|---|---|
API_PORT / API_URL / API_ENV |
Server bind/port/env |
DATABASE_READ_URL / DATABASE_WRITE_URL |
DB endpoints |
LOG_LEVEL |
API log level |
API_KEY_PREFIX |
API key prefix (e.g. cal_) |
REWRITE_API_V2_PREFIX |
Rewrite /api/v2 → /v2 (default on) |
ENABLE_SLOTS_WORKERS / SLOTS_WORKER_POOL_SIZE |
Worker-thread slot computation |
ENABLE_ASYNC_TASKER |
Async Tasker |
DOCS_URL |
Swagger/docs URL |
GET_LICENSE_KEY_URL / CALCOM_LICENSE_KEY |
(EE remnants; unused in Cal.diy) |
6.6 Video & conferencing
| Variable | Purpose |
|---|---|
DAILY_API_KEY / DAILY_SCALE_PLAN / DAILY_WEBHOOK_SECRET |
Cal Video (Daily.co) |
ZOOM_CLIENT_ID / ZOOM_CLIENT_SECRET |
Zoom |
MS_GRAPH_CLIENT_ID / MS_GRAPH_CLIENT_SECRET |
Teams / Office 365 |
HUDDLE01_API_TOKEN |
Huddle01 |
CAL_VIDEO_BUCKET_NAME / _REGION / _ASSUME_ROLE_ARN |
Cal Video recording storage (S3) |
B2_APPLICATION_KEY_ID / B2_APPLICATION_KEY / B2_BUCKET_* |
Backblaze B2 storage |
6.7 Payments
| Variable | Purpose |
|---|---|
STRIPE_PRIVATE_KEY / NEXT_PUBLIC_STRIPE_PUBLIC_KEY / STRIPE_CLIENT_ID |
Stripe |
STRIPE_WEBHOOK_SECRET / _APPS / _BILLING |
Stripe webhook secrets |
PAYMENT_FEE_FIXED / PAYMENT_FEE_PERCENTAGE |
Platform fee |
6.8 Calendar & integration credentials
| Variable | Purpose |
|---|---|
GOOGLE_API_CREDENTIALS / GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
Google Calendar OAuth |
GOOGLE_LOGIN_ENABLED |
Enable Google sign-in |
GOOGLE_WEBHOOK_TOKEN / GOOGLE_WEBHOOK_URL |
Google calendar push |
MICROSOFT_WEBHOOK_TOKEN / MICROSOFT_WEBHOOK_URL / OUTLOOK_LOGIN_ENABLED |
Microsoft calendar |
HUBSPOT_*, SALESFORCE_*, ZOHOCRM_*, BASECAMP3_*, CLOSECOM_*, SLACK_*, LARK_*, VITAL_*, ZAPIER_INVITE_LINK, … |
App-store integration credentials |
6.9 Observability & jobs
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SENTRY_DSN / SENTRY_DSN / SENTRY_TRACES_SAMPLE_RATE / SENTRY_PROFILES_SAMPLE_RATE |
Sentry |
AXIOM_TOKEN / AXIOM_DATASET |
Axiom logging |
NEXT_PUBLIC_POSTHOG_KEY / _HOST |
PostHog |
TRIGGER_SECRET_KEY / TRIGGER_API_URL / TRIGGER_DEV_PROJECT_REF |
trigger.dev background jobs |
VERCEL_PROMOTE_WEBHOOK_SECRET |
Verifies Vercel deploy-promoted webhook (HMAC-SHA1) |
NEXT_PUBLIC_LOGGER_LEVEL |
tRPC log verbosity (0–6) |
6.10 Feature toggles & misc
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_DISABLE_SIGNUP |
Disable public signup |
NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL |
Schedule grid interval (min, default 15) |
NEXT_PUBLIC_MINUTES_TO_BOOK |
Slot reservation window |
NEXT_PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS / _RESERVATION_INTERVAL_SECONDS |
Booker refetch cadence |
NEXT_PUBLIC_BOOKER_NUMBER_OF_DAYS_TO_LOAD |
Booker prefetch window |
RESERVED_SUBDOMAINS / USERNAME_BLACKLIST_URL |
Username controls |
CALCOM_TELEMETRY_DISABLED |
Disable anonymous telemetry |
EMBED_PUBLIC_EMBED_VERSION / _FINGER_PRINT / _WEBAPP_URL / _EMBED_LIB_URL |
Embed build vars |
Tip
Both
.envand.env.appStoremust containDATABASE_URL. App-store-specific OAuth credentials live in.env.appStore. The Turboenv-checktasks fail the build if expected variables are missing.
7. Database & Data Model
7.1 Prisma setup
The data layer is Prisma 6.16 over PostgreSQL. packages/prisma/index.ts builds the client with the @prisma/adapter-pg driver adapter and four custom client extensions:
| Extension | Effect |
|---|---|
excludeLockedUsersExtension |
Hides locked users from queries |
excludePendingPaymentsExtension |
Hides teams with pending payment |
bookingIdempotencyKeyExtension |
Enforces booking idempotency keys |
disallowUndefinedDeleteUpdateManyExtension |
Guards against accidental mass deleteMany/updateMany with undefined filters |
// Read/write split: readonlyPrisma uses INSIGHTS_DATABASE_URL if set, else the primary.
export const readonlyPrisma = process.env.INSIGHTS_DATABASE_URL
? customPrisma({ datasources: { db: { url: process.env.INSIGHTS_DATABASE_URL } } })
: prisma;
Connection pooling is opt-in via USE_POOL=true (a pg.Pool with max: 5, 5-minute idle timeout). Prisma's log level is derived from NEXT_PUBLIC_LOGGER_LEVEL.
Note
A custom enum generator (
enum-generator.ts) emitsas constTypeScript objects for every Prisma enum, so enums are available as plain importable constants and types without pulling in the full Prisma runtime.
7.2 Core entities
The schema (packages/prisma/schema.prisma) is large; the central entities for scheduling:
| Entity | Role |
|---|---|
User |
A host. Has username, password (bcrypt), metadata, default conferencing app, plan |
EventType |
A bookable offering with all its settings (see User Guide §6.2) |
Booking |
A scheduled meeting (uid, attendees, status, location, recurring info, seats) |
Attendee |
A guest on a booking |
Schedule / Availability |
Working-hours definitions and weekly/override slots |
SelectedCalendar |
A connected calendar used for conflict-checking + sync state |
DestinationCalendar |
Where new events are written |
Credential |
Encrypted third-party app credentials |
App |
App-store app registration |
Webhook |
Outbound webhook subscriptions |
ApiKey |
Hashed API keys (prefixed cal_) |
Feature / UserFeatures / TeamFeatures |
Feature flags (global + per-user/team) |
BookingAudit / AuditActor |
Immutable booking audit trail |
Team / Membership / OAuthClient / Role |
Org/PBAC scaffolding (EE remnants) |
7.3 Event type configuration model
Event type behavior is driven by a large set of scalar fields plus JSON metadata validated by Zod (packages/prisma/zod-utils.ts). The "managed event type props" list (allManagedEventTypeProps) enumerates every configurable field — confirmation rules, buffers, booking/duration limits, period windows, seats, recurring config, locations, booking fields, colors, time-zone locking, reschedule/cancel locks, and more (see User Guide §6.2 for the catalogue).
Booking-form fields are validated by fieldSchema/baseFieldSchema with these types: name, text, textarea, number, email, phone, address, multiemail, select, multiselect, checkbox, radio, radioInput, boolean, url. Field editability is governed by an EditableSchema enum (system, system-but-optional, system-but-hidden, user, user-readonly).
7.4 Booking Audit System
Cal.diy records every booking change in a tamper-proof audit trail built on two tables:
AuditActor— who/what performed an action (types:USER,GUEST,ATTENDEE,SYSTEM,APP). Uses UUID primary keys and soft references (no FK) toUser/Attendeeso audits survive deletions. PII fields are nullable for anonymization.BookingAudit— append-only records with UUID v7 (time-sortable) primary keys, a plain-stringbookingUid(no FK, survives booking deletion), an explicit businesstimestamp, anoperationIdto correlate multi-booking operations, and a JSONdatafield with per-action versioned payloads.
Tracked actions include CREATED, RESCHEDULED, ACCEPTED, CANCELLED, REJECTED, RESCHEDULE_REQUESTED, ATTENDEE_ADDED/REMOVED, REASSIGNMENT, LOCATION_CHANGED, NO_SHOW_UPDATED, SEAT_BOOKED, SEAT_RESCHEDULED. The source channel is one of WEBAPP, API_V1, API_V2, WEBHOOK, SYSTEM, UNKNOWN.
Important
The audit system is compliance-oriented: it satisfies HIPAA §164.312(b) (immutable audit) and GDPR Article 17 (anonymization by nulling
userUuid/phone/name). The processing queue carries zero PII — only IDs (userUuid/attendeeId/actorId) — so third-party queue providers can be used safely. Records are immutable and append-only; the database rejects deleting anAuditActorthat still has audit records (onDelete: Restrict).
Architecture: BookingEventHandlerService (entry point) → BookingAuditProducerService (queues tasks) → BookingAuditTaskConsumer (validates, resolves actors, writes immutable records).
7.5 Migrations & maintenance scripts
| Script | Purpose |
|---|---|
db-migrate / db-deploy |
Apply schema (dev / prod) |
db-seed |
Seed dummy users |
seed-app-store |
Populate app-store apps |
db-reset |
Full reset (dev) |
delete-app.ts <slug> |
Remove an app + its credentials (local) |
auto-migrations.ts |
Auto-run migrate deploy on boot (skippable) |
8. The Platform API v2 (Internals)
8.1 Bootstrapping
apps/api/v2/src/main.ts exposes the app as either a local server or a Vercel serverless handler via a singleton NestServer. bootstrap.ts configures the application:
- Versioning:
VersioningType.CUSTOM— reads thecal-api-versionheader, validates it againstAPI_VERSIONS, and falls back toVERSION_2024_04_15. - Security:
helmet(), CORS withorigin: "*"and an explicit allowed-header list includingx-cal-client-id,x-cal-secret-key,x-cal-platform-embed, andcal-api-version. - Validation: a global
ValidationPipewithwhitelist: trueandtransform: true(strips unknown fields, coerces types). - Exception filters (ordered):
PrismaExceptionFilter,ZodExceptionFilter,HttpExceptionFilter,TRPCExceptionFilter,CalendarServiceExceptionFilter. cookie-parser, optionalAPI_GLOBAL_PREFIX, customqsquery parsing (supports?ids[]=1&ids[]=2witharrayLimit: 1000).
8.2 App module composition
AppModule wires:
ConfigModule(global, env-driven viaappConfig)RedisModule+BullModule(queue; appends?tls=truein production)ThrottlerModule.forRootAsyncbacked byThrottlerStorageRedisServicePrismaModule,EndpointsModule(all resource modules),AuthModule,JwtModule- Global providers:
SentryGlobalFilter,ResponseInterceptor(request IDs),CustomThrottlerGuard
Middleware order (configured in AppModule): RawBodyMiddleware (for /v2/billing/webhook, /v2/webhooks/vercel/deployment-promoted) → UrlencodedBodyMiddleware (for /v2/auth/oauth2/token) → JsonBodyMiddleware (all) → RequestIdMiddleware → AppLoggerMiddleware → RedirectsMiddleware → RewriterMiddleware.
8.3 Authentication modes
| Mode | Credential | Header(s) |
|---|---|---|
| API key | cal_… |
Authorization: Bearer cal_… |
| OAuth client | client id + secret | x-cal-client-id, x-cal-secret-key |
| OAuth 2.0 | access token | Authorization: Bearer <token> |
The OAuth flow (oauth-flow.service.ts) exchanges authorization codes for access/refresh tokens; access tokens are cached in Redis (with a DB fallback). Invalid tokens raise Invalid Access Token, not present in Redis or DB.
8.4 Rate limiting
CustomThrottlerGuard enforces limits backed by Redis. Every response carries x-ratelimit-limit-{name}, x-ratelimit-remaining-{name}, and x-ratelimit-reset-{name} (seconds). Limits can be the global default or custom per API key, and multiple named windows (e.g. short + long) apply simultaneously. Exceeding any window returns 429 until its reset.
Note
A "dummy" throttler is registered only so NestJS invokes the guard at all — the actual default/custom logic lives in
CustomThrottlerGuard. Default limit/TTL/block-duration come fromRATE_LIMIT_DEFAULT_*env vars.
8.5 Resource modules
The API is organized into modules under apps/api/v2/src/modules/* (account-scoped) and apps/api/v2/src/platform/* (versioned platform resources):
| Area | Versions / notes |
|---|---|
| Bookings | 2024-04-15, 2024-08-13 (attendees, guests, seats, references, no-show, reassign, cal-video) |
| Event types | 2024-04-15, 2024-06-14 (+ transformers internal↔API) |
| Schedules | 2024-04-15, 2024-06-11 |
| Slots | 2024-04-15, 2024-09-04 (reserve/release) |
| Calendars | connect, busy times, unified calendars |
| Selected / destination calendars | conflict-check & write targets |
| Conferencing, OOO, timezones, me, users, memberships | account utilities |
| OAuth clients, OAuth2 controllers, atoms, api-keys, stripe, webhooks, verified-resources | platform/dev surface |
Tip
In
development,main.tscallsgenerateSwaggerForApp(app), producing a live OpenAPI document — the authoritative, exact API contract.
8.6 Special endpoints
GET /health— version-neutral health check, returnsOK.POST /v2/webhooks/vercel/deployment-promoted— HMAC-SHA1-verified (x-vercel-signature,VERCEL_PROMOTE_WEBHOOK_SECRET) webhook that promotes the matching trigger.dev deployment (with timing-safe comparison and exponential-backoff retries 2s/4s/8s).
9. Integrations & the App Store
packages/app-store is a directory of 100+ integrations. Each app is a workspace package (e.g. @calcom/googlecalendar) with a consistent shape: config.json (metadata), api/ (OAuth callbacks + endpoints), lib/ (service adapters), optional components/.
| Category | Apps (examples) |
|---|---|
| Calendars | Google, Office365/Outlook, Apple/CalDAV, Exchange 2013/2016/EWS, Lark, Feishu, Zoho, ICS feed |
| Video | Cal Video/Daily, Zoom, Google Meet, Teams, Jitsi, Whereby, Element Call, Huddle01, Mirotalk, Webex, Tandem, Riverside, Sylaps, Salesroom |
| Payments | Stripe, PayPal, HitPay, BTCPay Server, Alby, mock-payment-app |
| CRM | Salesforce, HubSpot, Pipedrive, Zoho CRM/Bigin, Close, Attio |
| Analytics | GA4, GTM, Plausible, Matomo, PostHog, Umami, Fathom, Meta Pixel, Databuddy |
| Automation | Zapier, Make, n8n, Pipedream |
| Messaging | WhatsApp/Twilio, Telegram, Signal, Discord, Basecamp, Intercom |
A code-generation CLI (@calcom/app-store-cli) emits *.generated.ts barrels from each app's config.json/package.json/api/components/lib. The CRON_ENABLE_APP_SYNC flag keeps app metadata in the DB in sync with config files (reporting-only dry-run when disabled).
Integration credential setup (OAuth redirect URIs)
Each OAuth integration needs a redirect URI registered with the provider. The pattern is <Cal.diy URL>/api/integrations/<app>/callback:
| Provider | Env vars | Redirect URI |
|---|---|---|
| Google Calendar | GOOGLE_API_CREDENTIALS |
/api/integrations/googlecalendar/callback + /api/auth/callback/google |
| Microsoft/Office 365 | MS_GRAPH_CLIENT_ID/SECRET |
/api/integrations/office365calendar/callback |
| Zoom | ZOOM_CLIENT_ID/SECRET |
/api/integrations/zoomvideo/callback |
| Daily (Cal Video) | DAILY_API_KEY |
n/a (API key) |
| Basecamp | BASECAMP3_CLIENT_ID/SECRET |
/api/integrations/basecamp3/callback |
| HubSpot | HUBSPOT_CLIENT_ID/SECRET |
/api/integrations/hubspot/callback |
| ZohoCRM | ZOHOCRM_CLIENT_ID/SECRET |
/api/integrations/zohocrm/callback |
After adding Google credentials, run cd packages/prisma && yarn seed-app-store and publish the Google OAuth consent screen.
10. Calendar Cache & Sync
The Calendar Cache and Sync feature (packages/features/calendar-subscription) reduces external calendar API calls and keeps availability fresh via provider webhooks instead of polling.
- Calendar Cache — stores availability locally (
CalendarCacheEventtable) to cut external calls. - Calendar Sync — subscribes to provider webhooks (Google, Office365) and applies changes in real time.
SelectedCalendaris extended with sync state:channelId,channelResourceId,channelResourceUri,channelKind,channelExpiration,syncToken,syncedAt,syncErrorAt,syncErrorCount,syncSubscribedAt.
Controlled by feature flags (insert into the Feature table, enable per user/team via UserFeatures/TeamFeatures):
| Flag | Effect |
|---|---|
calendar-subscription-cache |
Enable calendar caching |
calendar-subscription-sync |
Enable sync globally |
Required webhook env vars: GOOGLE_WEBHOOK_TOKEN, MICROSOFT_WEBHOOK_TOKEN (validate incoming webhooks); GOOGLE_WEBHOOK_URL/MICROSOFT_WEBHOOK_URL are optional (local-test overrides). Background cron jobs handle cache cleanup and renewing calendar watches.
11. Security
11.1 Secrets & encryption
| Concern | Mechanism |
|---|---|
| Cookie/session | NEXTAUTH_SECRET (must differ from default) |
| App-level encryption | CALENDSO_ENCRYPTION_KEY — AES-256, exactly 32 bytes |
| Stored credentials | Encrypted via CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY |
| Service accounts | CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY |
| API keys | Hashed in DB, prefixed (API_KEY_PREFIX, e.g. cal_) |
| Passwords | bcrypt; strength enforced by isPasswordValid (upper+lower, digit, length ≥7, ≥15 in strict/admin) |
Caution
The default placeholder value
secretforNEXTAUTH_SECRET/CALENDSO_ENCRYPTION_KEYMUST be replaced before any production use. The encryption key must be identical at build and run time, or previously encrypted data (calendar credentials, tokens) becomes undecryptable.
11.2 Content Security Policy
Set CSP_POLICY="non-strict" to enable Strict CSP (except unsafe-inline in style-src). Strict CSP is currently fully enforced only on the login page; other SSR pages run in report-only mode; SSG pages are not yet supported. Custom code may need adjustment for CSP compatibility.
11.3 API security
- helmet sets security headers; CORS allows all origins with an explicit header allow-list.
- ValidationPipe with
whiteliststrips unexpected fields (defense against mass assignment). - Rate limiting per API key / OAuth client / access token via Redis.
- Webhook signature verification: Vercel deploy webhook uses HMAC-SHA1 with timing-safe comparison; Stripe/Daily webhooks use their provider secrets (
STRIPE_WEBHOOK_SECRET*,DAILY_WEBHOOK_SECRET). - OAuth tokens are short-lived and stored in Redis with DB fallback; invalid/expired tokens are rejected.
11.4 Anti-abuse
| Control | Variable |
|---|---|
| Bot detection (Cloudflare Turnstile) | CLOUDFLARE_TURNSTILE_SECRET, NEXT_PUBLIC_CLOUDFLARE_SITEKEY, *_USE_TURNSTILE_IN_BOOKER |
| IP ban list | IP_BANLIST |
| Guest email blocklist | BLACKLISTED_GUEST_EMAILS |
| Username blocklist | USERNAME_BLACKLIST_URL, RESERVED_SUBDOMAINS |
| Email/domain require-exclude | per booking-field Zod rules |
| Watchlist | packages/features/watchlist (telemetry + DTO) |
11.5 Compliance
The Booking Audit System provides HIPAA-aligned immutable audit trails and GDPR Article 17 anonymization with zero-PII queueing (see §7.4). The bot-detection, bookingRequiresAuthentication, and requiresBookerEmailVerification controls limit unauthenticated abuse.
Warning
The API CORS policy is
origin: "*". This is appropriate for a public API authenticated by tokens/keys, but ensure no cookie-authenticated endpoints are exposed through it, and place the API behind your own gateway/WAF if you need origin restrictions.
12. Operations
12.1 Start / stop / logs
| Action | Command |
|---|---|
| Dev start | yarn dev |
| Prod start | yarn build && yarn start |
| Docker up/down | docker compose up -d / docker compose down |
| Tail logs (Docker) | docker compose logs -f calcom |
| Set log verbosity | NEXT_PUBLIC_LOGGER_LEVEL=3 (web), LOG_LEVEL (API) |
12.2 Database operations
# Backup (standard Postgres):
pg_dump "$DATABASE_URL" > calcom_backup_$(date +%F).sql
# Restore:
psql "$DATABASE_URL" < calcom_backup_2026-06-05.sql
# Apply migrations (prod):
yarn workspace @calcom/prisma db-deploy
# Inspect data:
yarn db-studio # Prisma Studio on :5555
12.3 Email in development
docker run -d -p 8025:8025 -p 1025:1025 mailhog/mailhog
# UI: http://localhost:8025 ; required when E2E_TEST_MAILHOG_ENABLED=1
12.4 Background jobs
- Tasker (
packages/features/tasker) runs async tasks;ENABLE_ASYNC_TASKERtoggles async mode,TASKER_ENABLE_WEBHOOKSenables webhook tasks. - trigger.dev runs durable jobs; configure
TRIGGER_SECRET_KEY/TRIGGER_API_URL/TRIGGER_DEV_PROJECT_REF. Deploy viayarn deploy:trigger:prod/:staging. - Cron endpoints are authenticated with
CRON_API_KEY/CRON_SECRET; app-metadata sync is gated byCRON_ENABLE_APP_SYNC.
12.5 Monitoring
| Tool | Setup |
|---|---|
| Sentry | SENTRY_DSN (+ traces/profiles sample rates); API initializes Sentry in instrument.ts before any module loads |
| Checkly | Synthetic checks in __checks__/**; checkly.config.ts runs from us-east-1/eu-west-1 every 10 min |
| Axiom | AXIOM_TOKEN/AXIOM_DATASET |
| PostHog | NEXT_PUBLIC_POSTHOG_KEY/_HOST |
12.6 Health checks
curl https://your-cal/health # web/API health
curl http://localhost:5555/v2/me \
-H "Authorization: Bearer cal_…" # authenticated round-trip
13. Performance & Scaling
13.1 Read/write DB split & pooling
The Platform API supports separate DATABASE_READ_URL/DATABASE_WRITE_URL with independent pool maxima (DATABASE_READ_POOL_MAX, DATABASE_WRITE_POOL_MAX, and worker-pool variants). Enable pg pooling with USE_POOL=true. A read replica for analytics can be set via INSIGHTS_DATABASE_URL.
13.2 Slot computation workers
Available-slot computation can be offloaded to worker threads: ENABLE_SLOTS_WORKERS=true with SLOTS_WORKER_POOL_SIZE. Slot results are cached (SLOTS_CACHE_TTL). The booker tunes its polling via NEXT_PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS, NEXT_PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS, and NEXT_PUBLIC_QUERY_RESERVATION_STALE_TIME_SECONDS.
13.3 Load testing (k6)
packages/testing/performance contains a Grafana k6 suite for the booking flow:
# Direct:
BASE_URL=https://your-cal k6 run tests/performance/smoke/booking.js
# Interactive Docker runner:
./tests/scripts/run-k6-local.sh # menu: smoke / load / stress / spike / all
| Profile | Peak virtual users |
|---|---|
| Load | 2,000 |
| Stress | 4,000 |
| Spike | 5,000 |
Thresholds live in utils/config.js (strictest for smoke, most lenient for spike).
13.4 Caching & queues
Redis backs the throttler storage, BullMQ queues, OAuth-token cache, and slot cache. Calendar Cache & Sync (§10) reduces external API load. In production, BullMQ connects to Redis with TLS.
14. Internationalization
Locale data lives in packages/i18n/locales/<locale>/common.json. The source locale is en, with 38 target languages (ar, de, es, es-419, fr, ja, ko, pt-BR, zh-CN, zh-TW, etc.) managed via Lingo.dev (i18n.json, LINGO_DOT_DEV_API_KEY). An i18n-unused.config.js regex finds unused translation keys (t("…") and <Trans i18nKey="…">).
15. Testing Strategy
| Layer | Tool | Location |
|---|---|---|
| Unit / integration | Vitest 4 (multi-project workspace) | packages/**, apps/** *.{test,spec}.{ts,tsx} |
| Component | Vitest + jsdom | packages/ui, packages/features/*, apps/web/components |
| Timezone-dependent | Vitest with TZ env |
*.timezone.test.ts (whole suite re-run per TZ) |
| Integration | Vitest (VITEST_MODE=integration) |
*.integration-test.ts |
| E2E | Playwright | apps/web/playwright, packages/app-store, embeds |
| Embed E2E | Playwright (chromium/firefox/webkit/mobile) | `packages/embeds/embed-core |
| Performance | k6 | packages/testing/performance |
| API (NestJS) | Jest + supertest | apps/api/v2/**/*.e2e-spec.ts |
| Synthetic | Checkly | __checks__/** |
yarn test # unit/integration (vitest)
yarn test -t getSchedule # filter by name
yarn test-e2e # Playwright
yarn playwright show-report test-results/reports/playwright-html-report
npx playwright install # download browsers if missing
Playwright timeouts are deliberately high locally (120s nav/expect, 240s test) and tight in CI (10s). The integration-test DB runs Postgres 18 in apps/web/test/docker-compose.yml on port 5433.
16. Repository Structure (for Developers)
| Path | Contents |
|---|---|
apps/web |
Next.js app (App Router + pages/, modules/, components/, lib/, server/) |
apps/api/v2 |
NestJS Platform API (src/modules, src/platform, src/middleware, src/lib, src/filters) |
apps/docs |
Nextra docs site |
packages/features |
Domain logic by feature (bookings, availability, eventtypes, auth, webhooks, schedules, calendars, conferencing, data-table, flags, tasker, …) |
packages/lib |
Shared utilities (dependency floor) |
packages/trpc |
tRPC routers (web app's internal API) |
packages/prisma |
Schema, migrations, seed, extensions, generated client |
packages/app-store |
Integrations + app-store CLI |
packages/platform/* |
atoms SDK, platform types/constants/enums/libraries |
packages/embeds/* |
Embed core / react / snippet |
packages/ui, packages/emails, packages/i18n |
Design system, email templates, locales |
specs/ |
Spec-first design docs (Claude-assisted feature workflow) |
Note
specs/documents a "spec-first development" workflow where design docs (design.md,implementation.md,decisions.md) are authored before code, with a rule that every PR be reviewable in under 10 minutes (≤5–7 files, ≤500 lines). This is a contributor-process artifact, not a runtime component.
Coding standards
Biome enforces formatting (110-col, 2-space, LF, double quotes, semicolons, trailing ES5 commas) and the import-boundary rules in §2.2. Import organization is grouped (@calcom/**, then @lib/@components/@server/@trpc, then ~, then relative).
17. Operational Troubleshooting
CLIENT_FETCH_ERROR in Docker
NextAuth can't resolve WEBAPP_URL from inside the container. Set NEXTAUTH_URL=http://localhost:3000/api/auth so the backend loops back to itself.
No key set vapidDetails.publicKey
Web Push keys missing. Generate with npx web-push generate-vapid-keys; set NEXT_PUBLIC_VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY.
Requests rejected behind a load balancer
SSL edge termination: set NODE_TLS_REJECT_UNAUTHORIZED=0 only if you trust the LB.
Invalid 'prisma.user.create()'
Set metadata to {} and leave id empty (autoincrement).
Prisma unexpected character / in variable name (Windows)
Replace the packages/prisma/.env symlink with a real copy: rm packages/prisma/.env && cp .env packages/prisma/.env.
Build OOM
Increase Node heap: export NODE_OPTIONS="--max-old-space-size=16384"; ensure MAX_OLD_SPACE_SIZE is set for the Docker build.
API 429s
Inspect x-ratelimit-* headers; raise the per-key custom limit or default RATE_LIMIT_DEFAULT_*.
Migrations not applying on boot
auto-migrations.ts skips if DATABASE_URL/DATABASE_DIRECT_URL are unset, Prisma can't connect, or SKIP_DB_MIGRATIONS=1. Run db-deploy manually.
Calendar sync not updating
Confirm the calendar-subscription-sync feature flag is enabled and GOOGLE_WEBHOOK_TOKEN/MICROSOFT_WEBHOOK_TOKEN are set; check syncErrorAt/syncErrorCount on the SelectedCalendar row.
18. Potential Improvements & Tech Debt
These observations are drawn from the source as of the documented commit:
- EE remnants: The schema and Platform API retain team/organization/PBAC scaffolding (
Team,Membership,OAuthClient,Role, PBAC seed scripts) even though enterprise features are "removed." A self-hoster cannot use these features but the code paths and DB tables remain — a source of confusion and dead surface area. - Build-time DB dependency: The Docker build requires a reachable database, complicating CI. The README notes this requirement "will be removed in the future."
DOCKER_BUILDKIT=0requirement: Building from source disables BuildKit to allow a build-time network bridge — a workaround that should be eliminated.- CSP maturity: Strict CSP is enforced only on the login page; other SSR pages are report-only and SSG pages unsupported. Full CSP rollout is incomplete.
- CORS
origin: "*": Acceptable for a token-authenticated API but warrants a configurable allow-list for stricter deployments. - Encryption-key coupling:
CALENDSO_ENCRYPTION_KEYmust match between build and run; a mismatch silently breaks decryption of stored credentials. A key-rotation story would reduce risk. - Method-not-implemented stubs: Some payment adapters (e.g. HitPay) contain
Method not implementedstubs — incomplete provider coverage. - Telemetry default: Anonymous telemetry is on unless
CALCOM_TELEMETRY_DISABLED=1— document this prominently for privacy-sensitive deployments.
19. Quick Reference
Ports
| Port | Service |
|---|---|
| 3000 | Web app |
| 5555 | Prisma Studio / Platform API |
| 5433 | Integration-test Postgres |
| 1025 / 8025 | MailHog SMTP / UI |
| 3100 / 3101 | embed-core / embed-react E2E |
Critical env vars (must set for production)
DATABASE_URL=postgresql://user:pass@host:5432/calendso
NEXT_PUBLIC_WEBAPP_URL=https://your-cal
NEXTAUTH_URL=https://your-cal/api/auth
NEXTAUTH_SECRET=<openssl rand -base64 32>
CALENDSO_ENCRYPTION_KEY=<openssl rand -base64 24> # 32 bytes
NEXT_PUBLIC_VAPID_PUBLIC_KEY=<npx web-push generate-vapid-keys>
VAPID_PRIVATE_KEY=<...>
REDIS_URL=redis://... # for Platform API
Key commands
| Command | Purpose |
|---|---|
yarn dx |
DB + app with seeds (Docker) |
yarn dev / yarn build && yarn start |
Dev / prod |
yarn workspace @calcom/prisma db-deploy |
Apply migrations (prod) |
yarn db-studio |
Prisma Studio |
cd packages/prisma && yarn seed-app-store |
Repopulate app store |
yarn test / yarn test-e2e |
Unit / E2E tests |
./tests/scripts/run-k6-local.sh |
Performance tests |
yarn deploy:trigger:prod |
Deploy trigger.dev jobs |
Platform API essentials
| Item | Value |
|---|---|
| Base prefix | /v2 (/api/v2 rewritten) |
| Default version | 2024-04-15 (header cal-api-version) |
| Auth | Authorization: Bearer cal_… / x-cal-client-id+x-cal-secret-key / OAuth2 token |
| Health | GET /health → OK |
| Rate-limit headers | x-ratelimit-{limit,remaining,reset}-{name} |
| Docs | Swagger generated in development |
Architecture one-liner
A Yarn-4/Turborepo monorepo: a Next.js+tRPC scheduling web app and a NestJS Platform API share a Prisma/PostgreSQL data layer and Redis, surrounded by an embed SDK, a React component SDK (@calcom/atoms), and a 100+ app integration store — all MIT-licensed, self-hosted, with enterprise features removed.