Local development
One command runs your whole app locally:
$ pnpm infra dev example/guestbook/app.tsexample/guestbook/app.ts, locally (state in .infra/dev, beside it): guestbook http://guestbook.localhost:8787/ and http://127.0.0.1:8787/ goes to guestbook ✓ applied migration 1 to D1 database guestbook-bookThat’s workerd, the runtime Cloudflare runs Workers in, through Miniflare, the engine under Cloudflare’s own local tools. Not a simulation in Node: your bundle, as it would deploy, with a local database behind it.
Every Worker, wired as deployed
Section titled “Every Worker, wired as deployed”infra dev reads the same declarations a ship does, and wires the same
things, locally:
-
KV, D1 and R2, each a local one, kept on disk.
-
Queues and their consumers: send a message, and the consumer gets it.
-
Calls between Workers, over service bindings, as RPC, with the same encoding as on Cloudflare.
-
Durable Objects, in the Worker that hosts them, WebSockets included.
-
Workflows of the app, run in workerd.
-
Files (
Assets.Directory), served before the Worker, as Cloudflare serves them, and variables, with the environment’s value. -
Rate limits and Analytics Engine.
-
Tail Workers of the app: a request to one Worker reaches its Tail Worker.
-
Containers, in Docker: each Dockerfile image is built as a ship builds it, and each object starts its own container from it. They’re removed when dev stops.
-
Secrets, from your shell’s environment, by binding name:
Terminal window STRIPE_KEY=sk_test_123 pnpm infra dev app.ts
Where to find each Worker
Section titled “Where to find each Worker”Each Worker that serves HTTP answers at its own host:
http://<worker>.localhost:8787/. Most browsers, and curl, resolve
*.localhost to your machine, so there’s nothing to set up. The first
Worker also answers at http://127.0.0.1:8787/ (and localhost:8787).
A Worker with no fetch (one only other Workers call, say) has no URL.
It’s reached through its bindings, as it is when deployed.
--port picks another port. --env pr-7 runs it with that environment’s
names and values: still local, but a variable chosen by a function gets
pr-7’s value.
It keeps your data
Section titled “It keeps your data”Local data lives in .infra/dev/ beside your app module: databases,
buckets, namespaces, queues, objects. Stop and start infra dev, and the
guestbook’s entries are still there. Delete the folder to start fresh.
(It’s in .gitignore already.)
It rebuilds as you save
Section titled “It rebuilds as you save”A change to any file in the app’s folder bundles it again, and the Workers are
reloaded with their data kept. Each reload prints the time and
reloaded, or not reloaded with the reason, such as a bundle that
failed. A typo doesn’t take down what was running.
Migrations first
Section titled “Migrations first”A D1 database’s migrations are applied before infra dev answers a
single request, so your code never meets a database without its tables.
A new migration, added while it runs, is applied on the next reload.
Crons fire, each at its minute
Section titled “Crons fire, each at its minute”Each of your crons fires when its schedule says, in UTC, read as
Cloudflare reads it (1 is Sunday; L, W and # work). The first
check is at the top of the next minute, and each firing prints the time,
the Worker, the cron, and how it went. Leave infra dev running over
lunch and your hourly sweep runs.
What it doesn’t run
Section titled “What it doesn’t run”Some things only exist on Cloudflare, and infra dev won’t pretend.
Each is said with a ! when it starts, so a failing call is never a
mystery:
- Workers AI and Vectorize run only on Cloudflare: left unset.
- The Secrets Store isn’t read: reading a store secret fails.
- Sending email and mTLS certificates: left unset.
- Other apps’ Workers, Durable Objects and workflows: a call through one of those bindings fails. A resource another app owns gets a local one of its own instead, apart from the owner’s.
- A Tail Worker outside the app isn’t run.
- A Hyperdrive config needs a
localconnection string to connect. - Workflow schedules don’t fire yet (a Worker’s crons do).
For those, test the handlers in memory with their stand-ins
(Ai.memory, Vectorize.memory, SecretsStore.memory, a contract’s
.memory(...)): see Testing.
In tests
Section titled “In tests”dev is also a function, from @forge-effect/infra/deploy, so a test
can start the whole app in workerd and call it. See
Testing.
Cloudflare’s own tools: infra config
Section titled “Cloudflare’s own tools: infra config”Prefer Cloudflare’s Vite plugin, or cf? infra config writes one
Worker of your app as a cloudflare.config.ts, which they read:
$ pnpm infra config example/guestbook/app.tswrote …/example/guestbook/cloudflare.config.ts// Generated by @forge-effect/infra from app.ts: the Worker guestbook. Don't edit it; change app.ts, and emit it again.// KV namespaces are named by @forge-effect/infra when it deploys (none here); a config can't name them.import { defineConfig } from '@cloudflare/config';
import * as entrypoint from './.infra/guestbook.ts' with { type: 'cf-worker' };
export default defineConfig({ worker: { name: "guestbook-guestbook", entrypoint, compatibilityDate: "2026-10-01", env: {"BOOK":{"type":"d1","name":"guestbook-book"},"SIGNING":{"type":"rate-limit","namespace":"1483869538","simple":{"limit":3,"period":60}}}, },});Its entry, .infra/guestbook.ts, is three lines: your Worker, served as
workerd calls it. With @cloudflare/vite-plugin and its
experimental.newConfig, vite dev serves the Worker under workerd with
local KV and D1, and vite build writes a Build Output that
ship deploys with --prebuilt.
- An app with several Workers: pick one with
--worker <name>. A config holds one. - It overwrites only files it generated. A
cloudflare.config.tsof your own is left alone, and it says so. - A config can’t name a KV namespace, so deployed from the config, a
namespace gets ship’s default name (
visits-api-cache, after the Worker), whereinfra shipnames it after the app (visits-cache).
For most work, infra dev is the better fit: it runs every Worker at
once, and wires the calls between them.
Good to know
Section titled “Good to know”- Locally, a Tail Worker’s events have no script name.
- workerd has no jurisdictions, so under
infra deva jurisdiction’s names are kept apart instead. infra devneeds Miniflare 5 (in beta), a dev dependency of the repo already, and Docker for an app with containers.- A container whose image is in Cloudflare’s registry, or Cloudflare’s
own Debian image (
Container.debian), doesn’t run locally: dev doesn’t pull the one, and Cloudflare keeps the other to itself. - Checked:
vite devon an emitted config counted visits in local KV and D1 through an Effect layer.