Environments, state and cleanup
Production is the app you ship with no flags. Any other name makes a complete second copy, with its own Workers and its own resources:
$ pnpm infra ship example/guestbook/app.ts --env pr-7 1.69s ✓ created D1 database guestbook-pr-7-book 2.18s ✓ applied migration 1 to D1 database guestbook-pr-7-book 3.43s ✓ created Worker guestbook-pr-7-guestbook, live 5.15s ✓ smoke test passed: 3 × GET /, answered by the new Worker 5.46s ✓ shipped guestbook-pr-7-guestbook: https://guestbook-pr-7-guestbook.<you>.workers.devAnd when the pull request closes:
$ pnpm infra destroy example/guestbook/app.ts --env pr-7 --yes✓ deleted worker guestbook-pr-7-guestbook✓ deleted d1 guestbook-pr-7-bookGone, all of it, with nothing left behind. This page is how that works, and how forge-effect keeps track of what’s yours.
Environments
Section titled “Environments”Every command takes --env <name>: plan, ship, destroy, prune,
state, check, secret put, even dev. Leave it out and it’s
production. A name is lowercase letters, digits and single hyphens,
starting with a letter: pr-7, staging, ada-test.
Names in your account
Section titled “Names in your account”Everything is named after the app, the environment (unless it’s production), and the name you declared:
| Declared | production | --env pr-7 |
|---|---|---|
the app guestbook’s Worker guestbook |
guestbook-guestbook |
guestbook-pr-7-guestbook |
its D1 database book |
guestbook-book |
guestbook-pr-7-book |
So two environments never share a resource by accident, and you can tell at a glance in the dashboard what’s whose.
What differs between them
Section titled “What differs between them”By default, nothing but the names: a preview is the same app, empty. Where you want a difference, a declaration takes a function of the environment’s name:
- a variable’s value,
- routes and custom domains, which are production’s alone unless you say otherwise,
- a Worker’s settings, such as logging less in previews.
A binding to another app points at that app’s copy in the same
environment, so pr-7 of your shop calls pr-7 of your accounts app.
To call production’s instead, say so: see
Workers calling Workers.
The state: what’s yours
Section titled “The state: what’s yours”The first ship makes a small D1 database in your account,
forge-effect-state. It records, for each app and environment:
- Each resource it owns, by its Cloudflare id, and whether the tool made it. The next ship binds it by that id, not by looking for the name.
- What happened: each ship, failure, prune and teardown.
- A lock, so two deploys of the same app and environment never race.
- The versions it deployed and the contracts it shipped, so a later check notices a deploy from elsewhere, and a contract change that would break callers.
What exists is always read from Cloudflare. The state only says what’s
the app’s. plan reads it without writing, so a first plan works before
there’s any state at all.
That small record buys some lovely guarantees.
Nothing is taken by accident
Section titled “Nothing is taken by accident”Say there’s already a D1 database called guestbook-book in your
account, made by hand last year. Your app declares book, which would be
guestbook-book. Without a record, a tool would happily bind it, and
your guestbook would write into someone’s old data.
Here, a name that exists but isn’t recorded as the app’s is name-taken.
The plan marks it with !, saying whose it is (another app’s, or not
made by this tool), and a ship stops before changing anything. Rename the
resource or the app, or delete the old one if it’s no one’s.
Something deleted by hand is made again
Section titled “Something deleted by hand is made again”If someone deletes the app’s database in the dashboard, the next plan says
it’ll be made again, as it was deleted outside the tool, and the ship does
that and records it anew. infra check --account reports it as drift,
along with any Worker running a version the tool didn’t deploy.
Nothing is deleted by accident
Section titled “Nothing is deleted by accident”Stop declaring a resource, and the next ship doesn’t delete it. It unbinds it, keeps it, and marks it orphaned:
- the plan says it’s no longer declared, and kept until prune,
infra prunedeletes it when you’re sure.
pnpm infra prune app.ts cache # delete the orphan named cachepnpm infra prune app.ts --allow-data-loss # delete every orphanprune with no names refuses, naming the orphans it would delete, unless
you pass --allow-data-loss. An orphan the tool didn’t make is only forgotten,
never deleted. A removed Durable Object is pruned by shipping its host
Worker with Cloudflare’s tombstone for the class, which deletes its
objects; see Durable Objects.
Two deploys never race
Section titled “Two deploys never race”Each ship, prune and destroy holds the app and environment’s lock. A second one stops at once, saying who holds it and since when, and asks you to try again when it’s done. A lock left by a deploy that died (a laptop closed mid-ship) is taken over after 30 minutes.
There’s history
Section titled “There’s history”pnpm infra state app.ts [--env pr-7] [--json]lists the resources the state records (each with its name in the account, its id, whether it’s active or orphaned, and whether the tool made it), then each ship, failure, prune and teardown, with when, and what. A failure’s line carries the reason it failed.
Tearing down
Section titled “Tearing down”infra destroy deletes an environment’s Workers and resources:
$ pnpm infra destroy example/guestbook/app.ts --env pr-7 --yes✓ deleted worker guestbook-pr-7-guestbook✓ deleted d1 guestbook-pr-7-book-
It asks for
--yes. Without it, it says it would delete the app’s Workers and resources in that environment, and stops. -
Production is guarded. Its data is real, so it wants one more flag:
$ pnpm infra destroy example/guestbook/app.ts --yesstopped: tearing down production deletes its data; pass --allow-data-loss if that's meant -
It goes in the order Cloudflare needs. R2 buckets are emptied first (Cloudflare deletes only an empty bucket), a thousand objects a request, before anything is deleted, so a bucket that can’t be emptied stops the teardown with nothing gone. Then routes and domains are detached, buckets’ notifications cleared, queue consumers removed, and the Workers deleted, callers first, before the resources they used.
-
It won’t pull the rug out. If another app still calls one of this app’s Workers, or uses one of its resources, destroy refuses and names it.
--allow-breakingoverrides that, when you mean it. -
Only what’s recorded. It deletes what the state says is the app’s in that environment. A Secrets Store secret, or a resource you bound by its name with
.external(...), was never the app’s, so it stays.
The history survives the teardown, so infra state still tells the story
afterwards.
Good to know
Section titled “Good to know”- Checked live: a plan before any state; a ship that made the state
database, the resources and their records; a second plan and ship that
bound them by id and made nothing; production’s teardown refused
without
--allow-data-loss, then done, with the history kept. - An R2 bucket that still holds something after its objects are deleted (an unfinished multipart upload, or a bucket lock) stops a teardown, with what to do about it.
- Previews cost what their resources cost. A D1 database per preview adds up on an account’s database limit: a test account hit it once, and that ship was undone.