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).
0. Prerequisites
Section titled “0. Prerequisites”- A Cloudflare account containing the
quarry.teamzone (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.jsonenginesfield (Node>= 22.12.0, pnpm>= 9; the pinned version inpackageManageris installed viacorepack enable). - wrangler (run via
pnpm dlx wrangleror installed globally) authenticated against the Cloudflare account for the one-time bucket/secret commands below. - Terraform
>= 1.6.0with thecloudflare/cloudflareprovider~> 5.0(CI installs its own; only needed locally for ad-hoc plans). Seeinfra/README.md.
1. Create the repository secrets
Section titled “1. Create the repository secrets”In GitHub → Settings → Secrets and variables → Actions, create the secrets:
CLOUDFLARE_API_TOKEN— scoped exactly as listed in the bootstrap section ofinfra/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 fromterraform output turnstile_sitekeyafter 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:
wrangler r2 bucket create quarry-tf-stateDetail (token creation, backend config): infra/README.md.
3. Provision the infrastructure
Section titled “3. Provision the infrastructure”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:
terraform output -raw d1_database_id # [[d1_databases]].database_idterraform output -raw d1_staging_database_id # [env.staging] d1 bindingterraform output -raw sessions_kv_namespace_id # [[kv_namespaces]].idterraform output -raw sessions_staging_kv_namespace_id # [env.staging] kv bindingCommit 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):
wrangler secret put TURNSTILE_SECRET # = terraform output turnstile (sensitive)wrangler secret put RESEND_API_KEY # lead-notification emailwrangler secret put LEADS_NOTIFY_TO # optional — notification recipientwrangler secret put LEADS_WEBHOOK_URL # optional — outbound webhook notify
wrangler secret put TURNSTILE_SECRET --env stagingwrangler secret put RESEND_API_KEY --env staging# (+ optional LEADS_* as above)D1 is bound to the Worker as DB; no connection-string secret exists.
6. Turn staging on
Section titled “6. Turn staging on”Set the repository variable STAGING_DEPLOY_ENABLED=true. From then on:
- every push to the default branch deploys staging →
https://staging.quarry.team(full production parity against the staging database;X-Robots-Tag: noindexenforced at the edge); - every PR touching the site gets an ephemeral preview URL posted as a PR comment (Worker version upload — no traffic promotion).
7. Soak and verify
Section titled “7. Soak and verify”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.devblocked, cache headers on hashed assets). - The full checklists live in the deploy runbook (pre-deploy and post-deploy verification).
8. Cutover
Section titled “8. Cutover”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.
9. First release
Section titled “9. First release”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.