Build Cal.com Teams
KINDAreplaces $16/mosaves $192/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- have readyroughly 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 - have readya 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 - have readyroughly 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 - API keyfree 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 - API keyfree 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 - have readyfree
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
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- 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
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
NEXT_PUBLIC_WEBAPP_URL | required | https://cal.example.com | The public URL. Appears in every link and email, so it must be correct before the first booking. |
DATABASE_URLsecret | required | postgres://user:pass@db:5432/cal | Your PostgreSQL connection string. |
NEXTAUTH_SECRETsecret | required | generate-a-long-random-value | Signs sessions. Part of a working restore, so back it up. |
CALENDSO_ENCRYPTION_KEYsecret | required | generate-a-long-random-value | Encrypts stored calendar credentials. Losing it means reconnecting every calendar, which is why it belongs in the backup. |
GOOGLE_API_CREDENTIALSsecret | required | {"web":{"client_id":"..."}} | From your calendar provider's developer console, with the redirect URL matching the public URL. |
EMAIL_SERVERsecret | required | smtp://user:pass@smtp.provider.com:587 | Transactional provider credentials. |
EMAIL_FROM | required | bookings@example.com | Must be on the verified sending domain or delivery will suffer. |
The build, in order
Running, on your own domain
The application is up, reachable over HTTPS, and an account exists.
Non-root user, key-only SSH, firewall allowing SSH and web ports.
terminalufw allow OpenSSH && ufw allow 80,443/tcp && ufw enable
Following the project's own deployment guidance for the version you are using.
Files
docker-compose.ymlBefore the first booking. Verify by generating a link and reading it.
Files
Caddyfile.env
done when · tick each as it passeswatch 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.
Real calendars, so availability is true
Your actual commitments block your availability.
Create the credentials the application expects and complete the authorisation flow for each person.
Every calendar representing a real commitment. This is the setting that decides whether availability is honest.
Create one, check the public page. Do not trust the configuration screen.
done when · tick each as it passeswatch 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.
Event types with buffers that reflect reality
Slots offered are slots you could actually take.
All three before publishing. The defaults allow a booking in five minutes with no gap.
Per person, with the zone explicit. Zones are where scheduling tools embarrass people.
A private window with a different browser zone. Check the times are what you expect.
done when · tick each as it passeswatch 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.
Team routing without double booking
A team event assigns fairly and never books two people into the same slot by mistake.
Round robin for one-of, collective for all-of. The distinction matters for availability computation.
Book several times and check distribution. Fairness rules vary and the default may not be what you assumed.
Two requests for the last slot at the same moment. Exactly one must succeed.
terminalab -n 2 -c 2 -p booking.json -T application/json https://cal.example.com/api/book
done when · tick each as it passeswatch 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.
Email that arrives
Confirmations, reminders and cancellations reach inboxes rather than spam folders.
The DNS records your provider specifies. Propagation takes hours, so start early.
Three different ones, checking the spam folder in each.
In two different mail clients. Cancellation is the one people forget to test.
done when · tick each as it passeswatch 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.
Backups, upgrades and the restore drillproduct builder
You can rebuild it, and an upgrade is a planned event rather than a surprise.
Both, dated, off the machine. The calendar credentials are part of a working restore.
Files
scripts/backup.shterminalpg_dump "$DATABASE_URL" | gzip > cal-$(date +%F).sql.gz
Then make a real booking and confirm it reaches a calendar. That last step is the actual test.
Files
README.mdBack up, read breaking changes, upgrade, verify a booking. Follow it once and record the time.
Files
docs/upgrade.md
done when · tick each as it passeswatch 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.
That is the whole plan for Cal.com Teams. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- 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
- 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
Need the files? The project pack on the verdict page hands your agent the whole brief · more scheduling.