Live rollout checkpoint

As of 31 July 2026, the two production substrates have completed every setup stage, and neon-law-stg is a created but unprovisioned project:

  • Both production GKE Autopilot clusters are RUNNING in us-west4, and no deployment reads Postgres: ENG-22 moved the store to SurrealDB, /health pings SurrealDB, and ops gcp setup provisions no Cloud SQL instance, so a deleted one is not recreated on the next run. An operator must export the two legacy production Postgres 15 instances to each deployment's own exports bucket and then delete them. Nothing has ever archived those instances — the nightly archive lane covers the SurrealDB tables only — so the export is what makes the deletion reversible, and it is not optional.
  • neon-law-stg and ghcr exist in the neonlaw.com organization and are linked to billing, but neither has been through its provisioner yet. Run ops gcp hub setup against the hub first, then ops gcp setup against staging — the environment provisioner grants against the hub repository, so the hub must exist before any environment names it.
  • Each provisioned row has five private assets, documents, exports, logs, and applications buckets.
  • On those rows, both the deployment runtime identity and the active operator have bucket-scoped roles/storage.objectAdmin; none of the buckets grants allUsers.

The five-bucket list is the current checkpoint, not the target topology. Issue #1103 coordinates the migration to exactly one private object-storage bucket per deployment and never one bucket per Project. Project growth adds rows and logical key space, with lanes such as {project-code}/documents/ and {project-code}/exports/ inside the deployment bucket; it does not create cloud buckets. Marketing bytes remain available through the same-origin /assets/* application route while the bucket itself stays private and does not grant allUsers. Until that atomic migration lands, setup reconciles the five existing bucket resources recorded above.

New clusters are created with --enable-fleet; the subsequent idempotent reconciliation can therefore report Changing existing fleet membership is not supported. Navigator treats only that exact response as already reconciled and continues; unrelated Fleet errors still stop setup.

Infrastructure is not deployment. The two production clusters currently have no application namespace, Deployment, Service, Gateway, or HTTPRoute, so their public hosts do not yet answer TLS. An operator must ship one immutable release to Neon production and Neon Law production, then prove /readyz, /version, certificate readiness, Restate registration, and the browser surface before posting the #navigator handoff.

For a prefix <name>, set NAVIGATOR_GKE_CLUSTER_NAME=<name>, NAVIGATOR_GKE_CONTEXT=gke_<project>_<region>_<name>, NAVIGATOR_K8S_NAMESPACE=<name>, NAVIGATOR_VPC_NAME=<name>-vpc, NAVIGATOR_SUBNETWORK_NAME=<name>-subnet, NAVIGATOR_GATEWAY_IP_NAME=<name>-gateway-ip, NAVIGATOR_ASSETS_BUCKET=<name>-assets, NAVIGATOR_DOCUMENTS_BUCKET=<name>-documents, NAVIGATOR_EXPORTS_BUCKET=<name>-exports, NAVIGATOR_LOGS_BUCKET=<name>-logs, NAVIGATOR_APPLICATIONS_BUCKET=<name>-applications, NAVIGATOR_GCP_SERVICE_ACCOUNT_ID=<name>-web, NAVIGATOR_DRIVE_GCP_SERVICE_ACCOUNT_ID=<name>-drive, and NAVIGATOR_WEB_SECRET_NAME=<name>-web-secrets. Bucket names are global; add one stable organization prefix if a short bucket name is already taken.

Also set NAVIGATOR_GCP_PROJECT_ID, NAVIGATOR_GCP_LOCATION, NAVIGATOR_PUBLIC_HOST, NAVIGATOR_WORKFLOWS_HOST, NAV_BASE_URL=https://$NAVIGATOR_PUBLIC_HOST, NAVIGATOR_WORKFLOWS_URL=https://$NAVIGATOR_WORKFLOWS_HOST/, NAVIGATOR_WEB_IMAGE, and NAVIGATOR_ASSET_BASE_URL=$NAV_BASE_URL/assets in that deployment's deployments/<name>/config.toml. Set that row's own NAVIGATOR_SURREAL_* coordinates and credentials in its secrets.enc.yaml.

Keep the mail rail complete in every deployment's secrets.enc.yaml: SENDGRID_API_KEY, SENDGRID_FROM_EMAIL, SENDGRID_INBOUND_SECRET, SENDGRID_EVENTS_SECRET, and SENDGRID_EVENTS_PUBLIC_KEY; staging uses non-production SendGrid credentials and production uses live credentials authenticated for its domain.

Provision each row with the same region-agnostic command — one run per deployments/ directory, its coordinates exported from that directory's config.toml (populate the directory first; the config is the source of every coordinate the provisioner reads, so a row is provisioned only once its directory exists). Each run creates five buckets and one Autopilot cluster; re-running reconciles only that row:


set -a; eval "$(grep ' = "' deployments/neon-law-stg/config.toml | sed 's/ = /=/')"; set +a
navigator ops gcp setup --dry-run

Check every printed project, region, resource prefix, and the two API batches. Provision the registry hub once before the runtime stacks; it runs no workloads:


navigator ops gcp hub setup \
  --project-id ghcr --region us-west4 --dry-run
navigator ops gcp hub setup \
  --project-id ghcr --region us-west4

Then apply the already-reviewed plans one deployment at a time. Keep staging first, and do not begin production until its command finishes:


set -a; eval "$(grep ' = "' deployments/neon-law-stg/config.toml | sed 's/ = /=/')"; set +a
navigator ops gcp setup

That is the live resource boundary, per deployment: five buckets, one VPC/subnet pair, one reserved gateway address, one Autopilot cluster, one KMS key, and two deployment service accounts. The command also enables the twenty-two required APIs in that runtime project. A completed command is not yet a deployed website; ops ship, DNS, and the browser checks later in this workshop remain required.

Setup provisions no database and generates no credential, so there is nothing printed to record. The store's own credentials go into deployments/<name>/secrets.enc.yaml (sops set — the plaintext never touches disk), then navigator ops secrets apply --deployment <name>; rotation happens at the provider first, per docs/deployment-secrets.md.

Resolve every reserved address and record the public value as NAVIGATOR_GATEWAY_IP in the matching config.toml — an IP is a public coordinate, so it is edited and committed like any other:


for name in $(ls deployments); do
  set -a; eval "$(grep ' = "' "deployments/${name}/config.toml" | sed 's/ = /=/')"; set +a
  echo "${name}:"
  gcloud compute addresses describe "$NAVIGATOR_GATEWAY_IP_NAME" \
    --global --project "$NAVIGATOR_GCP_PROJECT_ID" --format 'value(address)'
done

Populate the matching local Kubernetes context before shipping a row:


set -a; eval "$(grep ' = "' deployments/neon-law-stg/config.toml | sed 's/ = /=/')"; set +a
gcloud container clusters get-credentials \
  "$NAVIGATOR_GKE_CLUSTER_NAME" --region "$NAVIGATOR_GCP_LOCATION" \
  --project "$NAVIGATOR_GCP_PROJECT_ID"

The resulting context name must equal that config's NAVIGATOR_GKE_CONTEXT. Repeat once for each deployment you are operating.

The canonical matrix is the compact reference for every row.