calcom 1.0.0 — Cal.com scheduling - #528
Open
jacobecox wants to merge 8 commits into
Open
Conversation
Self-hosted Cal.com scheduling and booking pages on the existing `postgres` (or `postgres-highly-available`) template, pinned to calcom/cal.com:v6.2.0. - App tier is `standard` and volumeless; all state is in Postgres. - A second small workload drives Cal.com's seven scheduled endpoints, which nothing inside the container calls — without it the task queue, calendar sync and credential refresh silently never run while every health surface is green. - Private by default: /auth/setup grants instance-admin to the first caller and self-service signup cannot be disabled by env (NEXT_PUBLIC_*, inlined at build), so the README's first run is install private -> cpln port-forward -> claim admin -> then decide about public access. - nextAuthSecret, encryptionKey, cronSecret and cronApiKey are one required prerequisite dictionary secret, never values. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…firewall Two review findings, no blockers. 1. First run step 4 told the user to run an upgrade against a local `./calcom/versions/1.0.0` path, which does not exist for anyone who installed from the marketplace UI, and it omitted --dependency-update. The step is necessary — it is the only way to publish the app — so it stays, but it is now expressed as the values change it actually is: set publicAccess.enabled: true and upgrade the release, with the UI path and the CLI path both named. This follows the phrasing umami and open-webui already use for the same private- install-then-publish flow. Flags verified against `cpln helm upgrade --help`, not memory. 2. A user setting postgres.internalAccess.type to workload-list without listing this release's own workload cuts Cal.com off from its own database, and it fails as a boot hang (Prisma's migrate step blocks) rather than an error. A parent cannot inject into a subchart's list, so the chart now hard-fails at render, naming the exact link to add — mirroring the guard wordpress already has for its mariadb pass-through. type: none is refused outright. postgres-highly-available exposes no internalAccess knob, so that path needs no guard. Verified: bare `helm template --set global.cpln.gvc=test-gvc` renders byte-identical to before; both guard arms proven (bad list and type: none each fail with the new message, a list including the app still renders); lint carries the same two pre-existing R1 warnings about the abridged postgres README block and no new ones. Briefing updated with the new guard.
The policy granted four actions; the `postgres` template's own README -- the same pass-through, the same backup image -- specifies ten. The three missing multipart actions (AbortMultipartUpload, ListBucketMultipartUploads, ListMultipartUploadParts) are the ones a large dump needs, so the policy works until a user's database grows past the multipart threshold and then fails. Failing only once there is real data to lose is the worst shape for a backup defect. Also adds GetBucketLocation and the two versioning actions, matching the authoritative list. Verified the block still parses as JSON. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t the app
Two defects found in live testing, both of which read GREEN on every status
surface. Test-driven fixes on the unmerged 1.0.0, per CLAUDE.md.
1. BLOCKER — a fresh install could serve against an EMPTY database.
The image's scripts/start.sh gates the boot on scripts/wait-for-it.sh, which at
v6.2.0 is the eficode POSIX-sh variant testing readiness with `nc -w 1 -z`. The
Control Plane mesh sidecar completes the TCP handshake for anything, so the wait
returned success instantly in every case — proven in-container with three
negative controls that all had to fail and did not: a dead port (9999), a
nonexistent hostname, and the real port, all "up" in ~0 s. start.sh also runs
`set -x` without `set -e`, so the `prisma migrate deploy` that then failed did
not stop the boot; Cal.com served HTTP with ready: true, 200 on /api/version and
0 tables in information_schema. Intermittent by nature, and a fresh volumeset —
the first-install case a new user hits — is what loses the race.
Fix: mount our own /cpln/start.sh (opaque secret) and run it with /bin/bash,
replacing the image entrypoint. It waits by sending a Postgres SSLRequest and
requiring the one-byte S/N reply only a live backend produces, three times
consecutively, for up to 600 s; then runs the migration under `set -e` so a
failure crash-loops instead of serving. Follows keycloak 1.4.0 (protocol bytes,
not a TCP connect) and grafana-multi-location 2.0.0 (consecutive successes).
Verified against calcom/cal.com:v6.2.0 itself, not by inspection:
- /bin/bash 5.2.15 present (/bin/sh is dash); /dev/tcp works; NO psql and NO
pg_isready in the image (command -v exits 127); curl and python3 present.
- gate: live Postgres -> READY; dead port -> NOT-READY; nonexistent host ->
NOT-READY; accepts-but-never-replies (the sidecar's shape, and the arm that
actually matters) -> NOT-READY, bounded at 6 s by `read -t 5`.
- full boot with the new script: gate released after 3 consecutive OKs,
migration applied 125 tables, `Ready in 1005ms`, /api/version -> 200.
- negative control A: hanging server -> `[calcom] FATAL: ... did not answer
the Postgres protocol`, container exit 1.
- negative control B: gate passes, bad credentials -> `prisma migrate deploy`
exits 1, container exit 1, `yarn start` never reached.
/api/version stays the probe: v6.2.0 has no /api/health or equivalent under
apps/web (checked against the full tag tree) and /auth/setup answers 307 against
an empty database. The invariant is now enforced at boot instead, so "HTTP is
listening implies migrations applied" is true by construction — it was an
assumption before, and it was false. Liveness initialDelaySeconds 300 -> 780 so
a slow first install is not killed mid-wait, which would hide the diagnostic.
DATABASE_HOST is dropped: its only consumer in the whole image is start.sh
(verified by grep), which no longer runs.
2. A multi-location GVC silently split Cal.com into N independent instances.
A workload runs in every location its GVC has, and the chart declared no
localOptions: a default install into a 3-location GVC produced three app
replicas each bound by service DNS to its own local Postgres — three separate
databases sharing one NEXTAUTH_SECRET, so a session validated against a database
that did not contain the user. A table created in aws-us-east-1 did not exist in
the other two.
Fix: the CLAUDE.md confinement pattern — defaultOptions.minScale/maxScale 0 with
a single localOptions entry for a new top-level `location` value (default
aws-us-east-1), on both the app and the cron caller. Precedent: airflow 2.0.0.
An unasked-for location now starts nothing by construction. The app also reads
its own GVC at boot (new targetKind: gvc policy, scoped to the one install GVC)
and warns when other locations exist, plus refuses to start if it ever finds
itself outside `location`.
NOT fully solved, and said so in the README, values and briefing: the bundled
postgres / postgres-highly-available subcharts cannot be pinned — a parent
cannot template a subchart's values and neither exposes a location knob (the
same gap plane, docmost and metabase carry). In a multi-location GVC an empty
database still starts in every location with its own volumeset: idle, never
read, and billed. Data-splitting is fixed; cost sprawl is not.
Verified: bare `helm template --set global.cpln.gvc=test-gvc` renders clean;
multi-location and HA shapes render as intended; empty/plural `location` fail at
render with actionable messages; template-lint --offline shows no FAIL (two
pre-existing R1 WARNs on the how-to-switch-stores example blocks).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit raised it to 780 so the liveness deadline would sit above the startup script's 600s database wait. The platform caps the field at 600 and rejects the apply with a 400: "spec.containers[0].livenessProbe.initialDelaySeconds" must be less than or equal to 600 That is apply-time validation, so helm template renders cleanly and the chart is simply uninstallable -- measured on a default install, which was refused after 5.9s with the app workload never created. The intent survives the cap: the first restart cannot occur before initialDelay + failureThreshold * periodSeconds = 600 + 10*30 = 900s, which already clears the 600s wait budget. Round 2 ran its whole matrix against a scratch copy with exactly this one line changed, so everything it measured applies to this commit.
validateLocation checked empty and is-string but never the string's shape, so '//location/aws-us-east-1' and a comma-separated list were both accepted by Helm AND by the API, storing a placement that matches no real location: nothing runs anywhere, every location reports 'deactivated because maxScale is set to 0', and the logs are silent. Found on documenso in round 2 (measured over 426s on a real GVC); calcom had the identical gap. The link form is what spec.staticPlacement.locationLinks actually contains, so it is the natural copy-paste.
…not liveness, kills the container Round 2 found the startup gate's 600 s wait outliving the container: the platform terminated it at ~515 s having reached attempt 104/120, with `Health Check Failed` and a signal, and `|= "FATAL"` returned 0 lines. The operator saw a generic probe failure where the chart had written a sentence naming the database, and a database slower than ~515 s could never be waited out on a single attempt. The chart's own comment asserted that readiness exhausting its threshold during a slow database wait is benign. That is false here, and it is the assumption the 600 s budget rested on — only liveness had been re-budgeted. Measured on test-gvc with four throwaway busybox workloads whose readiness could never pass (port declared, nothing listening) and NO liveness probe: readiness deadline container start -> restart 5 s (0/5/1) 237.6 s, 240.0 s, 243.4 s 330 s (30/15/20) 563.2 s, 555.1 s 660 s (60/30/20) 1043.7 s no probes at all never restarted (and ready:true with nothing listening) So it is READINESS, not a platform "must become ready within N" timer (the no-probe control rules that out) and not liveness (none of them had one). The kill always lands after readiness's own deadline and moves with it. The grace on top is NOT a clean constant — 233 s at the 5 s and 330 s deadlines, 384 s at 660 s, and ~185 s on calcom itself at the same 330 s deadline — so the deadline, which is the floor, is the number to design against. Fix: the wait budget drops from 600 s to 240 s (MAX_ATTEMPTS 120 -> 48), which fires inside calcom's 330 s readiness deadline even after the GVC read's worst-case ~25 s. Readiness is deliberately left tight rather than widened, because failureThreshold x periodSeconds is also how long a sick replica stays in rotation. Nothing is lost by the shorter wait: the gate re-runs in full on every restart, so a slower database is still waited out across cycles — each one now saying why. Verified live on test-gvc against a database pinned to a nonexistent image: 01:19:56.090 [calcom] FATAL: PostgreSQL at test-cal4-postgres.test-gvc.cpln .local:5432 did not answer the Postgres protocol within 240s. Refusing to start: ... with `restarts.exitCode: 1` — the script's own exit, not the platform's 143. The false comment is corrected in workload.yaml and the briefing; README and briefing carry the 240 s number and the fact that the wait restarts each cycle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ssage quote Round 3 measured what an extra GVC location actually costs: not just an idle volumeset but a LIVE PostgreSQL -- ready 1/1, minCpu 250m, minMemory 512Mi, plus a bound 10GiB EBS volume, in every extra location. The README said 'never read and still billed' without a number; now it carries one in both places it appears. Also: the platform's deactivation message ends with a period, and the chart comment quoted it without one. Worth exactness because the message text IS the signal -- desiredScale is absent on a deactivated location, not present-and-0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cal.com scheduling — Next.js standalone, bundled PostgreSQL (or
postgresHA), a separate cron tier, private by default.Testing
Three rounds across
test-gvcand the three-locationtest-gvc-2. Round 1 found two defects, both now closed and measured closed rather than reasoned closed.Round 1, blocker: a default install could serve an EMPTY database
Cal.com's own
scripts/wait-for-it.shat v6.2.0 is the eficode POSIX-sh variant that probes withnc -w 1 -z. The mesh sidecar completes that handshake for anything, so the wait never waited. Three controls: a dead port, a nonexistent hostname and the real port all reported "up" in ~0 s.start.shrunsset -xbut notset -e, so the failedprisma migrate deploydid not stop the boot, and the readiness probe hits/api/version— served frompackage.json, touching no database. Net result:ready: true, HTTP 200, andinformation_schema.tablesreturning 0.Fixed with a chart-authored entrypoint that speaks the Postgres SSLRequest and requires the one-byte
S/Nreply three consecutive times, then migrates underset -e. Verified on a fresh volumeset: the gate held 50 s across 10 failing probes, then 588 migrations, and the poller found no sample where the app was ready with the schema absent.Patterns copied from
keycloak1.4.0 (protocol bytes) andgrafana-multi-location2.0.0 (consecutive successes), which already solve this correctly.Round 1, second defect: a multi-location GVC split Cal.com into N independent instances
Three app replicas, three separate databases, a table created in one location absent from the other two, every status surface green.
Fixed with
defaultOptions.minScale/maxScale: 0plus alocalOptionsentry and a requiredlocationvalue. Round 3 proved it on a real three-location GVC: both unlisted locations report verbatimThis workload location is deactivated because maxScale is set to 0.on both calcom workloads, withreplica getreturning[]— while the unpinned bundled Postgres returned a live replica in those same locations at the same moment, as the positive control.aws-us-east-1holds 125 tables; the other two hold 0, with the database present but unmigrated, so the probe demonstrably reached the right place.Two findings that outlived this template
initialDelaySecondsis capped at 600. A fix commit set 780 and made the chart uninstallable — a 400 at apply, invisible tohelm template. Now in CLAUDE.md's Platform API limits table.initialDelaySeconds + failureThreshold × periodSecondsand moves with it. The wait budget was shortened to 240 s rather than widening readiness — that product is also how long a sick replica stays in rotation.Known and stated honestly
The bundled
postgressubchart cannot be pinned by a parent, so each extra GVC location runs a live idle PostgreSQL —minCpu 250m,minMemory 512Miand a bound 10 GiB volume. Data-splitting is fixed; cost sprawl is not, and the README, values and briefing all say so with the numbers.render-vs-storedis 0 for calcom's own resources,localOptionsincluded (stored verbatim). The 9 remaining differences are inside the vendoredpostgres3.4.1 workload — a partialdefaultOptionsblock inherited by every template vendoring it, and not this chart's to fix.🤖 Generated with Claude Code