
bilan is a side project of mine: it turns a GitHub repository's pull request history into a dashboard of what's actually happening in it. It runs on Cloudflare: Workers for the code, D1 (SQLite) for the database, R2 for file storage, KV as a cache, and Workflows for long-running jobs.
Until yesterday, it was deployed the way most Workers projects are, with Wrangler, Cloudflare's CLI: a wrangler.jsonc config file, a wrangler deploy, and a handful of secrets set by hand on the Worker. It worked, but the definition of bilan was split between a config file, the Cloudflare dashboard, and my memory.
Yesterday, bilan moved to Alchemy, and its sync logic moved to a domain model built with two libraries I introduce in the next post: Effect and Alchemy Domain. Every pull request now gets its own copy of bilan, with its own database, storage, and URL, and it disappears when the pull request closes. Production never went down along the way.
I didn't write it, though. Agents led this migration: they planned it, wrote the code, opened the pull requests, and ran the cutover. My job was the one I described in AI Turned Every Engineer Into a Tech Lead: pick the direction, make the calls that shape the system, and review.
This is the first of two posts: this one covers the infrastructure, and the next one covers domain-driven design. I'll focus on principles and benefits rather than step-by-step instructions, which would get outdated quickly.
As of October 2026, Alchemy v2 is in beta. Pin exact versions if you try it.
Infrastructure as code means describing your cloud resources (servers, databases, storage, and how they connect) in files you commit, instead of clicking through a dashboard. A tool then creates, updates, or deletes resources until your account matches those files.
Alchemy does that in TypeScript. You declare your Workers, databases, buckets, and Workflows, and how they bind together. A binding is how Cloudflare gives code access to a resource: a Worker gets a handle to its database, its bucket, or another Worker.
I had three reasons to look at it:
I'm used to Terraform, the most common infrastructure-as-code tool, but I've always found it odd to write, and I'm never too sure I got it right until it's applied. Alchemy fixed that for me by using TypeScript: the same language, editor, and type checker as the rest of the app.
bilan's whole system now lives in the repository, in TypeScript, and gets reviewed like any other change: the database, the bucket, the cache, the sync Workflow, and three Workers that bind to each other. Adding a binding or renaming a resource shows up in a pull request. I no longer have to check the dashboard to understand why something is configured the way it is.
Types help here too. A binding refers to the resource itself rather than to a name repeated somewhere else, so a typo fails at compile time.
This is the principle I like the most. Alchemy calls each environment a stage, and production, each pull request, and my laptop come from the same definition. Only the stage changes.
// The production names stay pinned; every other stage gets its own.
const workerName = (base: string) => (prod ? base : `${base}-${stage}`);
prod keeps the names production already had. A pull request becomes stage pr-42, with its own database, bucket, cache, Workflow, and Workers. My laptop gets a local stage that runs everything locally, with no Cloudflare account involved.
Since every environment comes from the same definition, they don't drift apart. If something works in a preview, I can expect it to work in production, without anyone having to configure the preview by hand.
With a preview per pull request, I review a running app along with the diff. Of everything in this migration, it's the part I use the most.
Each same-repository pull request is deployed to its own URL on workers.dev, the address Cloudflare gives every Worker, starts with empty storage, and is destroyed when the pull request closes. A weekly job sweeps any stage that was left behind.
Three decisions keep previews safe:
workers.dev, never under bilan.dev. A preview can't set a cookie production would read. While at it, the session cookie moved to a __Host- name, which browsers lock to the exact host.When an agent opens a pull request, I open the preview and try the change myself.
Moving an app that's already in production is usually the riskiest part of an infrastructure migration. In bilan, production is a stage like the others, with stricter rules written next to the resources they protect, and that's what kept the cutover uneventful.
In bilan's case, two rules apply to production only:
Even the database migration history came along: the migrations Wrangler had already applied were adopted, so nothing ran twice. The cutover was a regular deploy, and nothing needed to be rolled back.
A deploy now carries the code, the schema migrations, and the secrets, in one step, from CI only.
Before, a deploy shipped code, migrations were their own command, and secrets were set by hand. Now:
production and preview), and every deploy sets them. What production runs with is what the environment says.main's current head deploys. A CI run that finishes late, or an old run re-run from the Actions tab, can't roll production back to an older definition of the system.That last rule came from reviewing the setup afterward. With infrastructure as code, deploying an old commit reapplies the old infrastructure definition along with the old code.
I didn't expect this part to matter as much as it did. Alchemy works well for agents, for the same reasons it works well for people:
The agents wrote the code, and I spent my time on the decisions: moving infrastructure first and the domain model second, putting previews on workers.dev, and choosing when to cut over.
Here's what to weigh before trying it:
If your app is a single Worker and you're happy with it, probably not yet.
If your app is a few Workers and some storage, or you want a preview of every pull request, yes. The previews alone were worth it for me, and having the whole system in one typed file makes later changes easier to reason about, whether I make them or an agent does.
In the next post, I cover how a small domain model, built with Effect and Alchemy Domain, replaced the most fragile code in bilan.