| 1 | --- |
| 2 | title: Local development |
| 3 | description: alchemy dev runs Workers in workerd with local simulators for KV, R2, D1, Queues, Hyperdrive, Workflows, Containers, and the Worker binding surface — browser rendering, images, email, cron, Stream, and more. |
| 4 | --- |
| 5 | |
| 6 | import Terminal from "../../../components/Terminal.astro"; |
| 7 | |
| 8 | `alchemy dev` runs your Cloudflare stack on your machine. Workers |
| 9 | execute in workerd — the same runtime used in production — with |
| 10 | local simulators behind their bindings, websites run their |
| 11 | frameworks' own dev servers with native HMR, containers run in |
| 12 | Docker, and code changes hot reload in milliseconds. |
| 13 | |
| 14 | ```sh |
| 15 | alchemy dev |
| 16 | ``` |
| 17 | |
| 18 | <Terminal content={`[g]✓[/g] [b]Bucket[/b] [d](Cloudflare.R2.Bucket)[/d] [g]created[/g] [d](local)[/d] |
| 19 | [g]✓[/g] [b]DB[/b] [d](Cloudflare.D1.Database)[/d] [g]created[/g] [d](local)[/d] |
| 20 | [g]✓[/g] [b]Worker[/b] [d](Cloudflare.Worker)[/d] [g]created[/g] [d](local → workerd)[/d] |
| 21 | [s2][d]• http://localhost:1337[/d] |
| 22 | |
| 23 | [d]Watching for changes ...[/d]`} /> |
| 24 | |
| 25 | ## Workers run in workerd |
| 26 | |
| 27 | Your Worker code executes in workerd with bindings wired to the |
| 28 | local simulators, so binding behavior matches what you deploy: |
| 29 | |
| 30 | ```typescript |
| 31 | export const Cache = Cloudflare.KV.Namespace("Cache"); |
| 32 | |
| 33 | export default class Api extends Cloudflare.Worker<Api>()( |
| 34 | "Api", |
| 35 | { main: import.meta.url }, |
| 36 | Effect.gen(function* () { |
| 37 | const kv = yield* Cloudflare.KV.ReadWriteNamespace(Cache); |
| 38 | return { |
| 39 | fetch: Effect.gen(function* () { |
| 40 | yield* kv.put("greeting", "hello"); |
| 41 | const value = yield* kv.get("greeting"); |
| 42 | return yield* HttpServerResponse.text(value ?? ""); |
| 43 | }).pipe(Effect.orDie), |
| 44 | }; |
| 45 | }).pipe(Effect.provide(Cloudflare.KV.ReadWriteNamespaceBinding)), |
| 46 | ) {} |
| 47 | ``` |
| 48 | |
| 49 | In dev, `kv.put` lands in a local KV simulator and the Worker |
| 50 | serves at `http://localhost:<port>`. Durable Objects run inside |
| 51 | the same workerd instance, cron triggers fire on their real |
| 52 | schedule (and the miniflare-compatible |
| 53 | `/cdn-cgi/handler/scheduled` route triggers them manually), and |
| 54 | because everything is a local process you can attach a debugger, |
| 55 | set breakpoints, and profile. |
| 56 | |
| 57 | A local resource's id is `dev:`-prefixed, which doubles as proof |
| 58 | that no cloud call ran. |
| 59 | |
| 60 | ## Websites run their own dev servers |
| 61 | |
| 62 | Every `Cloudflare.Website.*` resource — Vite, Next.js, Astro, |
| 63 | Nuxt, SvelteKit, Waku, Octane, Foldkit, StaticSite — runs its |
| 64 | framework's own dev server under `alchemy dev`: native HMR, no |
| 65 | cloud resources. The site's `url` is the local dev server's |
| 66 | address: |
| 67 | |
| 68 | ```typescript |
| 69 | const site = yield* Cloudflare.Website.Vite("Web"); |
| 70 | // dev: site.url = http://localhost:5173 — Vite's own dev server, HMR included |
| 71 | // deploy: site.url = the deployed site |
| 72 | ``` |
| 73 | |
| 74 | See |
| 75 | [Frontend frameworks](/cloudflare/frontend/frontends). |
| 76 | |
| 77 | ## Containers run in Docker |
| 78 | |
| 79 | A `Cloudflare.Container` runs as a real Docker container on your |
| 80 | machine, driven by its Durable Object in the local workerd: |
| 81 | |
| 82 | ```typescript |
| 83 | export class Sandbox extends Cloudflare.Container< |
| 84 | Sandbox, |
| 85 | { ping: () => Effect.Effect<string> } |
| 86 | >()("Sandbox") {} |
| 87 | |
| 88 | export default Sandbox.make( |
| 89 | { main: import.meta.url }, |
| 90 | Effect.gen(function* () { |
| 91 | return Sandbox.of({ |
| 92 | ping: () => Effect.succeed("pong"), |
| 93 | fetch: Effect.succeed(HttpServerResponse.text("hello")), |
| 94 | }); |
| 95 | }), |
| 96 | ); |
| 97 | ``` |
| 98 | |
| 99 | All three image sources work locally: `main` (a bundled Effect |
| 100 | program), `context` (your own Dockerfile), and `image` (a |
| 101 | pre-built remote image). |
| 102 | |
| 103 | ## What's emulated |
| 104 | |
| 105 | These resources ship a local provider and run as simulators in |
| 106 | dev: |
| 107 | |
| 108 | | Resource | Local behavior | |
| 109 | | ------------------------ | --------------------------------------------------------------------------------- | |
| 110 | | Workers | workerd, with hot reload and a `http://localhost:<port>` URL | |
| 111 | | Durable Objects | run inside the local workerd, including SQLite storage | |
| 112 | | KV Namespaces | local simulator; Node-side capability clients hit the same store | |
| 113 | | R2 Buckets | local simulator; an S3-compatible endpoint serves [presigned URLs](/cloudflare/data/r2-presigned-urls) | |
| 114 | | D1 Databases | local simulator; `migrations` and `importFiles` apply on every reconcile | |
| 115 | | Queues + consumers | local broker with batching, retry, and dead-letter semantics | |
| 116 | | Hyperdrive | connects directly to your database | |
| 117 | | Workflows | run locally with real step semantics | |
| 118 | | Containers | run as Docker containers on your machine | |
| 119 | | Secrets Store | local store seeded with the real (Redacted-sourced) values | |
| 120 | | Websites | the framework's own dev server (Vite, Astro, Next.js, Nuxt, SvelteKit, Waku, Octane, Foldkit, StaticSite) with native HMR | |
| 121 | |
| 122 | Worker-only bindings are simulated too: |
| 123 | |
| 124 | - **Email, both directions** — a `send_email` binding persists |
| 125 | messages as `.eml` files under `.alchemy/local/email`, and a |
| 126 | `POST` to `/cdn-cgi/handler/email` drives your `email()` handler |
| 127 | with accept, reject, and reply semantics. |
| 128 | - **Browser Rendering** — `env.BROWSER` drives a locally-launched |
| 129 | headless Chrome over CDP; `@cloudflare/puppeteer` works |
| 130 | unchanged. |
| 131 | - **Images** — `env.IMAGES` transforms run through sharp. |
| 132 | - **Stream** — uploads land in a local video store. |
| 133 | - **Tail consumers** — a local producer's trace events reach a |
| 134 | local tail Worker. |
| 135 | - **Secret keys** — `secret_key` CryptoKey bindings boot locally. |
| 136 | - **Access** — `dev: { access: { identity } }` stubs the |
| 137 | authenticated identity behind `ctx.access`, so gated paths are |
| 138 | testable without a login wall. See |
| 139 | [Protect a Worker with Access](/cloudflare/security/access). |
| 140 | |
| 141 | Resources whose provider has no local implementation (a Vectorize |
| 142 | index, DNS records) deploy to the real cloud, into your personal |
| 143 | [stage](/environments/stages), so a stack that mixes emulated and |
| 144 | live-only resources just works. |
| 145 | |
| 146 | ## Mix in the real cloud |
| 147 | |
| 148 | `Alchemy.remote()` opts any resource (or a whole scope) out of |
| 149 | local emulation: |
| 150 | |
| 151 | ```typescript |
| 152 | // Effect-native Worker |
| 153 | const browser = yield* Cloudflare.Browser("BROWSER").pipe(Alchemy.remote()); |
| 154 | |
| 155 | // async Worker env |
| 156 | env: { IMAGES: Cloudflare.Images.Images("IMAGES").pipe(Alchemy.remote()) } |
| 157 | ``` |
| 158 | |
| 159 | Local and live compose into hybrid topologies. Pin a queue live |
| 160 | and everything around it stays local — the local Worker produces |
| 161 | real messages into the real queue, and its local consumer drains |
| 162 | the same queue through a pull loop: |
| 163 | |
| 164 | ```typescript |
| 165 | // the queue is real, even in dev |
| 166 | const Jobs = Cloudflare.Queues.Queue("Jobs").pipe(Alchemy.remote()); |
| 167 | ``` |
| 168 | |
| 169 | Your dev session sees the same traffic as staging — messages |
| 170 | produced by deployed services or a teammate's session land in |
| 171 | your local handler. |
| 172 | |
| 173 | Switching a resource between local and live (or dev → deploy) |
| 174 | plans a **replacement** — dev state never silently becomes cloud |
| 175 | state. See |
| 176 | [Local development](/environments/local-development) for the |
| 177 | shared semantics. |
| 178 | |
| 179 | ## Custom port |
| 180 | |
| 181 | ```typescript |
| 182 | export default Cloudflare.Worker("Worker", { |
| 183 | main: import.meta.url, |
| 184 | dev: { |
| 185 | port: 3000, |
| 186 | }, |
| 187 | // ... |
| 188 | }); |
| 189 | ``` |
| 190 | |
| 191 | ## Test against the local stack |
| 192 | |
| 193 | The [test harness](/testing/test-harness) has the same dev mode — |
| 194 | `Test.make({ dev: true })` (available on both the Bun and Vitest |
| 195 | adapters, off by default) runs the suite's `deploy(Stack)` against |
| 196 | the local simulators instead of the cloud, no separate |
| 197 | `alchemy dev` terminal needed: |
| 198 | |
| 199 | ```typescript |
| 200 | const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ |
| 201 | providers: Cloudflare.providers(), |
| 202 | dev: true, |
| 203 | }); |
| 204 | ``` |
| 205 | |
| 206 | Workers, Durable Objects, Workflows, and the simulated bindings all |
| 207 | run in workerd exactly as described above, and the stack's `url` |
| 208 | points at `http://localhost:<port>`. See |
| 209 | [Test harness → dev](/testing/test-harness#dev) and |
| 210 | [Tutorial Part 4](/cloudflare/tutorial/part-4). |
| 211 | |
| 212 | ## Where next |
| 213 | |
| 214 | - [Local development](/environments/local-development) — the |
| 215 | shared `alchemy dev` concepts: hot reload, `Alchemy.remote()`, |
| 216 | and dev vs deploy semantics. |
| 217 | - [Frontend frameworks](/cloudflare/frontend/frontends) — |
| 218 | framework dev servers under `alchemy dev`. |
| 219 | - [Stages](/environments/stages) — how live-in-dev resources stay |
| 220 | isolated per developer. |
| 221 | - [Testing](/testing) — run the same test suite against the local |
| 222 | simulators with `Test.make({ dev: true })`. |
| 223 | - [Local Providers](/infrastructure-as-code/local-provider) — |
| 224 | build the local implementation of a resource. |