CI-time Cloudflare resource IDs
ADR-2026-0002: CI-time Cloudflare resource IDs
Section titled “ADR-2026-0002: CI-time Cloudflare resource IDs”Context and problem statement
Section titled “Context and problem statement”Citizenry deploys five applications to Cloudflare - three Workers
(api, admin-api, mcp), one migration Worker (migrator), and two
SvelteKit Pages projects (web, admin-web). Three of the four Workers
share a single D1 database (citizenry-vault), and migrator also
touches a second D1 (citizenry-identity). The same wrangler.toml
files are consumed for local development and for production deploys.
Two pressures shape the deploy story:
- Adopter ergonomics. A fresh fork should reach a running deployment
with a small, well-defined number of steps. The current
docs/deploy.mdflow is “fork → set repository secrets → push tomain.” - Multi-Worker resource sharing. The same D1 UUID must be wired into
four
wrangler.tomlfiles, and migrations must run against the same database the runtime Workers bind to.
Cloudflare ships several deployment paths that look like they could simplify this - the “Deploy to Cloudflare” button, the dashboard’s “Connect to Git” flow on Workers Builds, and automatic resource provisioning in Wrangler 4.45+. We need to choose one and document why the others were not chosen, because the trade-offs are non-obvious and the limits are spread across separate documentation pages and open bug reports.
Decision drivers
Section titled “Decision drivers”- Multi-Worker shared D1. Four
wrangler.tomlfiles must agree on one UUID for the vault D1 and a second UUID for the identity D1. Cloudflare D1 databases are identified by UUID, not byname; a name is not a unique key on the account. - Pages support.
apps/webandapps/admin-webdeploy as Cloudflare Pages projects and must ship in the same flow as the Workers. - Idempotent migrations.
wrangler d1 migrations apply --remotemust run against the resolved D1 UUID on every deploy, including the first. - Forkability. Anyone forking the public repo must be able to deploy without coordinating with us, and the committed configuration must not embed our UUIDs.
- Operator visibility. Failure modes during deploy must surface in a single readable place - logs, retry, diff.
Considered options
Section titled “Considered options”- CF “Deploy to Cloudflare” button. Single-click fork-and-deploy via OAuth; relies on Wrangler auto-provisioning.
- CF dashboard “Connect to Git” (Workers Builds). Per-Worker setup
in the dashboard; CF runs
wrangler deployon push. - GitHub Actions + CI-time wrangler config rendering (chosen).
Workflow calls the CF REST API to list-or-create D1 databases,
patches placeholder IDs in
wrangler.tomlwithin the CI workspace, then runswrangler deployfor each app. - Provision Worker. A bootstrap Worker installed via the Deploy button that calls the CF API to spawn the rest of the stack and self-destruct.
Pros and cons
Section titled “Pros and cons”| Deploy button | Connect to Git | GitHub Actions (chosen) | Provision Worker | |
|---|---|---|---|---|
| Multi-Worker shared D1 | weak - auto-provision is per-Worker, no shared-name guarantee | weak - IDs not written back; account uniqueness is by UUID, not name | ✓ same UUID patched into all four configs in one run | ✓ if the bootstrap script enumerates the other Workers |
| Pages projects supported | ✗ docs: “Pages applications are unsupported” | ✗ Pages is a separate creation flow | ✓ workflow runs wrangler pages deploy for both | ✗ Pages is not addressable from a Worker |
| ID write-back to repo | ✓ JSON/JSONC only - broken for TOML (workers-sdk#13632) | ✗ never written back, dashboard-only | n/a - repo keeps placeholders, CI re-resolves every run | n/a |
| Token scope for D1 ops | ✗ Workers Builds token has no D1 scope | ✗ same | ✓ operator-supplied token spans all needed scopes | ✗ same as Workers Builds |
d1 migrations apply --remote | broken - no resolved ID for TOML | broken - placeholder-only repo | ✓ runs against the rendered config | depends on bundled tooling |
| Operator UX on failure | dashboard logs per Worker | dashboard logs per Worker | single Actions run UI with diff and rerun | wrangler tail |
| Required adopter input | OAuth + per-Worker click | OAuth + per-Worker click | three GitHub secrets | OAuth + secret |
| Forkable without leaking IDs | ✓ | ✓ | ✓ | ✓ |
Decision
Section titled “Decision”We deploy via GitHub Actions. The committed wrangler.toml files
carry the literal placeholder database_id = "local-dev-placeholder"
for both D1 bindings. The workflow performs three steps before any
wrangler deploy:
scripts/ci/provision.mjscalls the Cloudflare REST API directly to list-or-create the two D1 databases by name. UUIDs are emitted to$GITHUB_OUTPUT.scripts/ci/render-wrangler.mjsrewritesapps/{api,admin-api,mcp,migrator}/wrangler.tomlin the CI workspace, substituting the placeholder with the resolved UUID. The substitution is anchored onbinding = "DB_VAULT"andbinding = "DB_IDENTITY"so unrelated fields are not touched.- The patched workspace is consumed by
wrangler deployfor each Worker,wrangler d1 migrations apply --remoteagainst the migrator config, andwrangler pages deployfor the two SvelteKit projects.
The patched files exist only in the ephemeral CI workspace and are never committed back. The repo stays in placeholder state, which makes it trivially forkable.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Multi-Worker shared D1 works correctly: all four
wrangler.tomlfiles receive the same UUID inside one workflow run. - Pages projects deploy in the same workflow alongside Workers.
- Migrations always run against the same UUID the runtime Workers bind to, because rendering happens before both the deploy and the migration step.
- Forks inherit a clean repo. No upstream UUID leaks into git history; every fork rebinds to its own Cloudflare account on first run.
- A single GitHub Actions run UI is the source of truth for deploy outcomes: per-step logs, diffs, reruns, and required-secret checks.
- Token scope is owned by the operator’s CF token and can be expanded independently of any platform default.
Negative / cost
Section titled “Negative / cost”- Operators must create a scoped Cloudflare API token by hand and paste
it as a GitHub repository secret. This step is irreducible until
Cloudflare supports GitHub OIDC for the Workers / D1 / Pages API
surfaces (tracked at
cloudflare/wrangler-action#402, open at the time of writing). - The committed
wrangler.tomlfiles are not directly deployable withwrangler deploy --remotefrom a fresh checkout; they require runningprovision.mjs+render-wrangler.mjsfirst. Localwrangler devis unaffected because miniflare ignores the placeholderdatabase_id. - Two small Node scripts (
provision.mjs,render-wrangler.mjs) join the CI surface and must be maintained as the binding shape evolves.
Neutral
Section titled “Neutral”provision.mjstalks to the Cloudflare REST API directly rather than throughwrangler. This sidestepscloudflare/workers-sdk#13632(TOML write-back) and keeps idempotency under our control, at the cost of restating endpoint paths in source.
Implementation
Section titled “Implementation”Already landed at the time of this ADR:
.github/workflows/deploy.yml- workflow withprovision → render → deploy → migrate → pagessteps.scripts/ci/provision.mjs- list-or-create the two D1 databases.scripts/ci/render-wrangler.mjs- patch placeholders in the CI workspace.scripts/ci/migrate-identity.sh,scripts/ci/push-secrets.sh,scripts/ci/deploy-pages.sh- companion scripts for Postgres migrations, secret rotation, and Pages deploys.
Adopter-facing documentation is docs/deploy.md.
Compliance / follow-up
Section titled “Compliance / follow-up”- Re-evaluate when either of the following lands upstream:
cloudflare/wrangler-action#402- OIDC support. Closing this would let us dropCLOUDFLARE_API_TOKENfrom GitHub secrets and authenticate via short-lived tokens minted from the workflow’s OIDC claim. The workflow shape stays the same; only the auth step changes.cloudflare/workers-sdk#13632- TOML write-back support. Closing this does not eliminateprovision.mjs(we still need the API call to share UUIDs across four configs) but would let us simplify the rendering step in single-Worker subprojects.
- If a future Worker is added that does not share D1 with the existing
four, prefer adding it as a separate workflow job over inflating
render-wrangler.mjswith conditional binding logic.
References
Section titled “References”docs/deploy.md- adopter walkthrough- Wrangler configuration · Cloudflare Workers docs
- Automatic resource provisioning for KV, R2, and D1 · Changelog (2025-10-24)
- Deploy to Cloudflare buttons · Cloudflare Workers docs
- Workers Builds - Git integration · Cloudflare Workers docs
cloudflare/workers-sdk#13632- TOML write-back not implementedcloudflare/wrangler-action#402- OIDC support request