Skip to content

Runbook: Bootstrap (from-zero rebuild)

How to stand up the entire quarry.team stack from nothing — a fresh Cloudflare account and a fresh GitHub repository — to a verified production cutover and a first tagged release. Each step links to the document that owns its detail; this runbook is the ordered recipe.

Related: infra/README.md (Terraform, token scopes, CI secrets) · deploy runbook (pipeline, environments, rollback) · release runbook (Release Drafter) · ADR 0013 (staging-first cutover model).

  • A Cloudflare account containing the quarry.team zone (the zone must already exist — Terraform looks it up by name, it does not create it).
  • A GitHub repository with the default branch protected and GitHub Actions enabled.
  • Node and pnpm per the root package.json engines field (Node >= 22.12.0, pnpm >= 9; the pinned version in packageManager is installed via corepack enable).
  • wrangler (run via pnpm dlx wrangler or installed globally) authenticated against the Cloudflare account for the one-time bucket/secret commands below.
  • Terraform >= 1.6.0 with the cloudflare/cloudflare provider ~> 5.0 (CI installs its own; only needed locally for ad-hoc plans). See infra/README.md.

In GitHub → Settings → Secrets and variables → Actions, create the secrets:

  • CLOUDFLARE_API_TOKEN — scoped exactly as listed in the bootstrap section of infra/README.md:
    • Zone (quarry.team only): Zone Read, DNS Write, Workers Routes Write, Zone WAF Write, Cache Rules Write, Transform Rules Write, Zone Settings Write.
    • Account: Workers Scripts Write, D1 Write, Workers KV Storage Write, Workers R2 Storage Write, Turnstile Sites Write.
  • CLOUDFLARE_ACCOUNT_ID — the account id (also used to build the R2 state endpoint).
  • R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY — an R2 S3 API token (Object Read & Write) for the Terraform state backend.
  • PUBLIC_TURNSTILE_SITE_KEY — the Turnstile widget site key, baked into the form markup at build time (available from terraform output turnstile_sitekey after step 3; set it before the first site deploy).

The full secrets table lives in infra/README.md → CI secrets & variables.

2. Bootstrap the Terraform state bucket (one-time)

Section titled “2. Bootstrap the Terraform state bucket (one-time)”

Create the remote-state bucket before the first terraform init:

Terminal window
wrangler r2 bucket create quarry-tf-state

Detail (token creation, backend config): infra/README.md.

Run the Infra (Terraform) workflow (workflow_dispatch) with plan, review the output, then run it again with apply. This creates the config plane: D1 databases (production + staging), sessions KV namespaces, the Turnstile widget, and the WAF / rate-limit / cache rulesets. (The Terraform state R2 bucket is bootstrapped out-of-band, not by this apply — see step 2.) The Worker itself is deployed by wrangler, not Terraform.

4. Copy the Terraform output ids into wrangler.toml

Section titled “4. Copy the Terraform output ids into wrangler.toml”

From the apply’s outputs, copy the four non-secret ids into apps/web/wrangler.toml:

Terminal window
terraform output -raw d1_database_id # [[d1_databases]].database_id
terraform output -raw d1_staging_database_id # [env.staging] d1 binding
terraform output -raw sessions_kv_namespace_id # [[kv_namespaces]].id
terraform output -raw sessions_staging_kv_namespace_id # [env.staging] kv binding

Commit the change. D1 schema migrations are applied automatically by the deploy jobs (wrangler d1 migrations apply runs before every deploy — see the deploy runbook).

5. Set the Worker secrets (per environment)

Section titled “5. Set the Worker secrets (per environment)”

Set each runtime secret once for production and once for staging (--env staging):

Terminal window
wrangler secret put TURNSTILE_SECRET # = terraform output turnstile (sensitive)
wrangler secret put RESEND_API_KEY # lead-notification email
wrangler secret put LEADS_NOTIFY_TO # optional — notification recipient
wrangler secret put LEADS_WEBHOOK_URL # optional — outbound webhook notify
wrangler secret put TURNSTILE_SECRET --env staging
wrangler secret put RESEND_API_KEY --env staging
# (+ optional LEADS_* as above)

D1 is bound to the Worker as DB; no connection-string secret exists.

Set the repository variable STAGING_DEPLOY_ENABLED=true. From then on:

  • every push to the default branch deploys staginghttps://staging.quarry.team (full production parity against the staging database; X-Robots-Tag: noindex enforced at the edge);
  • every PR touching the site gets an ephemeral preview URL posted as a PR comment (Worker version upload — no traffic promotion).

Use the staging period to verify the stack end to end before cutover:

  • Contact and careers forms submit end to end (Turnstile passes, record lands in the staging D1, notification email sends).
  • Edge rules behave (rate limiting on /api/*, *.workers.dev blocked, cache headers on hashed assets).
  • The full checklists live in the deploy runbook (pre-deploy and post-deploy verification).

Set the repository variable DEPLOY_ENABLED=true and run the deploy workflow (or push to the default branch). The production job deploys the same Worker with the apex/www custom-domain routes — DNS attaches to the Worker on deploy. Verify per the deploy runbook checklist. Rollback: wrangler rollback, or re-point the two DNS records.

Versioning and release notes run on Release Drafter, which maintains a rolling draft GitHub Release. Publishing the draft (with a short human lede) tags the version and makes the Release public; until then nothing is tagged while the site deploys continuously. Full steps: release runbook.