> ## Documentation Index
> Fetch the complete documentation index at: https://docs.controlplane.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Cal.com

> Deploy Cal.com, the open-source scheduling and booking platform, on Control Plane. One location with bundled PostgreSQL and a scheduled-job caller, or highly available. Installs private first. Needs an existing GVC, application keys and database credential secrets.

## Overview

Cal.com is an open-source scheduling platform, a Calendly alternative: public booking pages, event types, availability, calendar connections, teams and webhooks. This template deploys the Cal.com app (UI, booking pages and `/api/*` on port `3000`), a small caller that drives Cal.com's scheduled jobs, and a bundled PostgreSQL, single-instance by default and highly available behind one flag. Cal.com runs in exactly one location of your GVC and installs **private**: you create the admin account over a port-forward, then publish.

> [!NOTE]
>
> This template deploys into an existing [GVC](https://docs.controlplane.com/reference/gvc.md) that you already have — it does not create or manage a GVC. The `location` you set must already be one of that GVC's locations; if it is not, Cal.com starts nothing in the GVC.

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-calcom` | Cal.com app: UI, booking pages and `/api/*` on port `3000` (standard, `calcom.replicas` replicas, pinned to `location`). Each start waits for PostgreSQL, applies database migrations and seeds the app store | — |
| Workload | `RELEASE_NAME-calcom-cron` | One replica of the same image that calls Cal.com's scheduled-job endpoints every minute (queued email, calendar sync, credential refresh) | `cron.enabled: true` (default) |
| Workload | `RELEASE_NAME-postgres` | Bundled PostgreSQL from the [postgres](https://docs.controlplane.com/template-catalog/templates/postgres.md) template, holding all Cal.com data (stateful, 1 replica) | `postgres.enabled: true` (default) |
| Workload | `RELEASE_NAME-postgres-backup` | Cron job that dumps the database to object storage on a schedule | `postgres.backup.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha` | Patroni PostgreSQL cluster from the [postgres-highly-available](https://docs.controlplane.com/template-catalog/templates/postgres-highly-available.md) template (stateful, 3 replicas) | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha-proxy` | HAProxy leader endpoint Cal.com connects to | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-etcd` | etcd cluster Patroni uses for leader election | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha-backup` | Backup job for the highly available database | `postgresHA.backup.enabled: true` |
| Volume set | `RELEASE_NAME-pg-vs` | PostgreSQL data directory | `postgres.enabled: true` (default) |
| Volume set | `RELEASE_NAME-postgres-ha-vs` | Patroni data volumes | `postgresHA.enabled: true` |
| Volume set | `RELEASE_NAME-etcd-vs` | etcd data volumes | `postgresHA.enabled: true` |
| Identity | `RELEASE_NAME-calcom-identity` | Bound to the Cal.com and scheduled-job workloads | — |
| Policy | `RELEASE_NAME-calcom-policy` | Grants that identity `reveal` on exactly the secrets Cal.com reads | — |
| Policy | `RELEASE_NAME-calcom-gvc-policy` | Grants that identity `view` on the install GVC so the app can log a warning when the GVC has other locations | — |
| Identity | `RELEASE_NAME-pg-identity` | Bound to the bundled PostgreSQL and its backup job | `postgres.enabled: true` (default) |
| Policy | `RELEASE_NAME-pg-policy` | Grants the PostgreSQL identity `reveal` on its credentials secret only | `postgres.enabled: true` (default) |
| Identity | `RELEASE_NAME-postgres-ha-identity` | Bound to the Patroni PostgreSQL workloads | `postgresHA.enabled: true` |
| Policy | `RELEASE_NAME-postgres-ha-policy` | Grants that identity `reveal` on its credentials secret only | `postgresHA.enabled: true` |
| Identity | `RELEASE_NAME-etcd-identity` | Bound to the etcd workload | `postgresHA.enabled: true` |
| Policy | `RELEASE_NAME-etcd-policy` | Grants the etcd identity `reveal` on the secrets it reads | `postgresHA.enabled: true` |
| Secret | `RELEASE_NAME-etcd-startup`, `RELEASE_NAME-patroni-startup`, `RELEASE_NAME-postgres-proxy-startup`, `RELEASE_NAME-wal-g-backup-script` | Startup and backup scripts for the highly available database (contain no credentials) | `postgresHA.enabled: true` |
| Secret | `RELEASE_NAME-postgres-config` | Backup bucket and region for the highly available database | `postgresHA.enabled: true` and `postgresHA.backup.enabled: true` |

Your application keys and database credentials are **not** created by the template — see [Prerequisites](#prerequisites).

## Prerequisites

Two secrets must exist **before** installing, and the GVC should have **one location** (see the last step). The values never pass through Helm values.

**1. Create the application keys secret**

A [dictionary secret](https://docs.controlplane.com/guides/create-secret/dictionary.md) with four keys. `encryptionKey` must be exactly 32 characters:

```bash theme={null}
cpln secret create-dictionary --name APP_KEYS_SECRET_NAME \
  --entry nextAuthSecret="$(openssl rand -hex 32)" \
  --entry encryptionKey="$(openssl rand -hex 16)" \
  --entry cronSecret="$(openssl rand -hex 32)" \
  --entry cronApiKey="$(openssl rand -hex 32)"
```

Set `calcom.auth.secretName` to this name. Back the keys up outside Control Plane and never replace `encryptionKey`: it encrypts every connected calendar and app credential.

**2. Create the database credentials secret**

The bundled PostgreSQL initializes with these values and Cal.com connects with them:

```bash theme={null}
cpln secret create-dictionary --name DB_CREDENTIALS_SECRET_NAME \
  --entry username=calcom \
  --entry password="$(openssl rand -hex 24)" \
  --entry database=calcom
```

Set `postgres.config.credentialsSecretName` to this name (`postgresHA.config.credentialsSecretName` in highly available mode). Secret names are org-wide, so give each release its own secrets. The three values are part of Cal.com's connection URL, so use letters, digits, `-` and `_` only.

**3. Use a GVC with one location**

Cal.com runs only in the location named by `location`, which must already be a location of your GVC. The bundled PostgreSQL starts in every location of the GVC, so a GVC with a single location keeps one Cal.com against one database.

> [!WARNING]
>
> Create both secrets **before** installing. Installing without one reports success, but the deployment never becomes ready and `cpln logs` stays empty; `status.versions[].message` from the command below names the missing secret:
>
> ```bash theme={null}
> cpln workload get-deployments RELEASE_NAME-calcom --gvc GVC_NAME -o yaml
> ```
>
> A secret created after install is not picked up on its own. For the database credentials, run `cpln workload force-redeployment` on `RELEASE_NAME-postgres` (`RELEASE_NAME-postgres-ha` in highly available mode) and `RELEASE_NAME-calcom`; for the application keys, on `RELEASE_NAME-calcom` and `RELEASE_NAME-calcom-cron`. See [Troubleshooting](#troubleshooting).

Optional features need their own setup first: an SMTP relay (see [Email](#email)) and a backup bucket (see [Backing Up](#backing-up)).

## Installation

Once your two secrets exist, install from the marketplace registry, pointing the keys at them and `location` at a location of your GVC:

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/calcom \
  --version 1.0.0 --gvc GVC_NAME \
  --set location=LOCATION_NAME \
  --set calcom.auth.secretName=APP_KEYS_SECRET_NAME \
  --set postgres.config.credentialsSecretName=DB_CREDENTIALS_SECRET_NAME
```

Other ways to install and manage the template:

> [card] [UI](https://docs.controlplane.com/template-catalog/install-manage/ui.md)
>
> Browse, install, and manage templates visually

> [card] [CLI](https://docs.controlplane.com/template-catalog/install-manage/cli.md)
>
> Manage templates from your terminal

> [card] [Terraform](https://docs.controlplane.com/template-catalog/install-manage/terraform.md)
>
> Declare templates in your Terraform configurations

> [card] [Pulumi](https://docs.controlplane.com/template-catalog/install-manage/pulumi.md)
>
> Declare templates in your Pulumi programs

## Configuration

### Location

```yaml theme={null}
location: aws-us-east-1   # the ONE location of your GVC that Cal.com runs in; it must already be a GVC location
```

Use a bare location name such as `aws-us-east-1`. A link form (`//location/aws-us-east-1`), a list, or a comma-separated value is refused at render time.

### Application

```yaml theme={null}
calcom:
  image: calcom/cal.com:v6.2.0
  replicas: 1          # stateless; 2+ keeps the UI up through a rolling restart
  appUrl: ""           # empty = canonical endpoint (http://localhost:3000 when private); custom domain with scheme, e.g. https://cal.example.com
  resources:
    minCpu: 500m
    maxCpu: 2
    minMemory: 1Gi
    maxMemory: 4Gi     # boot runs migrations and the app-store seed
  auth:
    secretName: my-calcom-auth   # application keys (Prerequisites 1)
```

### Scheduled Jobs

```yaml theme={null}
cron:
  enabled: true        # calls Cal.com's scheduled-job endpoints every minute; needs internalAccess.type other than none
  resources:
    cpu: 100m
    memory: 128Mi
```

Cal.com runs no scheduler of its own. Keep the caller on unless you call the endpoints yourself: without it queued email, calendar sync and credential refresh never run while Cal.com still reports healthy.

### Email

```yaml theme={null}
email:
  enabled: false       # off = no booking confirmations, invites or password resets
  fromAddress: no-reply@example.com
  fromName: Cal.com
  host: smtp.example.com
  port: 587
  auth:
    secretName: ""     # optional dictionary with username + password, e.g. my-calcom-smtp; empty = unauthenticated relay
```

Turn email on before inviting anyone. For an authenticated relay, create the dictionary and set `email.auth.secretName`:

```bash theme={null}
cpln secret create-dictionary --name SMTP_SECRET_NAME \
  --entry username=SMTP_USERNAME \
  --entry password=SMTP_PASSWORD
```

### Access

```yaml theme={null}
publicAccess:
  enabled: false       # claim the admin account over `cpln port-forward` first, then set true
internalAccess:
  type: same-gvc       # none | same-gvc | same-org | workload-list
  workloads: []        # workload-list only; this release's own workloads are always added
  # workloads:
  #   - //gvc/GVC_NAME/workload/WORKLOAD_NAME
```

A firewall change takes time to propagate after the upgrade reports success, and it restarts Cal.com: at `calcom.replicas: 1` that is a short outage, so set `replicas: 2` for zero-downtime changes.

### Database

```yaml theme={null}
# set postgresHA.enabled: true (and postgres.enabled: false) for near-zero-downtime upgrades and failover
postgres:
  enabled: true
  image: postgres:18
  config:
    credentialsSecretName: my-calcom-db-credentials   # Prerequisites 2
  resources:
    minCpu: 250m
    maxCpu: 1
    minMemory: 512Mi
    maxMemory: 1Gi
  volumeset:
    capacity: 10       # GiB (minimum 10)
```

Highly available mode runs three Patroni replicas, three etcd members, and an HAProxy leader endpoint. Enable exactly one of `postgres` and `postgresHA`:

```yaml theme={null}
postgresHA:
  enabled: false
  config:
    credentialsSecretName: my-calcom-db-credentials   # same shape, in HA mode
  replicas: 3
  volumeset:
    capacity: 10       # GiB per replica (minimum 10)
```

The `postgres.backup` and `postgresHA.backup` blocks pass straight through to the underlying templates — see [Backing Up](#backing-up).

## Connecting

| What | Address | Notes |
| - | - | - |
| Private UI (default) | `http://localhost:3000` after `cpln port-forward RELEASE_NAME-calcom 3000:3000 --gvc GVC_NAME` | Your Cal.com account |
| Public UI and booking pages | `status.canonicalEndpoint` from `cpln workload get RELEASE_NAME-calcom --gvc GVC_NAME -o yaml` | Needs `publicAccess.enabled: true` |
| Internal | `http://RELEASE_NAME-calcom.GVC_NAME.cpln.local:3000` | From other workloads in the GVC |
| Version | `GET /api/version` | No authentication |
| PostgreSQL | `RELEASE_NAME-postgres.GVC_NAME.cpln.local:5432`, or `RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local:5432` in HA mode | Values from the database credentials secret (`cpln secret reveal DB_CREDENTIALS_SECRET_NAME -o yaml`) |

Verify the install through a tunnel:

```bash theme={null}
cpln port-forward RELEASE_NAME-calcom 3000:3000 --gvc GVC_NAME
curl -s http://localhost:3000/api/version
```

### First Run

Whoever completes `/auth/setup` first becomes the instance admin, so the install starts private:

1. Install with `publicAccess.enabled: false` (the default).
2. Run `cpln port-forward RELEASE_NAME-calcom 3000:3000 --gvc GVC_NAME` and open `http://localhost:3000/auth/setup`. Use `http://localhost:3000` exactly: while private, Cal.com issues its session cookie for that origin.
3. Create your admin account (the license step can be skipped), then finish the onboarding: Cal.com redirects every page until it is done.
4. Set `publicAccess.enabled: true` and run `cpln helm upgrade` with your install values. The firewall change takes time to propagate and restarts Cal.com.

### Webhooks

Add webhooks under Settings, Developer, Webhooks. Cal.com delivers to public URLs and to `*.cpln.local` workload URLs in the GVC, for example `http://RECEIVER_WORKLOAD.GVC_NAME.cpln.local:8080/path`. Each delivery carries an `X-Cal-Signature-256` header: the HMAC-SHA256 of the request body, keyed with the webhook's secret.

### Calendar and Video Integrations

Google Calendar, Outlook Calendar and Zoom need your own OAuth app. An admin enters its keys at `/settings/admin/apps`: open the app, edit its keys, save, then switch the app on. The keys are stored in the database, not in a secret. Register the matching callback with the provider, using the public URL of your instance:

```text theme={null}
https://YOUR_URL/api/integrations/googlecalendar/callback
https://YOUR_URL/api/integrations/office365calendar/callback
https://YOUR_URL/api/integrations/zoomvideo/callback
```

If you set `calcom.appUrl`, the callback host is that domain. Users then connect the app from their own settings.

### Teams and Licensed Features

The template runs the official image without a Cal.com license key. Teams work; Workflows, Organizations and other licensed features stay locked. The separate v2 API service is not included.

## Operations

### Backing Up

Database backups are off by default. Enable `postgres.backup.enabled: true` (or `postgresHA.backup.enabled: true`) and choose a `provider` of `aws`, `gcp`, or `minio`; the bucket, cloud account, and IAM setup are described in [Backing Up](https://docs.controlplane.com/template-catalog/templates/postgres.md#backing-up) for the single-instance database and [Backing Up](https://docs.controlplane.com/template-catalog/templates/postgres-highly-available.md#backing-up) for highly available mode, and the `backup` blocks of `values.yaml` use the same keys.

### Restoring a Backup

Restore steps belong to the underlying database templates: [Restoring a Backup](https://docs.controlplane.com/template-catalog/templates/postgres.md#restoring-a-backup) and [Restoring a Backup](https://docs.controlplane.com/template-catalog/templates/postgres-highly-available.md#restoring-a-backup).

### Rotating a Secret

Rotating a referenced secret does not restart Cal.com. After changing a value, redeploy the app, and also `RELEASE_NAME-calcom-cron` when you changed `cronSecret`. Never replace `encryptionKey`.

```bash theme={null}
cpln workload force-redeployment RELEASE_NAME-calcom --gvc GVC_NAME
```

### Scaling and Availability

`calcom.replicas` is a real knob: the app tier is stateless, and with two or more replicas the UI stays up through a rolling restart or a firewall change. For database availability, set `postgresHA.enabled: true` and `postgres.enabled: false` at install.

### Troubleshooting

> [accordion] Deployment never becomes ready and the logs are empty
>
> **Symptom:** `cpln helm install` reported success, `cpln logs` returns nothing, and `cpln workload get-deployments RELEASE_NAME-calcom --gvc GVC_NAME -o yaml` names a secret under `status.versions[].message`, beginning `The secret SECRET_NAME no longer exists` even if it never existed.
>
> **Cause:** A prerequisite secret does not exist: the application keys or the database credentials.
>
> **Fix:** Create it (see [Prerequisites](#prerequisites)), then run `cpln workload force-redeployment` with `--gvc GVC_NAME` on each workload that reads it: for the database credentials `RELEASE_NAME-postgres` (`RELEASE_NAME-postgres-ha` in HA mode) and `RELEASE_NAME-calcom`; for the application keys `RELEASE_NAME-calcom` and `RELEASE_NAME-calcom-cron`. The deployment does not retry on its own.

> [accordion] Cal.com starts nothing and reports it is deactivated
>
> **Symptom:** The deployment message reads `This workload location is deactivated because maxScale is set to 0.`
>
> **Cause:** `location` is not one of the GVC's locations. The default is `aws-us-east-1`.
>
> **Fix:** Set `location` to a location your GVC already has, and run `cpln helm upgrade` with your install values.

> [accordion] The app restarts and logs a FATAL line about PostgreSQL
>
> **Symptom:** The log shows `[calcom] FATAL: PostgreSQL at ... did not answer` and the app retries.
>
> **Cause:** PostgreSQL did not answer within 240 seconds of the app's start.
>
> **Fix:** Check the database workload (`RELEASE_NAME-postgres`, or `RELEASE_NAME-postgres-ha-proxy` in HA mode) and its credentials secret.

> [accordion] The app fails with an authentication error against the database
>
> **Symptom:** The app does not become ready and its log shows `P1000: Authentication failed against database server`.
>
> **Cause:** The password in the credentials secret no longer matches the one PostgreSQL initialized with.
>
> **Fix:** Restore the original password in the secret and run `cpln workload force-redeployment RELEASE_NAME-calcom --gvc GVC_NAME`.

> [accordion] Render refuses the install
>
> **Symptom:** `cpln helm install` fails with a message beginning `calcom:`, such as `calcom: enable exactly one backing store`.
>
> **Cause:** A value is missing or invalid: a link-form or list `location`, `calcom.auth.secretName` or the database credentials secret name left empty, `cron.enabled` with `internalAccess.type: none`, `calcom.appUrl` without a scheme, email enabled without `email.host` or `email.fromAddress`, both or neither of `postgres.enabled` and `postgresHA.enabled`, a database `internalAccess` of `none`, or a database `workload-list` that omits `RELEASE_NAME-calcom`.
>
> **Fix:** Correct the value the message names and reinstall. With `workload-list`, include `//gvc/GVC_NAME/workload/RELEASE_NAME-calcom`.

## Important Notes

* **Install into a GVC with one location** and set `location` to it; the bundled PostgreSQL starts in every GVC location, and the app logs a warning naming the extra locations.
* **Never replace `encryptionKey`**: it encrypts every connected calendar and app credential.
* **After rotating a key or creating a late prerequisite secret**, run `cpln workload force-redeployment`; neither is picked up on its own.
* **Any firewall change restarts Cal.com**; use `calcom.replicas: 2` to avoid downtime.
* **Turn email on before inviting anyone**: without SMTP, booking confirmations, invites and password resets are not sent.
* **Signup cannot be disabled by an environment variable**; use the `disable-signup` feature flag under Settings, Admin, Features.

## External References

> [card] [Cal.com](https://cal.com/)
>
> Product site

> [card] [Self-hosting with Docker](https://cal.diy/docker)
>
> Upstream self-hosting guide

> [card] [Environment Variables](https://github.com/calcom/cal.com/blob/v6.2.0/.env.example)
>
> Every setting the server reads at v6.2.0

> [card] [Cal.com on GitHub](https://github.com/calcom/cal.com/releases)
>
> Source code and releases
