Operations
How Whereabot runs in production, and the traps we already hit. The step-by-step runbook is infra/README.md. The code and workflows are the source of truth for every value. This page names the files and explains why they are shaped the way they are.
Delivery is GitOps
Every change reaches dev and prod through a workflow on main. Nobody deploys from a laptop except as break-glass.
| What | Workflow | Trigger |
|---|---|---|
| Tests, lint, line cap | ci.yml | every PR and push |
| Database migrations | migrations.yml | push to main: dev, then production |
| Relay + Supabase config | deploy.yml | push to main: test, image, dev, then production after approval |
| Landing page and docs | site-cloudflare-deploy.yml | push to main |
| iOS app to TestFlight | ios-testflight-release.yml | tag app-vX.Y.Z |
Migrations and deploys are separate workflows with no ordering between them. Keep that in mind for the next section.
Config that needs a migration ships in two pushes
The Supabase auth config (for example an auth hook) is applied by deploy.yml. The Postgres function it points at is created by migrations.yml. If both land in one push, the config can reach an environment before the function exists, and the apply fails.
- First push: the migration only.
- Check that
migrations.ymlapplied it in dev and production. - Second push: the config in
supabase.tfthat uses it.
New migration files must sort after every migration already applied. CI refuses a file with an older timestamp. See adding a migration.
Optional secrets stay behind a flag
Cloud Run refuses a revision that mounts a secret with no version. The APNs key secret is created empty, and a human adds the key later. So the relay mounts it only when apns_enabled is true. See push.tf and cloud_run.tf. Turn the flag on per environment only after the secret has a version.
Domains, DNS and certificates
The live hosts are listed once, in infra/README.md, Live URLs.
- Relay host. A Cloud Run domain mapping (
cloud_run_domain_mapping.tf). Google lets only a verified owner of the domain create a mapping, and the CI service account is not one. A human domain owner creates the mapping once, then imports it into the prod state. The resource ignorescertificate_mode, because it reads back empty after import and would force a replacement. - DNS records. In
infra/dns/, applied by a human. The relay CNAME must be DNS only (not proxied). Google issues the certificate and must see the CNAME. - Verify, do not trust the dashboard. A record saved in the Cloudflare dashboard did not persist once. After any DNS change, query a public resolver for the record, then wait until the relay answers over HTTPS. The certificate can take a while after DNS is correct.
- Site host. The site workflow attaches the custom domain to the Worker and owns the apex record.
Release the iOS app
The normal path is a tag. The steps are in infra/README.md, Release the iOS app.
scripts/release-app-version.shbumps every version file. Merge that, then push the tag it prints.- Fallback when CI cannot run: build and upload from a Mac with the same two steps as the workflow (a Tauri iOS build with the App Store Connect export method, then an upload with
altool). Use the same API key and environment values the workflow reads. - TestFlight rejects a version it already has. Bump and tag again.
Steps that need a human
Some steps change shared accounts. An agent prepares them, but a human does them or explicitly approves them:
- DNS changes in the Cloudflare account that owns the zone.
- Writing secret values (GitHub environment secrets, Secret Manager versions).
- IAM grants and domain ownership in Google Search Console.
- Approving the
productionenvironment gate.
After a human step, verify the result (resolver query, /health, workflow run) before you continue.