Skip to content

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.

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.

Terminal window
gcloud auth login
gcloud config set project <your-project>
gcloud auth application-default login # Terraform uses ADC

You need roles/owner (or equivalent) on the project for the first run.

Terminal window
cd cavs-node/terraform
cp terraform.tfvars.example terraform.tfvars

Required 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
Terminal window
cd cavs-node/terraform
./deploy.sh

Four phases:

  1. Bootstrap — targeted apply of the required APIs and Artifact Registry.

  2. Buildgcloud builds submit builds the images via cloudbuild.yaml, with cavs-node/ as the build source. One Go Dockerfile parameterized by --build-arg SERVICE=api|worker; the web Dockerfile bakes the public VITE_FIREBASE_* build args. cavs-node/.gcloudignore keeps the upload small.

  3. Apply — the full Terraform apply.

  4. Wire URLs — captures the live service URLs into urls.auto.tfvars and re-applies. (A Cloud Run service can’t reference its own URL, and api ↔ web would cycle.)

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.

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.
Terminal window
curl -sS https://your-domain/readyz # → ready
curl -sS https://your-domain/api/v1/plans # → the plan catalogue
  1. All three services report Ready = True.
  2. GET /readyz returns 200 — this proves database connectivity end to end.
  3. GET /api/v1/plans returns the catalogue.
  4. The dashboard loads and sign-in works on the canonical origin.
  5. An invitation email arrives with a link on the canonical origin.
  6. A test push registers: the generation bumps and objects appear.
  7. Metrics are arriving in your monitoring backend.
  8. The billing webhook endpoint is registered and reachable.
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

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 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.