Vibecode Cal.com Teams
track this build5 phases, 15 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Cal.com Teams, self-host a team booking service with event types, availability, and routing. The hard boundary is managed hosting, connectors, upgrades, support, and enterprise controls, plus calendar integrations and reliability.
You are building a lean indie version of Cal.com Teams.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Cal.com Teams · indie build
Cal.com self-hosted on a server you own, connected to real calendar accounts so availability reflects actual commitments, with team event types, round robin assignment, buffers and notice periods configured deliberately, confirmation and reminder email that arrives, and a restore you have performed.
Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Application | Cal.com, self-hosted | the scheduling logic, availability computation and booking pages already exist and are the hard part |
| Database | PostgreSQL | bookings are money-adjacent and concurrency-sensitive, which is exactly what it is for |
| Calendars | Real provider connections through their APIs | availability computed from a calendar that is not the one you actually use is worse than no availability at all |
| Email | A transactional provider with a verified domain | a confirmation that lands in spam is a missed meeting |
| Edge | A reverse proxy with automatic certificates | booking pages are public and must be trusted by a stranger's browser |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **A Linux server with room for the application and PostgreSQL** · roughly 10 to 25 USD a month
- Why: Both run here, and an undersized server produces slow booking pages, which people abandon.
- Get it: Any virtual server provider. Check the project's current requirements before choosing.
- Verify: The application starts and the booking page loads promptly
- [ ] **A second server for the restore drill** · a few USD if deleted the same day
- Why: Restoring onto the machine that already has the configuration proves nothing, and the missing configuration is exactly what causes a broken restore here.
- Get it: The same provider, smallest suitable plan, destroyed after the drill.
- Verify: You can SSH to it
- [ ] **A domain with DNS you control** · roughly 10 to 15 USD a year
- Why: Needed for certificates, for the public URL, and for the email sending domain records.
- Get it: Any registrar. You will add an A record and several email verification records.
- Verify: The hostname resolves and email records validate
- [ ] **Calendar provider API credentials** · free for the API itself; some providers require a verification review before an application can request calendar access broadly
- Why: Without them availability is computed from nothing, which is worse than having no booking page.
- Get it: Create an application in your calendar provider's developer console, enable the calendar scope, and add the redirect URL matching your public URL. Approval steps vary by provider and can take time.
- Verify: An account connects and its events appear as busy time
- [ ] **A transactional email provider with a verified domain** · free tiers usually cover a small team's volume
- Why: Confirmations, invitations, reminders and cancellations all depend on delivery. A booking system whose email fails is worse than a shared inbox.
- Get it: Create an account, add the DNS records, and take the credentials. Domain verification is the slow step, so start it in Phase 1.
- Verify: A confirmation arrives at three different providers and is not in spam
- [ ] **Two people and two mail providers for testing** · free
- Why: Phase 4's concurrency test and Phase 5's delivery test both need someone other than you.
- Get it: Ask a colleague. Ten minutes of their time covers both.
- Verify: A simultaneous booking test and a cross-provider delivery test are both completed
## Quick start
```sh
ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Managed hosting, upgrades and migrations.
- The connector catalogue for conferencing, payments and customer systems.
- Support when a customer cannot book.
- Enterprise controls such as single sign-on and audit logs.
- managed hosting, connectors, upgrades, support, and enterprise controls
- multiple calendar providers
- team routing and round-robin
- payments
- timezone edge-case maturity
If one of those is essential to you, that is the reason to keep paying for Cal.com Teams, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Cal.com Teams
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a self-hosted team booking service with real availability, routing and conflict-safe scheduling to replace Cal.com Teams. Build it in phases, in the order below.
Do not write the whole thing in one pass. Finish a phase, run its "Done when"
check, fix what fails, and only then start the next phase.
### Rule zero
Do not write a scheduling engine. Cal.com is open source and this is a deployment task:
run it, connect the calendars, configure team routing, and operate it. Every phase below
is deploy, configure and verify, and the last is a restore drill you actually perform.
Double booking is the failure that matters here, because it is visible to a customer and
cannot be undone politely. Every phase is arranged around not doing it.
### Stack (fixed, do not substitute)
- Application: Cal.com, self-hosted · the scheduling logic, availability computation and booking pages already exist and are the hard part
- Database: PostgreSQL · bookings are money-adjacent and concurrency-sensitive, which is exactly what it is for
- Calendars: Real provider connections through their APIs · availability computed from a calendar that is not the one you actually use is worse than no availability at all
- Email: A transactional provider with a verified domain · a confirmation that lands in spam is a missed meeting
- Edge: A reverse proxy with automatic certificates · booking pages are public and must be trusted by a stranger's browser
### Data model (create this before Phase 1)
- The application owns its schema. What you own is the configuration, and it is worth recording.
- instance · version, host, database, and the deploy method
- calendar connections · which account, which calendars are read for busy time, and which one receives new events
- event types · duration, buffers before and after, minimum notice, booking window, and which team members
- routing · round robin, collective or a single host, and the fairness rule
- backups · database and configuration, off the machine, with a restore date
### Phase 1 · Running, on your own domain
Build: Provision a server, run the application and its database, put a reverse proxy in front
for certificates, and create the first account. Set the public URL correctly before
anything else, because links are generated from it.
Trap: The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking.
Done when: The booking page loads over HTTPS with a valid certificate. An account exists and can sign in. The public URL matches the domain, verified by checking a generated link. Restarting the server brings everything back automatically.
Do not build yet: No calendars, no team, no bookings.
### Phase 2 · Real calendars, so availability is true
Build: Connect the calendar accounts people really use. Choose which calendars are read for
busy time and which receives new bookings. Then test with a real event: create something
in your calendar and confirm the slot disappears from the booking page.
Trap: Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about.
Done when: Creating an event in a connected calendar removes the corresponding slot from the public booking page within the refresh interval. A booking made through the page appears in the destination calendar with the right details. Disconnecting and reconnecting an account does not duplicate existing bookings.
Do not build yet: No team routing yet.
### Phase 3 · Event types with buffers that reflect reality
Build: Create event types with duration, buffers before and after, minimum notice and a booking
window. Set working hours per person including time zone. Then book as an outsider and
check the offered times against what you would actually accept.
Trap: Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive.
Done when: A booking page shows no slot within the minimum notice period. Consecutive slots respect the configured buffer. A booker in another time zone sees times correctly converted, verified by changing a browser's zone. Booking outside working hours is not offered.
Do not build yet: No team routing yet.
### Phase 4 · Team routing without double booking
Build: Add team members and create team event types with round robin or collective assignment.
Verify the fairness rule matches what you intended. Then test the concurrency case
explicitly: two people booking the last slot at the same moment.
Trap: Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone.
Done when: A round robin event distributes across members according to the configured rule. A collective event only offers slots where every required member is free. Two simultaneous bookings of the last slot result in exactly one confirmation and one clear refusal. A member marked unavailable is not assigned.
Do not build yet: No email verification or backups yet.
### Phase 5 · Email that arrives
Build: Configure a transactional provider with a verified sending domain. Test delivery to
several different mail providers. Verify calendar invitations attach correctly and that
a cancellation actually removes the event from the booker's calendar.
Trap: An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs.
Done when: A confirmation arrives at three different mail providers and is not in spam. The invitation adds an event to the booker's calendar in two different clients. A cancellation removes the event rather than leaving it. A reminder arrives at the configured interval before the meeting.
Do not build yet: Operations come last.
### Phase 6 · Backups, upgrades and the restore drill
Build: Nightly database backups off the machine, including the configuration and credentials
needed for a working restore. An upgrade procedure that backs up first and reads the
release notes. A restore onto a clean server, with a booking made against it.
Trap: Restoring the database without the calendar connection credentials leaves a system that looks correct and computes availability from nothing, which is exactly the state that produces double bookings. Back up the configuration alongside the database, and confirm calendar sync works before pointing the domain at a restored instance.
Done when: Nightly backups land off the server. A restore onto a clean server produces a working instance where a test booking succeeds and appears in a real calendar. The upgrade procedure has been followed once with a backup taken first. The restore date is recorded.
Do not build yet: Nothing after this.
### Out of scope (and why)
- Managed hosting and upgrades. Version upgrades, database migrations and security patches are now your responsibility and they arrive regularly.
- The connector catalogue for video conferencing, payments and customer systems, each of which is credentials and maintenance.
- Support. When a customer cannot book at nine on a Monday, you are the person who finds out why.
- Enterprise controls such as single sign-on, audit logs and administrative policy.
### README must contain
- That the public URL appears in every link and email and must be right before the first booking
- Which calendars are read for busy time, and that missing one produces double bookings
- The buffers, notice period and booking window, and why the defaults are too permissive
- That simultaneous booking of the last slot was tested explicitly
- The backup contents, including configuration, and the date of the last restore
===== AGENTS.md =====
# Agent instructions · Cal.com Teams indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Cal.com, self-hosted, PostgreSQL, Real provider connections through their APIs, A transactional provider with a verified domain, A reverse proxy with automatic certificates. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
## Known traps
- The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking.
- Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about.
- Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive.
- Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone.
- An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs.
===== BUILD_PLAN.md =====
# Build plan · Cal.com Teams
Cal.com self-hosted on a server you own, connected to real calendar accounts so availability reflects actual commitments, with team event types, round robin assignment, buffers and notice periods configured deliberately, confirmation and reminder email that arrives, and a restore you have performed.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Running, on your own domain
The application is up, reachable over HTTPS, and an account exists.
### Steps
1. Provision and close the server
Non-root user, key-only SSH, firewall allowing SSH and web ports.
```sh
ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable
```
2. Run the application and database
Following the project's own deployment guidance for the version you are using.
Files: `docker-compose.yml`
3. Set the public URL and get certificates
Before the first booking. Verify by generating a link and reading it.
Files: `Caddyfile`, `.env`
### Done when
- [ ] The booking page loads over HTTPS with a valid certificate
- [ ] An account can sign in
- [ ] Generated links use the correct public URL
- [ ] A server restart brings everything back
### Watch out
- The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking.
## Phase 2 · Real calendars, so availability is true
Your actual commitments block your availability.
### Steps
1. Connect the provider accounts
Create the credentials the application expects and complete the authorisation flow for each person.
2. Select busy calendars deliberately
Every calendar representing a real commitment. This is the setting that decides whether availability is honest.
3. Test with a real event
Create one, check the public page. Do not trust the configuration screen.
### Done when
- [ ] A real calendar event removes the slot from the public page
- [ ] A booking appears in the destination calendar correctly
- [ ] Reconnecting an account creates no duplicates
- [ ] Every commitment-bearing calendar is selected as busy
### Watch out
- Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about.
## Phase 3 · Event types with buffers that reflect reality
Slots offered are slots you could actually take.
### Steps
1. Configure duration, buffers and notice
All three before publishing. The defaults allow a booking in five minutes with no gap.
2. Set working hours with zones
Per person, with the zone explicit. Zones are where scheduling tools embarrass people.
3. Book as an outsider from another zone
A private window with a different browser zone. Check the times are what you expect.
### Done when
- [ ] No slot is offered within the minimum notice
- [ ] Buffers separate consecutive slots
- [ ] Another time zone sees correctly converted times
- [ ] Slots outside working hours are not offered
### Watch out
- Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive.
## Phase 4 · Team routing without double booking
A team event assigns fairly and never books two people into the same slot by mistake.
### Steps
1. Add members and team event types
Round robin for one-of, collective for all-of. The distinction matters for availability computation.
2. Verify the fairness rule
Book several times and check distribution. Fairness rules vary and the default may not be what you assumed.
3. Test simultaneous booking
Two requests for the last slot at the same moment. Exactly one must succeed.
```sh
ab -n 2 -c 2 -p booking.json -T application/json https://cal.example.com/api/book
```
### Done when
- [ ] Round robin distributes according to the configured rule
- [ ] Collective offers only slots where everyone is free
- [ ] Simultaneous booking of the last slot yields one confirmation
- [ ] An unavailable member is not assigned
### Watch out
- Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone.
## Phase 5 · Email that arrives
Confirmations, reminders and cancellations reach inboxes rather than spam folders.
### Steps
1. Verify the sending domain
The DNS records your provider specifies. Propagation takes hours, so start early.
2. Test across mail providers
Three different ones, checking the spam folder in each.
3. Verify invitations and cancellations
In two different mail clients. Cancellation is the one people forget to test.
### Done when
- [ ] Confirmations arrive at three providers and avoid spam
- [ ] Invitations add an event in two different clients
- [ ] A cancellation removes the event
- [ ] A reminder arrives at the configured interval
### Watch out
- An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs.
## Not in this build
- Managed hosting, upgrades and migrations.
- The connector catalogue for conferencing, payments and customer systems.
- Support when a customer cannot book.
- Enterprise controls such as single sign-on and audit logs.
## After v1, if you want it
- A staging instance restored from last night's backup, which also exercises the restore weekly
- Payment collection at booking time, which the application supports and which adds a webhook to operate
- A synthetic booking check that books and cancels nightly, catching a broken calendar connection before a customer does
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. The public URL. Appears in every link and email, so it must be correct before the first booking.
NEXT_PUBLIC_WEBAPP_URL=https://cal.example.com
# Required · secret. Your PostgreSQL connection string.
DATABASE_URL=postgres://user:pass@db:5432/cal
# Required · secret. Signs sessions. Part of a working restore, so back it up.
NEXTAUTH_SECRET=generate-a-long-random-value
# Required · secret. Encrypts stored calendar credentials. Losing it means reconnecting every calendar, which is why it belongs in the backup.
CALENDSO_ENCRYPTION_KEY=generate-a-long-random-value
# Required · secret. From your calendar provider's developer console, with the redirect URL matching the public URL.
GOOGLE_API_CREDENTIALS={"web":{"client_id":"..."}}
# Required · secret. Transactional provider credentials.
EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587
# Required. Must be on the verified sending domain or delivery will suffer.
EMAIL_FROM=bookings@example.com
You are building a lean indie version of Cal.com Teams.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Cal.com Teams · indie build
Cal.com self-hosted on a server you own, connected to real calendar accounts so availability reflects actual commitments, with team event types, round robin assignment, buffers and notice periods configured deliberately, confirmation and reminder email that arrives, and a restore you have performed.
Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Application | Cal.com, self-hosted | the scheduling logic, availability computation and booking pages already exist and are the hard part |
| Database | PostgreSQL | bookings are money-adjacent and concurrency-sensitive, which is exactly what it is for |
| Calendars | Real provider connections through their APIs | availability computed from a calendar that is not the one you actually use is worse than no availability at all |
| Email | A transactional provider with a verified domain | a confirmation that lands in spam is a missed meeting |
| Edge | A reverse proxy with automatic certificates | booking pages are public and must be trusted by a stranger's browser |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **A Linux server with room for the application and PostgreSQL** · roughly 10 to 25 USD a month
- Why: Both run here, and an undersized server produces slow booking pages, which people abandon.
- Get it: Any virtual server provider. Check the project's current requirements before choosing.
- Verify: The application starts and the booking page loads promptly
- [ ] **A second server for the restore drill** · a few USD if deleted the same day
- Why: Restoring onto the machine that already has the configuration proves nothing, and the missing configuration is exactly what causes a broken restore here.
- Get it: The same provider, smallest suitable plan, destroyed after the drill.
- Verify: You can SSH to it
- [ ] **A domain with DNS you control** · roughly 10 to 15 USD a year
- Why: Needed for certificates, for the public URL, and for the email sending domain records.
- Get it: Any registrar. You will add an A record and several email verification records.
- Verify: The hostname resolves and email records validate
- [ ] **Calendar provider API credentials** · free for the API itself; some providers require a verification review before an application can request calendar access broadly
- Why: Without them availability is computed from nothing, which is worse than having no booking page.
- Get it: Create an application in your calendar provider's developer console, enable the calendar scope, and add the redirect URL matching your public URL. Approval steps vary by provider and can take time.
- Verify: An account connects and its events appear as busy time
- [ ] **A transactional email provider with a verified domain** · free tiers usually cover a small team's volume
- Why: Confirmations, invitations, reminders and cancellations all depend on delivery. A booking system whose email fails is worse than a shared inbox.
- Get it: Create an account, add the DNS records, and take the credentials. Domain verification is the slow step, so start it in Phase 1.
- Verify: A confirmation arrives at three different providers and is not in spam
- [ ] **Two people and two mail providers for testing** · free
- Why: Phase 4's concurrency test and Phase 5's delivery test both need someone other than you.
- Get it: Ask a colleague. Ten minutes of their time covers both.
- Verify: A simultaneous booking test and a cross-provider delivery test are both completed
## Quick start
```sh
ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Managed hosting, upgrades and migrations.
- The connector catalogue for conferencing, payments and customer systems.
- Support when a customer cannot book.
- Enterprise controls such as single sign-on and audit logs.
- managed hosting, connectors, upgrades, support, and enterprise controls
- multiple calendar providers
- team routing and round-robin
- payments
- timezone edge-case maturity
If one of those is essential to you, that is the reason to keep paying for Cal.com Teams, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Cal.com Teams
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a self-hosted team booking service with real availability, routing and conflict-safe scheduling to replace Cal.com Teams. Build it in phases, in the order below.
Do not write the whole thing in one pass. Finish a phase, run its "Done when"
check, fix what fails, and only then start the next phase.
### Rule zero
Do not write a scheduling engine. Cal.com is open source and this is a deployment task:
run it, connect the calendars, configure team routing, and operate it. Every phase below
is deploy, configure and verify, and the last is a restore drill you actually perform.
Double booking is the failure that matters here, because it is visible to a customer and
cannot be undone politely. Every phase is arranged around not doing it.
### Stack (fixed, do not substitute)
- Application: Cal.com, self-hosted · the scheduling logic, availability computation and booking pages already exist and are the hard part
- Database: PostgreSQL · bookings are money-adjacent and concurrency-sensitive, which is exactly what it is for
- Calendars: Real provider connections through their APIs · availability computed from a calendar that is not the one you actually use is worse than no availability at all
- Email: A transactional provider with a verified domain · a confirmation that lands in spam is a missed meeting
- Edge: A reverse proxy with automatic certificates · booking pages are public and must be trusted by a stranger's browser
### Data model (create this before Phase 1)
- The application owns its schema. What you own is the configuration, and it is worth recording.
- instance · version, host, database, and the deploy method
- calendar connections · which account, which calendars are read for busy time, and which one receives new events
- event types · duration, buffers before and after, minimum notice, booking window, and which team members
- routing · round robin, collective or a single host, and the fairness rule
- backups · database and configuration, off the machine, with a restore date
### Phase 1 · Running, on your own domain
Build: Provision a server, run the application and its database, put a reverse proxy in front
for certificates, and create the first account. Set the public URL correctly before
anything else, because links are generated from it.
Trap: The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking.
Done when: The booking page loads over HTTPS with a valid certificate. An account exists and can sign in. The public URL matches the domain, verified by checking a generated link. Restarting the server brings everything back automatically.
Do not build yet: No calendars, no team, no bookings.
### Phase 2 · Real calendars, so availability is true
Build: Connect the calendar accounts people really use. Choose which calendars are read for
busy time and which receives new bookings. Then test with a real event: create something
in your calendar and confirm the slot disappears from the booking page.
Trap: Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about.
Done when: Creating an event in a connected calendar removes the corresponding slot from the public booking page within the refresh interval. A booking made through the page appears in the destination calendar with the right details. Disconnecting and reconnecting an account does not duplicate existing bookings.
Do not build yet: No team routing yet.
### Phase 3 · Event types with buffers that reflect reality
Build: Create event types with duration, buffers before and after, minimum notice and a booking
window. Set working hours per person including time zone. Then book as an outsider and
check the offered times against what you would actually accept.
Trap: Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive.
Done when: A booking page shows no slot within the minimum notice period. Consecutive slots respect the configured buffer. A booker in another time zone sees times correctly converted, verified by changing a browser's zone. Booking outside working hours is not offered.
Do not build yet: No team routing yet.
### Phase 4 · Team routing without double booking
Build: Add team members and create team event types with round robin or collective assignment.
Verify the fairness rule matches what you intended. Then test the concurrency case
explicitly: two people booking the last slot at the same moment.
Trap: Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone.
Done when: A round robin event distributes across members according to the configured rule. A collective event only offers slots where every required member is free. Two simultaneous bookings of the last slot result in exactly one confirmation and one clear refusal. A member marked unavailable is not assigned.
Do not build yet: No email verification or backups yet.
### Phase 5 · Email that arrives
Build: Configure a transactional provider with a verified sending domain. Test delivery to
several different mail providers. Verify calendar invitations attach correctly and that
a cancellation actually removes the event from the booker's calendar.
Trap: An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs.
Done when: A confirmation arrives at three different mail providers and is not in spam. The invitation adds an event to the booker's calendar in two different clients. A cancellation removes the event rather than leaving it. A reminder arrives at the configured interval before the meeting.
Do not build yet: Operations come last.
### Phase 6 · Backups, upgrades and the restore drill
Build: Nightly database backups off the machine, including the configuration and credentials
needed for a working restore. An upgrade procedure that backs up first and reads the
release notes. A restore onto a clean server, with a booking made against it.
Trap: Restoring the database without the calendar connection credentials leaves a system that looks correct and computes availability from nothing, which is exactly the state that produces double bookings. Back up the configuration alongside the database, and confirm calendar sync works before pointing the domain at a restored instance.
Done when: Nightly backups land off the server. A restore onto a clean server produces a working instance where a test booking succeeds and appears in a real calendar. The upgrade procedure has been followed once with a backup taken first. The restore date is recorded.
Do not build yet: Nothing after this.
### Out of scope (and why)
- Managed hosting and upgrades. Version upgrades, database migrations and security patches are now your responsibility and they arrive regularly.
- The connector catalogue for video conferencing, payments and customer systems, each of which is credentials and maintenance.
- Support. When a customer cannot book at nine on a Monday, you are the person who finds out why.
- Enterprise controls such as single sign-on, audit logs and administrative policy.
### README must contain
- That the public URL appears in every link and email and must be right before the first booking
- Which calendars are read for busy time, and that missing one produces double bookings
- The buffers, notice period and booking window, and why the defaults are too permissive
- That simultaneous booking of the last slot was tested explicitly
- The backup contents, including configuration, and the date of the last restore
===== AGENTS.md =====
# Agent instructions · Cal.com Teams indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Cal.com, self-hosted, PostgreSQL, Real provider connections through their APIs, A transactional provider with a verified domain, A reverse proxy with automatic certificates. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
## Known traps
- The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking.
- Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about.
- Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive.
- Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone.
- An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs.
===== BUILD_PLAN.md =====
# Build plan · Cal.com Teams
Cal.com self-hosted on a server you own, connected to real calendar accounts so availability reflects actual commitments, with team event types, round robin assignment, buffers and notice periods configured deliberately, confirmation and reminder email that arrives, and a restore you have performed.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Running, on your own domain
The application is up, reachable over HTTPS, and an account exists.
### Steps
1. Provision and close the server
Non-root user, key-only SSH, firewall allowing SSH and web ports.
```sh
ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable
```
2. Run the application and database
Following the project's own deployment guidance for the version you are using.
Files: `docker-compose.yml`
3. Set the public URL and get certificates
Before the first booking. Verify by generating a link and reading it.
Files: `Caddyfile`, `.env`
### Done when
- [ ] The booking page loads over HTTPS with a valid certificate
- [ ] An account can sign in
- [ ] Generated links use the correct public URL
- [ ] A server restart brings everything back
### Watch out
- The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking.
## Phase 2 · Real calendars, so availability is true
Your actual commitments block your availability.
### Steps
1. Connect the provider accounts
Create the credentials the application expects and complete the authorisation flow for each person.
2. Select busy calendars deliberately
Every calendar representing a real commitment. This is the setting that decides whether availability is honest.
3. Test with a real event
Create one, check the public page. Do not trust the configuration screen.
### Done when
- [ ] A real calendar event removes the slot from the public page
- [ ] A booking appears in the destination calendar correctly
- [ ] Reconnecting an account creates no duplicates
- [ ] Every commitment-bearing calendar is selected as busy
### Watch out
- Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about.
## Phase 3 · Event types with buffers that reflect reality
Slots offered are slots you could actually take.
### Steps
1. Configure duration, buffers and notice
All three before publishing. The defaults allow a booking in five minutes with no gap.
2. Set working hours with zones
Per person, with the zone explicit. Zones are where scheduling tools embarrass people.
3. Book as an outsider from another zone
A private window with a different browser zone. Check the times are what you expect.
### Done when
- [ ] No slot is offered within the minimum notice
- [ ] Buffers separate consecutive slots
- [ ] Another time zone sees correctly converted times
- [ ] Slots outside working hours are not offered
### Watch out
- Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive.
## Phase 4 · Team routing without double booking
A team event assigns fairly and never books two people into the same slot by mistake.
### Steps
1. Add members and team event types
Round robin for one-of, collective for all-of. The distinction matters for availability computation.
2. Verify the fairness rule
Book several times and check distribution. Fairness rules vary and the default may not be what you assumed.
3. Test simultaneous booking
Two requests for the last slot at the same moment. Exactly one must succeed.
```sh
ab -n 2 -c 2 -p booking.json -T application/json https://cal.example.com/api/book
```
### Done when
- [ ] Round robin distributes according to the configured rule
- [ ] Collective offers only slots where everyone is free
- [ ] Simultaneous booking of the last slot yields one confirmation
- [ ] An unavailable member is not assigned
### Watch out
- Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone.
## Phase 5 · Email that arrives
Confirmations, reminders and cancellations reach inboxes rather than spam folders.
### Steps
1. Verify the sending domain
The DNS records your provider specifies. Propagation takes hours, so start early.
2. Test across mail providers
Three different ones, checking the spam folder in each.
3. Verify invitations and cancellations
In two different mail clients. Cancellation is the one people forget to test.
### Done when
- [ ] Confirmations arrive at three providers and avoid spam
- [ ] Invitations add an event in two different clients
- [ ] A cancellation removes the event
- [ ] A reminder arrives at the configured interval
### Watch out
- An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs.
## Not in this build
- Managed hosting, upgrades and migrations.
- The connector catalogue for conferencing, payments and customer systems.
- Support when a customer cannot book.
- Enterprise controls such as single sign-on and audit logs.
## After v1, if you want it
- A staging instance restored from last night's backup, which also exercises the restore weekly
- Payment collection at booking time, which the application supports and which adds a webhook to operate
- A synthetic booking check that books and cancels nightly, catching a broken calendar connection before a customer does
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. The public URL. Appears in every link and email, so it must be correct before the first booking.
NEXT_PUBLIC_WEBAPP_URL=https://cal.example.com
# Required · secret. Your PostgreSQL connection string.
DATABASE_URL=postgres://user:pass@db:5432/cal
# Required · secret. Signs sessions. Part of a working restore, so back it up.
NEXTAUTH_SECRET=generate-a-long-random-value
# Required · secret. Encrypts stored calendar credentials. Losing it means reconnecting every calendar, which is why it belongs in the backup.
CALENDSO_ENCRYPTION_KEY=generate-a-long-random-value
# Required · secret. From your calendar provider's developer console, with the redirect URL matching the public URL.
GOOGLE_API_CREDENTIALS={"web":{"client_id":"..."}}
# Required · secret. Transactional provider credentials.
EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587
# Required. Must be on the verified sending domain or delivery will suffer.
EMAIL_FROM=bookings@example.com
You are building a production product version of Cal.com Teams.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== PRODUCT.md =====
# Cal.com Teams · product brief
## Problem
The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Cal.com Teams, self-host a team booking service with event types, availability, and routing. The hard boundary is managed hosting, connectors, upgrades, support, and enterprise controls, plus calendar integrations and reliability.
## Product outcome
A booking service on your own infrastructure where availability reflects real commitments, buffers and notice are set deliberately, simultaneous bookings cannot double book, email reliably arrives, and a restore has been performed and dated.
## Target user
A builder who needs a maintainable product foundation, not a one-off demo.
## Required capabilities
- Google Calendar OAuth credentials
- PostgreSQL
- public deploy target
- Resend API key
- domain with HTTPS
## Explicit non-goals for v1
- Managed hosting, upgrades and migrations.
- The connector catalogue for conferencing, payments and customer systems.
- Support when a customer cannot book.
- Enterprise controls such as single sign-on and audit logs.
- managed hosting, connectors, upgrades, support, and enterprise controls
- multiple calendar providers
- team routing and round-robin
- payments
- timezone edge-case maturity
## Success criteria
- A real calendar event is confirmed to remove the slot
- Simultaneous booking of the last slot yields exactly one confirmation
- Confirmations arrive at three providers and avoid spam
- Invitations and cancellations are verified in two mail clients
- A restore produced a working booking that reached a calendar, and is dated
===== BRIEF.md =====
# Build brief · Cal.com Teams
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a self-hosted team booking service with real availability, routing and conflict-safe scheduling to replace Cal.com Teams. Build it in phases, in the order below.
Do not write the whole thing in one pass. Finish a phase, run its "Done when"
check, fix what fails, and only then start the next phase.
### Rule zero
Do not write a scheduling engine. Cal.com is open source and this is a deployment task:
run it, connect the calendars, configure team routing, and operate it. Every phase below
is deploy, configure and verify, and the last is a restore drill you actually perform.
Double booking is the failure that matters here, because it is visible to a customer and
cannot be undone politely. Every phase is arranged around not doing it.
### Stack (fixed, do not substitute)
- Application: Cal.com, self-hosted · the scheduling logic, availability computation and booking pages already exist and are the hard part
- Database: PostgreSQL · bookings are money-adjacent and concurrency-sensitive, which is exactly what it is for
- Calendars: Real provider connections through their APIs · availability computed from a calendar that is not the one you actually use is worse than no availability at all
- Email: A transactional provider with a verified domain · a confirmation that lands in spam is a missed meeting
- Edge: A reverse proxy with automatic certificates · booking pages are public and must be trusted by a stranger's browser
### Data model (create this before Phase 1)
- The application owns its schema. What you own is the configuration, and it is worth recording.
- instance · version, host, database, and the deploy method
- calendar connections · which account, which calendars are read for busy time, and which one receives new events
- event types · duration, buffers before and after, minimum notice, booking window, and which team members
- routing · round robin, collective or a single host, and the fairness rule
- backups · database and configuration, off the machine, with a restore date
### Phase 1 · Running, on your own domain
Build: Provision a server, run the application and its database, put a reverse proxy in front
for certificates, and create the first account. Set the public URL correctly before
anything else, because links are generated from it.
Trap: The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking.
Done when: The booking page loads over HTTPS with a valid certificate. An account exists and can sign in. The public URL matches the domain, verified by checking a generated link. Restarting the server brings everything back automatically.
Do not build yet: No calendars, no team, no bookings.
### Phase 2 · Real calendars, so availability is true
Build: Connect the calendar accounts people really use. Choose which calendars are read for
busy time and which receives new bookings. Then test with a real event: create something
in your calendar and confirm the slot disappears from the booking page.
Trap: Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about.
Done when: Creating an event in a connected calendar removes the corresponding slot from the public booking page within the refresh interval. A booking made through the page appears in the destination calendar with the right details. Disconnecting and reconnecting an account does not duplicate existing bookings.
Do not build yet: No team routing yet.
### Phase 3 · Event types with buffers that reflect reality
Build: Create event types with duration, buffers before and after, minimum notice and a booking
window. Set working hours per person including time zone. Then book as an outsider and
check the offered times against what you would actually accept.
Trap: Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive.
Done when: A booking page shows no slot within the minimum notice period. Consecutive slots respect the configured buffer. A booker in another time zone sees times correctly converted, verified by changing a browser's zone. Booking outside working hours is not offered.
Do not build yet: No team routing yet.
### Phase 4 · Team routing without double booking
Build: Add team members and create team event types with round robin or collective assignment.
Verify the fairness rule matches what you intended. Then test the concurrency case
explicitly: two people booking the last slot at the same moment.
Trap: Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone.
Done when: A round robin event distributes across members according to the configured rule. A collective event only offers slots where every required member is free. Two simultaneous bookings of the last slot result in exactly one confirmation and one clear refusal. A member marked unavailable is not assigned.
Do not build yet: No email verification or backups yet.
### Phase 5 · Email that arrives
Build: Configure a transactional provider with a verified sending domain. Test delivery to
several different mail providers. Verify calendar invitations attach correctly and that
a cancellation actually removes the event from the booker's calendar.
Trap: An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs.
Done when: A confirmation arrives at three different mail providers and is not in spam. The invitation adds an event to the booker's calendar in two different clients. A cancellation removes the event rather than leaving it. A reminder arrives at the configured interval before the meeting.
Do not build yet: Operations come last.
### Phase 6 · Backups, upgrades and the restore drill
Build: Nightly database backups off the machine, including the configuration and credentials
needed for a working restore. An upgrade procedure that backs up first and reads the
release notes. A restore onto a clean server, with a booking made against it.
Trap: Restoring the database without the calendar connection credentials leaves a system that looks correct and computes availability from nothing, which is exactly the state that produces double bookings. Back up the configuration alongside the database, and confirm calendar sync works before pointing the domain at a restored instance.
Done when: Nightly backups land off the server. A restore onto a clean server produces a working instance where a test booking succeeds and appears in a real calendar. The upgrade procedure has been followed once with a backup taken first. The restore date is recorded.
Do not build yet: Nothing after this.
### Out of scope (and why)
- Managed hosting and upgrades. Version upgrades, database migrations and security patches are now your responsibility and they arrive regularly.
- The connector catalogue for video conferencing, payments and customer systems, each of which is credentials and maintenance.
- Support. When a customer cannot book at nine on a Monday, you are the person who finds out why.
- Enterprise controls such as single sign-on, audit logs and administrative policy.
### README must contain
- That the public URL appears in every link and email and must be right before the first booking
- Which calendars are read for busy time, and that missing one produces double bookings
- The buffers, notice period and booking window, and why the defaults are too permissive
- That simultaneous booking of the last slot was tested explicitly
- The backup contents, including configuration, and the date of the last restore
===== ARCHITECTURE.md =====
# Architecture · Cal.com Teams
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Application | Cal.com, self-hosted | the scheduling logic, availability computation and booking pages already exist and are the hard part |
| Database | PostgreSQL | bookings are money-adjacent and concurrency-sensitive, which is exactly what it is for |
| Calendars | Real provider connections through their APIs | availability computed from a calendar that is not the one you actually use is worse than no availability at all |
| Email | A transactional provider with a verified domain | a confirmation that lands in spam is a missed meeting |
| Edge | A reverse proxy with automatic certificates | booking pages are public and must be trusted by a stranger's browser |
## Modules
Each module has one owner concern and a documented way to replace it.
| Module | Owns | How to replace it |
| --- | --- | --- |
| Application | Availability, booking and the public pages. | Not yours to replace. Upgrading it is the only change you make here. |
| Calendars | The truth about who is busy. | Providers differ; the rule that every commitment-bearing calendar is read as busy does not. |
| Event types | What may be booked and when. | Configuration. The buffers and notice period are the settings that make the system usable in practice. |
| Email | Confirmations, invitations and reminders. | Providers are interchangeable. Verified domain and cross-provider testing survive any change. |
| Backups | Database and configuration together. | Destination is replaceable; including the encryption key and calendar credentials is not. |
## Configuration
Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists.
- `NEXT_PUBLIC_WEBAPP_URL` · required · The public URL. Appears in every link and email, so it must be correct before the first booking.
- `DATABASE_URL` · required, secret · Your PostgreSQL connection string.
- `NEXTAUTH_SECRET` · required, secret · Signs sessions. Part of a working restore, so back it up.
- `CALENDSO_ENCRYPTION_KEY` · required, secret · Encrypts stored calendar credentials. Losing it means reconnecting every calendar, which is why it belongs in the backup.
- `GOOGLE_API_CREDENTIALS` · required, secret · From your calendar provider's developer console, with the redirect URL matching the public URL.
- `EMAIL_SERVER` · required, secret · Transactional provider credentials.
- `EMAIL_FROM` · required · Must be on the verified sending domain or delivery will suffer.
## Production baseline
- Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives.
- Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised.
- Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts.
- Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists.
- Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path.
## Decision records
For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
===== AGENTS.md =====
# Agent instructions · Cal.com Teams product build
- Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Cal.com, self-hosted, PostgreSQL, Real provider connections through their APIs, A transactional provider with a verified domain, A reverse proxy with automatic certificates.
- Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit.
- Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present.
- Never invent cryptography or silently weaken a requirement to make a check pass.
- Put every external service behind an interface with a deterministic fake for tests.
- Add migrations and rollback or recovery notes for every persistent data change.
- Log useful operational context without credentials, tokens, passwords or personal data.
- Update documentation and run every check before completing a milestone.
## Known traps
- The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking.
- Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about.
- Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive.
- Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone.
- An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs.
- Restoring the database without the calendar connection credentials leaves a system that looks correct and computes availability from nothing, which is exactly the state that produces double bookings. Back up the configuration alongside the database, and confirm calendar sync works before pointing the domain at a restored instance.
===== MILESTONES.md =====
# Delivery milestones · Cal.com Teams
Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer.
## M1 · Running, on your own domain
The application is up, reachable over HTTPS, and an account exists.
### Steps
1. Provision and close the server
Non-root user, key-only SSH, firewall allowing SSH and web ports.
```sh
ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable
```
2. Run the application and database
Following the project's own deployment guidance for the version you are using.
Files: `docker-compose.yml`
3. Set the public URL and get certificates
Before the first booking. Verify by generating a link and reading it.
Files: `Caddyfile`, `.env`
### Done when
- [ ] The booking page loads over HTTPS with a valid certificate
- [ ] An account can sign in
- [ ] Generated links use the correct public URL
- [ ] A server restart brings everything back
### Watch out
- The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking.
## M2 · Real calendars, so availability is true
Your actual commitments block your availability.
### Steps
1. Connect the provider accounts
Create the credentials the application expects and complete the authorisation flow for each person.
2. Select busy calendars deliberately
Every calendar representing a real commitment. This is the setting that decides whether availability is honest.
3. Test with a real event
Create one, check the public page. Do not trust the configuration screen.
### Done when
- [ ] A real calendar event removes the slot from the public page
- [ ] A booking appears in the destination calendar correctly
- [ ] Reconnecting an account creates no duplicates
- [ ] Every commitment-bearing calendar is selected as busy
### Watch out
- Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about.
## M3 · Event types with buffers that reflect reality
Slots offered are slots you could actually take.
### Steps
1. Configure duration, buffers and notice
All three before publishing. The defaults allow a booking in five minutes with no gap.
2. Set working hours with zones
Per person, with the zone explicit. Zones are where scheduling tools embarrass people.
3. Book as an outsider from another zone
A private window with a different browser zone. Check the times are what you expect.
### Done when
- [ ] No slot is offered within the minimum notice
- [ ] Buffers separate consecutive slots
- [ ] Another time zone sees correctly converted times
- [ ] Slots outside working hours are not offered
### Watch out
- Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive.
## M4 · Team routing without double booking
A team event assigns fairly and never books two people into the same slot by mistake.
### Steps
1. Add members and team event types
Round robin for one-of, collective for all-of. The distinction matters for availability computation.
2. Verify the fairness rule
Book several times and check distribution. Fairness rules vary and the default may not be what you assumed.
3. Test simultaneous booking
Two requests for the last slot at the same moment. Exactly one must succeed.
```sh
ab -n 2 -c 2 -p booking.json -T application/json https://cal.example.com/api/book
```
### Done when
- [ ] Round robin distributes according to the configured rule
- [ ] Collective offers only slots where everyone is free
- [ ] Simultaneous booking of the last slot yields one confirmation
- [ ] An unavailable member is not assigned
### Watch out
- Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone.
## M5 · Email that arrives
Confirmations, reminders and cancellations reach inboxes rather than spam folders.
### Steps
1. Verify the sending domain
The DNS records your provider specifies. Propagation takes hours, so start early.
2. Test across mail providers
Three different ones, checking the spam folder in each.
3. Verify invitations and cancellations
In two different mail clients. Cancellation is the one people forget to test.
### Done when
- [ ] Confirmations arrive at three providers and avoid spam
- [ ] Invitations add an event in two different clients
- [ ] A cancellation removes the event
- [ ] A reminder arrives at the configured interval
### Watch out
- An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs.
## M6 · Backups, upgrades and the restore drill (production only)
You can rebuild it, and an upgrade is a planned event rather than a surprise.
### Steps
1. Back up database and configuration
Both, dated, off the machine. The calendar credentials are part of a working restore.
Files: `scripts/backup.sh`
```sh
pg_dump "$DATABASE_URL" | gzip > cal-$(date +%F).sql.gz
```
2. Restore onto a clean server
Then make a real booking and confirm it reaches a calendar. That last step is the actual test.
Files: `README.md`
3. Write and follow the upgrade procedure
Back up, read breaking changes, upgrade, verify a booking. Follow it once and record the time.
Files: `docs/upgrade.md`
### Done when
- [ ] Nightly backups land off the server
- [ ] A restore produces a working booking that reaches a calendar
- [ ] The upgrade procedure has been followed and timed
- [ ] The restore date is recorded
### Watch out
- Restoring the database without the calendar connection credentials leaves a system that looks correct and computes availability from nothing, which is exactly the state that produces double bookings. Back up the configuration alongside the database, and confirm calendar sync works before pointing the domain at a restored instance.
===== OPERATIONS.md =====
# Operations · Cal.com Teams
## Backup
Nightly database dump plus the environment configuration, both off the machine. The calendar encryption key is part of a working restore, and without it every calendar must be reconnected by hand.
## Restore
Fresh server, restore database and configuration, verify calendar sync, then make a real booking and confirm it reaches a calendar. Only then repoint the domain.
Do a restore drill before the first real user, and write the date here when it passes.
## Monitoring
Uptime on the booking page, email delivery and bounce rates, and calendar sync failures. A silently failed calendar connection is the condition that produces double bookings, and it does not announce itself.
## Incident checklist
For a double booking, check calendar connection health first, then the busy calendar selection. For a customer who cannot book, check the notice period and booking window before assuming a bug. Keep the pre-upgrade backup until a full booking cycle has completed successfully.
1. Contain the issue without destroying evidence or user data.
2. Record the timeline and affected scope.
3. Rotate exposed secrets and revoke compromised sessions or credentials.
4. Restore from a verified backup when needed.
5. Document the root cause, the remediation and the regression test.
## Release gate
- [ ] A real calendar event is confirmed to remove the slot
- [ ] Simultaneous booking of the last slot yields exactly one confirmation
- [ ] Confirmations arrive at three providers and avoid spam
- [ ] Invitations and cancellations are verified in two mail clients
- [ ] A restore produced a working booking that reached a calendar, and is dated
## Launch constraint
Do not market omitted Cal.com Teams capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. The public URL. Appears in every link and email, so it must be correct before the first booking.
NEXT_PUBLIC_WEBAPP_URL=https://cal.example.com
# Required · secret. Your PostgreSQL connection string.
DATABASE_URL=postgres://user:pass@db:5432/cal
# Required · secret. Signs sessions. Part of a working restore, so back it up.
NEXTAUTH_SECRET=generate-a-long-random-value
# Required · secret. Encrypts stored calendar credentials. Losing it means reconnecting every calendar, which is why it belongs in the backup.
CALENDSO_ENCRYPTION_KEY=generate-a-long-random-value
# Required · secret. From your calendar provider's developer console, with the redirect URL matching the public URL.
GOOGLE_API_CREDENTIALS={"web":{"client_id":"..."}}
# Required · secret. Transactional provider credentials.
EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587
# Required. Must be on the verified sending domain or delivery will suffer.
EMAIL_FROM=bookings@example.com
# Cal.com Teams · indie build Cal.com self-hosted on a server you own, connected to real calendar accounts so availability reflects actual commitments, with team event types, round robin assignment, buffers and notice periods configured deliberately, confirmation and reminder email that arrives, and a restore you have performed. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Application | Cal.com, self-hosted | the scheduling logic, availability computation and booking pages already exist and are the hard part | | Database | PostgreSQL | bookings are money-adjacent and concurrency-sensitive, which is exactly what it is for | | Calendars | Real provider connections through their APIs | availability computed from a calendar that is not the one you actually use is worse than no availability at all | | Email | A transactional provider with a verified domain | a confirmation that lands in spam is a missed meeting | | Edge | A reverse proxy with automatic certificates | booking pages are public and must be trusted by a stranger's browser | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **A Linux server with room for the application and PostgreSQL** · roughly 10 to 25 USD a month - Why: Both run here, and an undersized server produces slow booking pages, which people abandon. - Get it: Any virtual server provider. Check the project's current requirements before choosing. - Verify: The application starts and the booking page loads promptly - [ ] **A second server for the restore drill** · a few USD if deleted the same day - Why: Restoring onto the machine that already has the configuration proves nothing, and the missing configuration is exactly what causes a broken restore here. - Get it: The same provider, smallest suitable plan, destroyed after the drill. - Verify: You can SSH to it - [ ] **A domain with DNS you control** · roughly 10 to 15 USD a year - Why: Needed for certificates, for the public URL, and for the email sending domain records. - Get it: Any registrar. You will add an A record and several email verification records. - Verify: The hostname resolves and email records validate - [ ] **Calendar provider API credentials** · free for the API itself; some providers require a verification review before an application can request calendar access broadly - Why: Without them availability is computed from nothing, which is worse than having no booking page. - Get it: Create an application in your calendar provider's developer console, enable the calendar scope, and add the redirect URL matching your public URL. Approval steps vary by provider and can take time. - Verify: An account connects and its events appear as busy time - [ ] **A transactional email provider with a verified domain** · free tiers usually cover a small team's volume - Why: Confirmations, invitations, reminders and cancellations all depend on delivery. A booking system whose email fails is worse than a shared inbox. - Get it: Create an account, add the DNS records, and take the credentials. Domain verification is the slow step, so start it in Phase 1. - Verify: A confirmation arrives at three different providers and is not in spam - [ ] **Two people and two mail providers for testing** · free - Why: Phase 4's concurrency test and Phase 5's delivery test both need someone other than you. - Get it: Ask a colleague. Ten minutes of their time covers both. - Verify: A simultaneous booking test and a cross-provider delivery test are both completed ## Quick start ```sh ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Managed hosting, upgrades and migrations. - The connector catalogue for conferencing, payments and customer systems. - Support when a customer cannot book. - Enterprise controls such as single sign-on and audit logs. - managed hosting, connectors, upgrades, support, and enterprise controls - multiple calendar providers - team routing and round-robin - payments - timezone edge-case maturity If one of those is essential to you, that is the reason to keep paying for Cal.com Teams, and the README should say so rather than pretend.
# Build brief · Cal.com Teams The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted team booking service with real availability, routing and conflict-safe scheduling to replace Cal.com Teams. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a scheduling engine. Cal.com is open source and this is a deployment task: run it, connect the calendars, configure team routing, and operate it. Every phase below is deploy, configure and verify, and the last is a restore drill you actually perform. Double booking is the failure that matters here, because it is visible to a customer and cannot be undone politely. Every phase is arranged around not doing it. ### Stack (fixed, do not substitute) - Application: Cal.com, self-hosted · the scheduling logic, availability computation and booking pages already exist and are the hard part - Database: PostgreSQL · bookings are money-adjacent and concurrency-sensitive, which is exactly what it is for - Calendars: Real provider connections through their APIs · availability computed from a calendar that is not the one you actually use is worse than no availability at all - Email: A transactional provider with a verified domain · a confirmation that lands in spam is a missed meeting - Edge: A reverse proxy with automatic certificates · booking pages are public and must be trusted by a stranger's browser ### Data model (create this before Phase 1) - The application owns its schema. What you own is the configuration, and it is worth recording. - instance · version, host, database, and the deploy method - calendar connections · which account, which calendars are read for busy time, and which one receives new events - event types · duration, buffers before and after, minimum notice, booking window, and which team members - routing · round robin, collective or a single host, and the fairness rule - backups · database and configuration, off the machine, with a restore date ### Phase 1 · Running, on your own domain Build: Provision a server, run the application and its database, put a reverse proxy in front for certificates, and create the first account. Set the public URL correctly before anything else, because links are generated from it. Trap: The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking. Done when: The booking page loads over HTTPS with a valid certificate. An account exists and can sign in. The public URL matches the domain, verified by checking a generated link. Restarting the server brings everything back automatically. Do not build yet: No calendars, no team, no bookings. ### Phase 2 · Real calendars, so availability is true Build: Connect the calendar accounts people really use. Choose which calendars are read for busy time and which receives new bookings. Then test with a real event: create something in your calendar and confirm the slot disappears from the booking page. Trap: Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about. Done when: Creating an event in a connected calendar removes the corresponding slot from the public booking page within the refresh interval. A booking made through the page appears in the destination calendar with the right details. Disconnecting and reconnecting an account does not duplicate existing bookings. Do not build yet: No team routing yet. ### Phase 3 · Event types with buffers that reflect reality Build: Create event types with duration, buffers before and after, minimum notice and a booking window. Set working hours per person including time zone. Then book as an outsider and check the offered times against what you would actually accept. Trap: Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive. Done when: A booking page shows no slot within the minimum notice period. Consecutive slots respect the configured buffer. A booker in another time zone sees times correctly converted, verified by changing a browser's zone. Booking outside working hours is not offered. Do not build yet: No team routing yet. ### Phase 4 · Team routing without double booking Build: Add team members and create team event types with round robin or collective assignment. Verify the fairness rule matches what you intended. Then test the concurrency case explicitly: two people booking the last slot at the same moment. Trap: Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone. Done when: A round robin event distributes across members according to the configured rule. A collective event only offers slots where every required member is free. Two simultaneous bookings of the last slot result in exactly one confirmation and one clear refusal. A member marked unavailable is not assigned. Do not build yet: No email verification or backups yet. ### Phase 5 · Email that arrives Build: Configure a transactional provider with a verified sending domain. Test delivery to several different mail providers. Verify calendar invitations attach correctly and that a cancellation actually removes the event from the booker's calendar. Trap: An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs. Done when: A confirmation arrives at three different mail providers and is not in spam. The invitation adds an event to the booker's calendar in two different clients. A cancellation removes the event rather than leaving it. A reminder arrives at the configured interval before the meeting. Do not build yet: Operations come last. ### Phase 6 · Backups, upgrades and the restore drill Build: Nightly database backups off the machine, including the configuration and credentials needed for a working restore. An upgrade procedure that backs up first and reads the release notes. A restore onto a clean server, with a booking made against it. Trap: Restoring the database without the calendar connection credentials leaves a system that looks correct and computes availability from nothing, which is exactly the state that produces double bookings. Back up the configuration alongside the database, and confirm calendar sync works before pointing the domain at a restored instance. Done when: Nightly backups land off the server. A restore onto a clean server produces a working instance where a test booking succeeds and appears in a real calendar. The upgrade procedure has been followed once with a backup taken first. The restore date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Managed hosting and upgrades. Version upgrades, database migrations and security patches are now your responsibility and they arrive regularly. - The connector catalogue for video conferencing, payments and customer systems, each of which is credentials and maintenance. - Support. When a customer cannot book at nine on a Monday, you are the person who finds out why. - Enterprise controls such as single sign-on, audit logs and administrative policy. ### README must contain - That the public URL appears in every link and email and must be right before the first booking - Which calendars are read for busy time, and that missing one produces double bookings - The buffers, notice period and booking window, and why the defaults are too permissive - That simultaneous booking of the last slot was tested explicitly - The backup contents, including configuration, and the date of the last restore
# Agent instructions · Cal.com Teams indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Cal.com, self-hosted, PostgreSQL, Real provider connections through their APIs, A transactional provider with a verified domain, A reverse proxy with automatic certificates. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking. - Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about. - Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive. - Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone. - An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs.
# Build plan · Cal.com Teams Cal.com self-hosted on a server you own, connected to real calendar accounts so availability reflects actual commitments, with team event types, round robin assignment, buffers and notice periods configured deliberately, confirmation and reminder email that arrives, and a restore you have performed. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Running, on your own domain The application is up, reachable over HTTPS, and an account exists. ### Steps 1. Provision and close the server Non-root user, key-only SSH, firewall allowing SSH and web ports. ```sh ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable ``` 2. Run the application and database Following the project's own deployment guidance for the version you are using. Files: `docker-compose.yml` 3. Set the public URL and get certificates Before the first booking. Verify by generating a link and reading it. Files: `Caddyfile`, `.env` ### Done when - [ ] The booking page loads over HTTPS with a valid certificate - [ ] An account can sign in - [ ] Generated links use the correct public URL - [ ] A server restart brings everything back ### Watch out - The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking. ## Phase 2 · Real calendars, so availability is true Your actual commitments block your availability. ### Steps 1. Connect the provider accounts Create the credentials the application expects and complete the authorisation flow for each person. 2. Select busy calendars deliberately Every calendar representing a real commitment. This is the setting that decides whether availability is honest. 3. Test with a real event Create one, check the public page. Do not trust the configuration screen. ### Done when - [ ] A real calendar event removes the slot from the public page - [ ] A booking appears in the destination calendar correctly - [ ] Reconnecting an account creates no duplicates - [ ] Every commitment-bearing calendar is selected as busy ### Watch out - Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about. ## Phase 3 · Event types with buffers that reflect reality Slots offered are slots you could actually take. ### Steps 1. Configure duration, buffers and notice All three before publishing. The defaults allow a booking in five minutes with no gap. 2. Set working hours with zones Per person, with the zone explicit. Zones are where scheduling tools embarrass people. 3. Book as an outsider from another zone A private window with a different browser zone. Check the times are what you expect. ### Done when - [ ] No slot is offered within the minimum notice - [ ] Buffers separate consecutive slots - [ ] Another time zone sees correctly converted times - [ ] Slots outside working hours are not offered ### Watch out - Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive. ## Phase 4 · Team routing without double booking A team event assigns fairly and never books two people into the same slot by mistake. ### Steps 1. Add members and team event types Round robin for one-of, collective for all-of. The distinction matters for availability computation. 2. Verify the fairness rule Book several times and check distribution. Fairness rules vary and the default may not be what you assumed. 3. Test simultaneous booking Two requests for the last slot at the same moment. Exactly one must succeed. ```sh ab -n 2 -c 2 -p booking.json -T application/json https://cal.example.com/api/book ``` ### Done when - [ ] Round robin distributes according to the configured rule - [ ] Collective offers only slots where everyone is free - [ ] Simultaneous booking of the last slot yields one confirmation - [ ] An unavailable member is not assigned ### Watch out - Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone. ## Phase 5 · Email that arrives Confirmations, reminders and cancellations reach inboxes rather than spam folders. ### Steps 1. Verify the sending domain The DNS records your provider specifies. Propagation takes hours, so start early. 2. Test across mail providers Three different ones, checking the spam folder in each. 3. Verify invitations and cancellations In two different mail clients. Cancellation is the one people forget to test. ### Done when - [ ] Confirmations arrive at three providers and avoid spam - [ ] Invitations add an event in two different clients - [ ] A cancellation removes the event - [ ] A reminder arrives at the configured interval ### Watch out - An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs. ## Not in this build - Managed hosting, upgrades and migrations. - The connector catalogue for conferencing, payments and customer systems. - Support when a customer cannot book. - Enterprise controls such as single sign-on and audit logs. ## After v1, if you want it - A staging instance restored from last night's backup, which also exercises the restore weekly - Payment collection at booking time, which the application supports and which adds a webhook to operate - A synthetic booking check that books and cancels nightly, catching a broken calendar connection before a customer does
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. The public URL. Appears in every link and email, so it must be correct before the first booking.
NEXT_PUBLIC_WEBAPP_URL=https://cal.example.com
# Required · secret. Your PostgreSQL connection string.
DATABASE_URL=postgres://user:pass@db:5432/cal
# Required · secret. Signs sessions. Part of a working restore, so back it up.
NEXTAUTH_SECRET=generate-a-long-random-value
# Required · secret. Encrypts stored calendar credentials. Losing it means reconnecting every calendar, which is why it belongs in the backup.
CALENDSO_ENCRYPTION_KEY=generate-a-long-random-value
# Required · secret. From your calendar provider's developer console, with the redirect URL matching the public URL.
GOOGLE_API_CREDENTIALS={"web":{"client_id":"..."}}
# Required · secret. Transactional provider credentials.
EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587
# Required. Must be on the verified sending domain or delivery will suffer.
EMAIL_FROM=bookings@example.com
# Cal.com Teams · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Cal.com Teams, self-host a team booking service with event types, availability, and routing. The hard boundary is managed hosting, connectors, upgrades, support, and enterprise controls, plus calendar integrations and reliability. ## Product outcome A booking service on your own infrastructure where availability reflects real commitments, buffers and notice are set deliberately, simultaneous bookings cannot double book, email reliably arrives, and a restore has been performed and dated. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Google Calendar OAuth credentials - PostgreSQL - public deploy target - Resend API key - domain with HTTPS ## Explicit non-goals for v1 - Managed hosting, upgrades and migrations. - The connector catalogue for conferencing, payments and customer systems. - Support when a customer cannot book. - Enterprise controls such as single sign-on and audit logs. - managed hosting, connectors, upgrades, support, and enterprise controls - multiple calendar providers - team routing and round-robin - payments - timezone edge-case maturity ## Success criteria - A real calendar event is confirmed to remove the slot - Simultaneous booking of the last slot yields exactly one confirmation - Confirmations arrive at three providers and avoid spam - Invitations and cancellations are verified in two mail clients - A restore produced a working booking that reached a calendar, and is dated
# Build brief · Cal.com Teams The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a self-hosted team booking service with real availability, routing and conflict-safe scheduling to replace Cal.com Teams. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not write a scheduling engine. Cal.com is open source and this is a deployment task: run it, connect the calendars, configure team routing, and operate it. Every phase below is deploy, configure and verify, and the last is a restore drill you actually perform. Double booking is the failure that matters here, because it is visible to a customer and cannot be undone politely. Every phase is arranged around not doing it. ### Stack (fixed, do not substitute) - Application: Cal.com, self-hosted · the scheduling logic, availability computation and booking pages already exist and are the hard part - Database: PostgreSQL · bookings are money-adjacent and concurrency-sensitive, which is exactly what it is for - Calendars: Real provider connections through their APIs · availability computed from a calendar that is not the one you actually use is worse than no availability at all - Email: A transactional provider with a verified domain · a confirmation that lands in spam is a missed meeting - Edge: A reverse proxy with automatic certificates · booking pages are public and must be trusted by a stranger's browser ### Data model (create this before Phase 1) - The application owns its schema. What you own is the configuration, and it is worth recording. - instance · version, host, database, and the deploy method - calendar connections · which account, which calendars are read for busy time, and which one receives new events - event types · duration, buffers before and after, minimum notice, booking window, and which team members - routing · round robin, collective or a single host, and the fairness rule - backups · database and configuration, off the machine, with a restore date ### Phase 1 · Running, on your own domain Build: Provision a server, run the application and its database, put a reverse proxy in front for certificates, and create the first account. Set the public URL correctly before anything else, because links are generated from it. Trap: The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking. Done when: The booking page loads over HTTPS with a valid certificate. An account exists and can sign in. The public URL matches the domain, verified by checking a generated link. Restarting the server brings everything back automatically. Do not build yet: No calendars, no team, no bookings. ### Phase 2 · Real calendars, so availability is true Build: Connect the calendar accounts people really use. Choose which calendars are read for busy time and which receives new bookings. Then test with a real event: create something in your calendar and confirm the slot disappears from the booking page. Trap: Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about. Done when: Creating an event in a connected calendar removes the corresponding slot from the public booking page within the refresh interval. A booking made through the page appears in the destination calendar with the right details. Disconnecting and reconnecting an account does not duplicate existing bookings. Do not build yet: No team routing yet. ### Phase 3 · Event types with buffers that reflect reality Build: Create event types with duration, buffers before and after, minimum notice and a booking window. Set working hours per person including time zone. Then book as an outsider and check the offered times against what you would actually accept. Trap: Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive. Done when: A booking page shows no slot within the minimum notice period. Consecutive slots respect the configured buffer. A booker in another time zone sees times correctly converted, verified by changing a browser's zone. Booking outside working hours is not offered. Do not build yet: No team routing yet. ### Phase 4 · Team routing without double booking Build: Add team members and create team event types with round robin or collective assignment. Verify the fairness rule matches what you intended. Then test the concurrency case explicitly: two people booking the last slot at the same moment. Trap: Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone. Done when: A round robin event distributes across members according to the configured rule. A collective event only offers slots where every required member is free. Two simultaneous bookings of the last slot result in exactly one confirmation and one clear refusal. A member marked unavailable is not assigned. Do not build yet: No email verification or backups yet. ### Phase 5 · Email that arrives Build: Configure a transactional provider with a verified sending domain. Test delivery to several different mail providers. Verify calendar invitations attach correctly and that a cancellation actually removes the event from the booker's calendar. Trap: An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs. Done when: A confirmation arrives at three different mail providers and is not in spam. The invitation adds an event to the booker's calendar in two different clients. A cancellation removes the event rather than leaving it. A reminder arrives at the configured interval before the meeting. Do not build yet: Operations come last. ### Phase 6 · Backups, upgrades and the restore drill Build: Nightly database backups off the machine, including the configuration and credentials needed for a working restore. An upgrade procedure that backs up first and reads the release notes. A restore onto a clean server, with a booking made against it. Trap: Restoring the database without the calendar connection credentials leaves a system that looks correct and computes availability from nothing, which is exactly the state that produces double bookings. Back up the configuration alongside the database, and confirm calendar sync works before pointing the domain at a restored instance. Done when: Nightly backups land off the server. A restore onto a clean server produces a working instance where a test booking succeeds and appears in a real calendar. The upgrade procedure has been followed once with a backup taken first. The restore date is recorded. Do not build yet: Nothing after this. ### Out of scope (and why) - Managed hosting and upgrades. Version upgrades, database migrations and security patches are now your responsibility and they arrive regularly. - The connector catalogue for video conferencing, payments and customer systems, each of which is credentials and maintenance. - Support. When a customer cannot book at nine on a Monday, you are the person who finds out why. - Enterprise controls such as single sign-on, audit logs and administrative policy. ### README must contain - That the public URL appears in every link and email and must be right before the first booking - Which calendars are read for busy time, and that missing one produces double bookings - The buffers, notice period and booking window, and why the defaults are too permissive - That simultaneous booking of the last slot was tested explicitly - The backup contents, including configuration, and the date of the last restore
# Architecture · Cal.com Teams ## Stack | Part | Choice | Why | | --- | --- | --- | | Application | Cal.com, self-hosted | the scheduling logic, availability computation and booking pages already exist and are the hard part | | Database | PostgreSQL | bookings are money-adjacent and concurrency-sensitive, which is exactly what it is for | | Calendars | Real provider connections through their APIs | availability computed from a calendar that is not the one you actually use is worse than no availability at all | | Email | A transactional provider with a verified domain | a confirmation that lands in spam is a missed meeting | | Edge | A reverse proxy with automatic certificates | booking pages are public and must be trusted by a stranger's browser | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Application | Availability, booking and the public pages. | Not yours to replace. Upgrading it is the only change you make here. | | Calendars | The truth about who is busy. | Providers differ; the rule that every commitment-bearing calendar is read as busy does not. | | Event types | What may be booked and when. | Configuration. The buffers and notice period are the settings that make the system usable in practice. | | Email | Confirmations, invitations and reminders. | Providers are interchangeable. Verified domain and cross-provider testing survive any change. | | Backups | Database and configuration together. | Destination is replaceable; including the encryption key and calendar credentials is not. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `NEXT_PUBLIC_WEBAPP_URL` · required · The public URL. Appears in every link and email, so it must be correct before the first booking. - `DATABASE_URL` · required, secret · Your PostgreSQL connection string. - `NEXTAUTH_SECRET` · required, secret · Signs sessions. Part of a working restore, so back it up. - `CALENDSO_ENCRYPTION_KEY` · required, secret · Encrypts stored calendar credentials. Losing it means reconnecting every calendar, which is why it belongs in the backup. - `GOOGLE_API_CREDENTIALS` · required, secret · From your calendar provider's developer console, with the redirect URL matching the public URL. - `EMAIL_SERVER` · required, secret · Transactional provider credentials. - `EMAIL_FROM` · required · Must be on the verified sending domain or delivery will suffer. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Cal.com Teams product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Cal.com, self-hosted, PostgreSQL, Real provider connections through their APIs, A transactional provider with a verified domain, A reverse proxy with automatic certificates. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking. - Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about. - Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive. - Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone. - An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs. - Restoring the database without the calendar connection credentials leaves a system that looks correct and computes availability from nothing, which is exactly the state that produces double bookings. Back up the configuration alongside the database, and confirm calendar sync works before pointing the domain at a restored instance.
# Delivery milestones · Cal.com Teams Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Running, on your own domain The application is up, reachable over HTTPS, and an account exists. ### Steps 1. Provision and close the server Non-root user, key-only SSH, firewall allowing SSH and web ports. ```sh ufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable ``` 2. Run the application and database Following the project's own deployment guidance for the version you are using. Files: `docker-compose.yml` 3. Set the public URL and get certificates Before the first booking. Verify by generating a link and reading it. Files: `Caddyfile`, `.env` ### Done when - [ ] The booking page loads over HTTPS with a valid certificate - [ ] An account can sign in - [ ] Generated links use the correct public URL - [ ] A server restart brings everything back ### Watch out - The configured public URL appears in every booking link, confirmation email and calendar invitation. Setting it wrong and correcting it later leaves broken links in emails already sent, and those are the links customers click. Set it correctly before the first booking. ## M2 · Real calendars, so availability is true Your actual commitments block your availability. ### Steps 1. Connect the provider accounts Create the credentials the application expects and complete the authorisation flow for each person. 2. Select busy calendars deliberately Every calendar representing a real commitment. This is the setting that decides whether availability is honest. 3. Test with a real event Create one, check the public page. Do not trust the configuration screen. ### Done when - [ ] A real calendar event removes the slot from the public page - [ ] A booking appears in the destination calendar correctly - [ ] Reconnecting an account creates no duplicates - [ ] Every commitment-bearing calendar is selected as busy ### Watch out - Reading only the destination calendar for busy time is the common misconfiguration, and it means a meeting in your personal calendar does not block a slot. Select every calendar that represents a real commitment, including the ones you would rather not think about. ## M3 · Event types with buffers that reflect reality Slots offered are slots you could actually take. ### Steps 1. Configure duration, buffers and notice All three before publishing. The defaults allow a booking in five minutes with no gap. 2. Set working hours with zones Per person, with the zone explicit. Zones are where scheduling tools embarrass people. 3. Book as an outsider from another zone A private window with a different browser zone. Check the times are what you expect. ### Done when - [ ] No slot is offered within the minimum notice - [ ] Buffers separate consecutive slots - [ ] Another time zone sees correctly converted times - [ ] Slots outside working hours are not offered ### Watch out - Zero buffer and zero notice means someone can book you for the next five minutes, back to back with the previous meeting, and the first time it happens you will disable the whole thing. Set both before publishing the link, because the defaults are permissive. ## M4 · Team routing without double booking A team event assigns fairly and never books two people into the same slot by mistake. ### Steps 1. Add members and team event types Round robin for one-of, collective for all-of. The distinction matters for availability computation. 2. Verify the fairness rule Book several times and check distribution. Fairness rules vary and the default may not be what you assumed. 3. Test simultaneous booking Two requests for the last slot at the same moment. Exactly one must succeed. ```sh ab -n 2 -c 2 -p booking.json -T application/json https://cal.example.com/api/book ``` ### Done when - [ ] Round robin distributes according to the configured rule - [ ] Collective offers only slots where everyone is free - [ ] Simultaneous booking of the last slot yields one confirmation - [ ] An unavailable member is not assigned ### Watch out - Two bookers submitting the same slot within the same second is the case that produces a double booking, and it will not appear in casual testing. Exercise it deliberately with two simultaneous requests, and confirm exactly one succeeds and the other is told the slot has gone. ## M5 · Email that arrives Confirmations, reminders and cancellations reach inboxes rather than spam folders. ### Steps 1. Verify the sending domain The DNS records your provider specifies. Propagation takes hours, so start early. 2. Test across mail providers Three different ones, checking the spam folder in each. 3. Verify invitations and cancellations In two different mail clients. Cancellation is the one people forget to test. ### Done when - [ ] Confirmations arrive at three providers and avoid spam - [ ] Invitations add an event in two different clients - [ ] A cancellation removes the event - [ ] A reminder arrives at the configured interval ### Watch out - An invitation that does not carry a correctly formed calendar attachment leaves the booker with an email and no event, and they will not notice until they miss it. Test the invitation in at least two different mail clients, because handling differs. ## M6 · Backups, upgrades and the restore drill (production only) You can rebuild it, and an upgrade is a planned event rather than a surprise. ### Steps 1. Back up database and configuration Both, dated, off the machine. The calendar credentials are part of a working restore. Files: `scripts/backup.sh` ```sh pg_dump "$DATABASE_URL" | gzip > cal-$(date +%F).sql.gz ``` 2. Restore onto a clean server Then make a real booking and confirm it reaches a calendar. That last step is the actual test. Files: `README.md` 3. Write and follow the upgrade procedure Back up, read breaking changes, upgrade, verify a booking. Follow it once and record the time. Files: `docs/upgrade.md` ### Done when - [ ] Nightly backups land off the server - [ ] A restore produces a working booking that reaches a calendar - [ ] The upgrade procedure has been followed and timed - [ ] The restore date is recorded ### Watch out - Restoring the database without the calendar connection credentials leaves a system that looks correct and computes availability from nothing, which is exactly the state that produces double bookings. Back up the configuration alongside the database, and confirm calendar sync works before pointing the domain at a restored instance.
# Operations · Cal.com Teams ## Backup Nightly database dump plus the environment configuration, both off the machine. The calendar encryption key is part of a working restore, and without it every calendar must be reconnected by hand. ## Restore Fresh server, restore database and configuration, verify calendar sync, then make a real booking and confirm it reaches a calendar. Only then repoint the domain. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Uptime on the booking page, email delivery and bounce rates, and calendar sync failures. A silently failed calendar connection is the condition that produces double bookings, and it does not announce itself. ## Incident checklist For a double booking, check calendar connection health first, then the busy calendar selection. For a customer who cannot book, check the notice period and booking window before assuming a bug. Keep the pre-upgrade backup until a full booking cycle has completed successfully. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A real calendar event is confirmed to remove the slot - [ ] Simultaneous booking of the last slot yields exactly one confirmation - [ ] Confirmations arrive at three providers and avoid spam - [ ] Invitations and cancellations are verified in two mail clients - [ ] A restore produced a working booking that reached a calendar, and is dated ## Launch constraint Do not market omitted Cal.com Teams capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. The public URL. Appears in every link and email, so it must be correct before the first booking.
NEXT_PUBLIC_WEBAPP_URL=https://cal.example.com
# Required · secret. Your PostgreSQL connection string.
DATABASE_URL=postgres://user:pass@db:5432/cal
# Required · secret. Signs sessions. Part of a working restore, so back it up.
NEXTAUTH_SECRET=generate-a-long-random-value
# Required · secret. Encrypts stored calendar credentials. Losing it means reconnecting every calendar, which is why it belongs in the backup.
CALENDSO_ENCRYPTION_KEY=generate-a-long-random-value
# Required · secret. From your calendar provider's developer console, with the redirect URL matching the public URL.
GOOGLE_API_CREDENTIALS={"web":{"client_id":"..."}}
# Required · secret. Transactional provider credentials.
EMAIL_SERVER=smtp://user:pass@smtp.provider.com:587
# Required. Must be on the verified sending domain or delivery will suffer.
EMAIL_FROM=bookings@example.com
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Cal.com Teams because people pay because one double booking or missed invite can cost more than years of subscription fees. The recurring cost buys OAuth refresh, webhooks, timezone databases, race conditions, reminders, deliverability, privacy, and uptime, not just the visible interface.
xmanaged hosting, connectors, upgrades, support, and enterprise controls
xmultiple calendar providers
xteam routing and round-robin
xpayments
xtimezone edge-case maturity
Cal.com Teams pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| individual | $0/user | $0/user | 1 user, unlimited event types/calendars/meetings, email and SMS notifications, 100+ app integrations and payment collection |
| teams | $16/user | $12/user | 1 team; shared availability, round-robin, managed and collective event types |
| organizations | $37.33/user | $28/user | Unlimited sub-teams, organization subdomain, SSO/SCIM and advanced security/admin |
| enterprise | custom | — | Custom organization scale, security, support and procurement terms |
free tier1 user with unlimited event types, connected calendars and meetings; email/SMS notifications and 100+ integrations
billingmonthly + annual (-25%) for hosted Teams/Organizations; Enterprise annual custom
hidden costsHosted team pricing is per user. Self-hosting can remove the hosted subscription but shifts infrastructure, upgrades, backups and social/OAuth app configuration to the customer; some enterprise features require commercial licensing.
verified 2026-08-14 · source ↗
Vibecode Cal.com Teams
Kinda. The core of Cal.com Teams is buildable in a weekend with the prompt on this page, but there are real gaps: managed hosting, connectors, upgrades, support, and enterprise controls, multiple calendar providers. Read the honest list above before committing.
How much does Cal.com Teams cost?
Cal.com Teams costs about $16/month (Teams, checked 2026-08-14), which is $192 per year.
What do I lose by replacing Cal.com Teams?
Honestly: managed hosting, connectors, upgrades, support, and enterprise controls; multiple calendar providers; team routing and round-robin; payments; timezone edge-case maturity. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Cal.com Teams?
Yes: Cal.com (Mature open-source scheduling platform with calendar integrations and booking workflows.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.