0004. Delivery: the family's infrastructure as code, applied by pipelines
Status: accepted (owner, 20 October 2026; the delivery change)
Context
Section titled “Context”Decision 0001 left the GitHub repositories, the pipelines, DNS, the production OAuth apps and the secrets to a delivery change per repository, after the functionality was validated locally. On 20 October 2026 the owner decided to go to production, with two rules: every piece of infrastructure configuration is Terraform (Cloudflare, the registrar, GitHub, anything else that holds configuration), and everything runs as GitHub pipelines, never from a machine.
What was found that day: meltemi.dev and meltemi.page are registered at Dynadot with its
nameservers; the apex of meltemi.dev holds only the four A records of the IDE’s GitHub Pages
site; neither repository was a Git repository; the GitHub organisation is askenaz-dev and the
npm organisation meltemi. The Cloudflare provider declares Workers as entities, their Custom
Domains, D1, R2, zones, records, the Secrets Store (one per account, open beta) and account-owned
tokens, but cannot deploy a Worker’s static assets nor create a Durable Object binding in a first
apply; no Terraform provider exists for Dynadot.
Decision
Section titled “Decision”- One Terraform root, here.
packages/infradeclares the zones and their records, the databases, the bucket, the four Workers as entities (noworkers.dev, no previews), their Custom Domains, the Secrets Store and its eight secrets, a deploy token per product repository, and on GitHub both product repositories, theirproductionenvironments with the token and the ids, the release tokens and the rules ofmain. Its state is in R2 behind the S3 backend’s lock file. This repository holds it because it already holds what the family shares (decision 0001). - Terraform declares, Wrangler uploads. Each product’s
deploy.ymluploads versions (code, static assets, Durable Object migrations) onto the entities withwrangler deploy. Thewrangler.jsoncfiles hold no route, keepworkers_devandpreview_urlsoff, and keep placeholder ids;scripts/deploy-config.mjswrites the real ones from the variables Terraform set in theproductionenvironment, so nobody copies an id. - Secrets from the Store. Terraform generates the secrets nobody needs to know (the state
secret, the forms secret, the events secret the two products share) and receives the others as
sensitive variables; the Workers bind them as
<NAME>_STOREand read them once per isolate at their entry.wrangler secret putis never run. Rotation is a commit (rotations). - Pipelines.
infra-plancomments the plan on a pull request;infra-applyapplies the plan it made onmain;infra-driftopens an issue when the cloud differs;registrarpoints the domains at the zones’ nameservers through Dynadot’s API, once, standing in for the provider that does not exist.ci,deployandreleasebuild, upload and publish the code. - The bootstrap is the only thing done by hand: the repository, the state bucket and its key,
Terraform’s Cloudflare and GitHub tokens, the Dynadot key and the values only people can obtain
(the signing key, the OAuth secrets, the npm and marketplace tokens), in the
infrastructureenvironment. - The package is published.
@meltemi/identitygoes to npm, public, with provenance, fromrelease.ymlon a version tag; products install it instead of linking it. - Repository and licence.
askenaz-dev/meltemi-web, public, Apache-2.0.
Alternatives considered
Section titled “Alternatives considered”- Terraform-managed Worker versions (
cloudflare_worker_versionwith Wrangler’s bundle): one owner for bindings and secrets, but no static assets (the site, Diagrams’ playground) and a two-step apply for Durable Objects. - Classic Worker secrets set by the deploy workflow from GitHub secrets: no change to the Workers, but every value in two places and stale secrets left behind.
- A community Dynadot provider or a generic REST provider: none is published; the REST API’s per-request signature is beyond a generic provider. Dynadot also answers only whitelisted IP addresses, so the nameserver step runs once from the owner’s machine.
- Transferring the domains to Cloudflare Registrar: it would leave no registrar outside Cloudflare; the owner keeps Dynadot as the registrar and uses Cloudflare for DNS only.
- HCP Terraform for state and runs: a third account and a third home for secrets.
- A fourth repository for the infrastructure: cleaner, with one more bootstrap; the folder can move there unchanged if the estate grows.
Consequences
Section titled “Consequences”- The rollout has stages, each a pull request whose plan shows it: the first apply without
domains, the registrar step, the products’ first deploys,
attach_domains, thenapex_on_github_pages = falseand, in the next commit,site_on_apex = true, which hand the apex to the family site and let the IDE repository retire its Pages site. Two commits, because Cloudflare refuses a Custom Domain on a hostname that still has records and one apply cannot order the removal first; the apex answered nothing for the few minutes between them on 2026-10-21. - Secrets live in the Terraform state, which lives in a private bucket whose keys are two secrets
of the
infrastructureenvironment. - Dynadot stays the registrar; the nameserver step is the one action run outside a pipeline, from a whitelisted IP. DNSSEC is not set up: it would need the DS record at Dynadot as well.
- Meltemi Diagrams’ delivery binds what this root outputs and creates nothing.