Deploying
The reference deployment targets Google Cloud Run via Terraform. Nothing in the platform requires GCP — the services are plain containers — but this is the path that is actually in production.
What gets created
Section titled “What gets created”| Component | Resource |
|---|---|
cavs-api |
Cloud Run service, public (it does its own Firebase auth, so IAM invoker is allUsers) |
cavs-worker |
Cloud Run service, internal-only, min = max = 1, cpu_idle = false — an always-on Pub/Sub puller |
cavs-web |
Cloud Run service, public nginx serving the SPA and reverse-proxying /api |
| Events | Pub/Sub topic cavs.events + subscription cavs.worker |
| Secrets | Secret Manager — one secret per sensitive variable |
| Images | Artifact Registry repository cavs |
External, not provisioned by Terraform: MongoDB (Atlas) and object storage (Cloudflare R2 or any S3). Their credentials are stored as secrets.
Prerequisites
Section titled “Prerequisites”gcloud auth logingcloud config set project <your-project>gcloud auth application-default login # Terraform uses ADCYou need roles/owner (or equivalent) on the project for the first run.
Configure
Section titled “Configure”cd cavs-node/terraformcp terraform.tfvars.example terraform.tfvarsRequired before a deploy can succeed:
| Variable | Notes |
|---|---|
secrets.MONGODB_URI |
Your Atlas connection string |
secrets.STORAGE_ENCRYPTION_KEY |
openssl rand -base64 32 |
secrets.SESSION_TOKEN_SECRET |
openssl rand -base64 32 |
s3_account_id |
Cloudflare account id |
secrets.S3_ACCESS_KEY / S3_SECRET_KEY |
R2 API token, scoped to the bucket |
firebase_project_id |
Must match the web build |
app_base_url |
The origin users sign in on |
Deploy
Section titled “Deploy”cd cavs-node/terraform./deploy.shFour phases:
-
Bootstrap — targeted apply of the required APIs and Artifact Registry.
-
Build —
gcloud builds submitbuilds the images viacloudbuild.yaml, withcavs-node/as the build source. One Go Dockerfile parameterized by--build-arg SERVICE=api|worker; the web Dockerfile bakes the publicVITE_FIREBASE_*build args.cavs-node/.gcloudignorekeeps the upload small. -
Apply — the full Terraform apply.
-
Wire URLs — captures the live service URLs into
urls.auto.tfvarsand re-applies. (A Cloud Run service can’t reference its own URL, and api ↔ web would cycle.)
Custom domain
Section titled “Custom domain”Map your domain to the cavs-web service (done outside Terraform, in the Cloud Run
console or with gcloud). The web tier reverse-proxies /api to the API service, so the
dashboard and API share one origin.
Then set app_base_url to that origin.
Health checks
Section titled “Health checks”| Check | From | Notes |
|---|---|---|
/readyz |
anywhere | Pings MongoDB. Use this externally. |
/healthz |
container probes only | Google’s front end reserves this path and answers it itself, so an external request returns a GFE 404 that says nothing about your service. Cloud Run’s own probes hit the container directly, so /healthz probes still work. |
curl -sS https://your-domain/readyz # → readycurl -sS https://your-domain/api/v1/plans # → the plan cataloguePost-deploy verification
Section titled “Post-deploy verification”- All three services report
Ready = True. GET /readyzreturns 200 — this proves database connectivity end to end.GET /api/v1/plansreturns the catalogue.- The dashboard loads and sign-in works on the canonical origin.
- An invitation email arrives with a link on the canonical origin.
- A test push registers: the generation bumps and objects appear.
- Metrics are arriving in your monitoring backend.
- The billing webhook endpoint is registered and reachable.
Known deployment gotchas
Section titled “Known deployment gotchas”| Symptom | Cause |
|---|---|
invalid_rapt from Terraform |
Missing gcloud auth application-default login |
PERMISSION_DENIED from gcloud builds submit right after enabling the API |
IAM propagation lag. Retry. |
External /healthz returns 404 |
Google’s front end reserves the path. Use /readyz. |
cavs-api container won’t start |
S3/R2 not configured — the API refuses to boot without it |
| Secret Manager rejects a secret | Empty-string value. Omit the key. |
| Worker revision never Ready | No HTTP health server on $PORT |
Invitation links point at run.app |
app_base_url overwritten by urls.auto.tfvars |
| Docker build fails on cache mounts | cloudbuild.yaml needs DOCKER_BUILDKIT=1 per step |
| A stale invitation looks broken after a deploy | Check its created_at — it may predate the fix |
Other platforms
Section titled “Other platforms”Nothing is Cloud Run-specific beyond the Terraform module. The services are plain containers:
| Requirement | Notes |
|---|---|
cavs-api |
HTTP, public, horizontally scalable, stateless |
cavs-worker |
Always-on, one instance is enough to start; needs an HTTP health surface if your platform probes |
cavs-web |
Static SPA + a reverse proxy to the API on the same origin |
| MongoDB | Replica set. Managed (Atlas) recommended. |
| Object storage | Any S3-compatible endpoint with Range GET and multipart |
| Pub/Sub | Google Cloud Pub/Sub, or the emulator |
Kubernetes, ECS or plain Docker Compose all work. Keep the worker singleton-ish until you’ve read Scaling — every stage is idempotent, so multiple workers are safe, but that’s a capacity decision, not a correctness one.
Terraform state
Section titled “Terraform state”Terraform state and terraform.tfvars contain secrets. Both are gitignored in the
reference setup. Use a remote backend with encryption and locking for anything
beyond a single operator.
- Configuration reference — every variable and the production checklist.
- Observability — dashboards and alerts.
- Scaling — where the limits are.

