← All reference docs

Deploying the Oath registry (GitHub Actions → GCP)

Before any cutover, read docs/deploy-delta.md. The live registry is many commits behind, and the delta now includes journal-format and verification- semantics changes rather than only new features. That document reviews the cutover as a whole and states what it does NOT authorise.

This ships the #14 registry as a single-instance v1: oath serve (HTTP MCP) on Cloud Run over a gcsfuse-mounted store bucket, with the proof worker as a scheduled Cloud Run Job. Auth to GCP is keyless (Workload Identity Federation) — no service-account key ever touches GitHub.

What gets deployed

| Piece | Resource | Notes | |---|---|---| | API | Cloud Run service (oath serve) | Bearer-token auth; single instance (see the constraint below) | | Store | GCS bucket, gcsfuse-mounted at /store | Whole filesystem store: objects, meta, names.json, log.jsonl, proofq/. Versioned. | | Proof worker | Cloud Run Job (oath prove-worker) | Ticked by Cloud Scheduler (default every 5 min); binds require_proven names once proven | | Tokens | Secret Manager | Terraform mints an initial admin token and outputs it | | Images | Artifact Registry | Built + pushed by CI | | Database | Cloud SQL (Postgres) | Off by default — v1 needs no DB; enable with the Postgres driver (#14) |

The v1 constraint, up front

The store is the filesystem store over gcsfuse. Its journal and name index have a single safe writer, so the serve service is pinned to one instance and the worker runs briefly out of band. This is a real, persistent, low-traffic registry — not a multi-instance one. Multi-instance needs the Postgres name-index

One-time setup (do this async)

  1. Create a GCP project and link billing.

  2. Bootstrap — creates the Terraform state bucket, Artifact Registry, the keyless WIF trust, and the deployer service account. Run locally with an account that can administer the project:

    gcloud auth login
    PROJECT_ID=your-project GITHUB_REPO=miclip/oath-lang \
      ./terraform/bootstrap/bootstrap.sh
    # optional: REGION=us-central1 PREFIX=oath
    

    It prints the four secret values to set next. (Idempotent — safe to re-run.)

  3. Set GitHub repo variables (Settings → Secrets and variables → Actions → Variables tab). Keyless auth means there are no secrets to store — these are all identifiers, not credentials (WIF security is the provider's repo-bound attribute condition + IAM, not the secrecy of these strings):

    | Variable | From bootstrap output | |---|---| | GCP_PROJECT_ID | your project id | | GCP_DEPLOYER_SA | oath-deployer@…iam.gserviceaccount.com | | GCP_WIF_PROVIDER | projects/NUMBER/locations/global/workloadIdentityPools/…/providers/… | | TF_STATE_BUCKET | PROJECT-oath-tfstate | | GCP_REGION | optional; defaults to us-central1 |

Deploy

CI builds the image (kernel + pinned Z3 4.16.0), pushes it to Artifact Registry, and terraform applys. The run summary prints the API URL. Grab the admin token:

# locally, with the state backend configured, or from Cloud Console:
terraform -chdir=terraform output -raw admin_token

Verify it's live

API=https://…run.app         # from the workflow summary
TOKEN=…                      # terraform output -raw admin_token

# MCP initialize over HTTP
curl -s "$API/mcp" -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'

Put a definition, then watch the worker prove a require_proven name on the next tick (oath log shows pending → prove → put/accepted).

Custom domain (e.g. registry.oath-lang.org)

The run.app URL works as-is; this maps a vanity domain onto it with a Google-managed TLS cert. One-time:

  1. Verify the parent domain for the project. In Google Search Console, verify ownership of oath-lang.org (add the TXT record it gives you at your DNS host), then in the GCP console add the project's deployer as a verified owner. Without this, the domain mapping fails to create.

  2. Point CI at the domain — set a repo variable:

    gh variable set REGISTRY_DOMAIN --body registry.oath-lang.org
    
  3. Re-run the deploy. Terraform creates the mapping and the run summary prints the DNS record to add (a CNAME to ghs.googlehosted.com for a subdomain). Also available any time:

    terraform -chdir=terraform output -json custom_domain_dns
    
  4. Add that record at whatever hosts oath-lang.org DNS (your registrar or Vercel's DNS — the site itself stays on Vercel; this is just a new subdomain record). The cert provisions in ~15 min, then https://registry.oath-lang.org is live.

Cloud Run managed domain mappings are regional and simple; for multi-region or heavier routing, the scale-up path is a global HTTPS load balancer with a serverless NEG (more Terraform, not needed for v1).

Turning on signed verdicts (optional)

By default the worker records proofs unsigned (still fully re-verifiable). To make the proven badge an authenticated "the registry re-proved this" (docs/registry-auth.md):

oath keygen --out registry
gcloud secrets create oath-registry-key --data-file=registry.key --project=$PROJECT_ID
gcloud secrets add-iam-policy-binding oath-registry-key \
  --member="serviceAccount:oath-server@$PROJECT_ID.iam.gserviceaccount.com" \
  --role=roles/secretmanager.secretAccessor --project=$PROJECT_ID

Then mount it into the worker Job and set OATH_KEY to the mount path (a small addition to worker.tf; the entrypoint signs automatically when OATH_KEY is present). Keep the .key out of git.

Cost note

v1 is cheap: Cloud Run scales the serve instance and the worker Job to near-zero between requests/ticks, the store bucket is pennies, and the database is off. The main always-on cost is the single min-instance serve container. Drop min_instance_count to 0 in main.tf if cold starts are acceptable and you want it cheaper still (the store persists in GCS regardless).

Tear down

terraform destroy (the store bucket and — if enabled — the SQL instance carry prevent_destroy; remove those blocks first if you really mean it). The store bucket is the audit trail; back it up before destroying.


Rendered verbatim from docs/deploy.md in the repository. The markdown is the single source; this page is a copy checked for drift in CI, so what you read here is what an implementer reads.