Infrastructure
Everything a deployment is replaced around is declared in infra/ with
OpenTofu, so the repository says what exists in your Cloudflare account
instead of the dashboard holding that knowledge alone.
What it declares
Three resources per Runtime Context, named <slug>-<app>-<context>:
| Resource | For | Holds |
|---|---|---|
| R2 bucket | apps/web | uploaded files |
| Hyperdrive | apps/web | the pooled connection to Postgres |
| D1 | apps/site | the @nuxt/content dump |
Each is declared once in a provider module under infra/modules/, and one root capability file
composes it — a file per row of that table, not per application, so apps/web's two rows are two
files. A committed infra/contexts/<context>.tfvars file activates a Runtime Context; the root
derives an independent backend key and OpenTofu data directory for it.
What it does not declare
One resource, one owner.
| Owner | Owns |
|---|---|
| OpenTofu | the three resources above |
varlock-wrangler deploy | the Worker script, its versions, and every binding on it |
.github/workflows/deploy.yml | which event deploys which Runtime Context |
| Your secret store | credentials |
Committed .env.<context> | non-secret per-context values, including names and ids |
Worker scripts are absent on purpose. A Worker's code changes on every push and its bindings are
generated from nuxt.config.ts at build time, so declaring it here would give it two writers. The
Postgres instance behind Hyperdrive is absent too — Cloudflare does not run it, and its connection
string reaches OpenTofu from your secret store.
Running it
infra/tofu.sh <context> <command>
The context is the first argument and has no default. The script fetches every credential into one
process, so nothing lands in your shell. Per context, the order is: init, plan, read it, apply,
commit the ids from output into that context's .env.<context>, then deploy.
.tfvars, and never
commit a *.tfstate. The state holds a database password.infra/contexts/production.tfvars.example exists; committing it as production.tfvars
activates it.
A machine identity is allowed only init, validate, plan, output, show, providers and
version — every write subcommand is refused by name.Nothing discovers these resources at runtime, and an apply produces two kinds of output. The names
your slug decides — the bucket and the D1 database — are printed so a plan says what it created, and
go nowhere: each application derives them. The ids Cloudflare mints are the ones no rule can know in
advance, so paste those into that context's value file in the same commit as the apply, or the next
build uses the old ones. Project identity covers which names
derive.
Going further
infra/README.md is the runbook: how to run it, how to add a Runtime Context, and what to do when a
command refuses. First deploy is the one-time setup for the
non-production contexts, and
Going to production activates the last one.
