Skip to content

Local development

alchemy dev runs your stack on your machine. Workers execute in workerd, KV Namespaces, R2 Buckets, D1 Databases, and Queues are emulated locally, and code changes hot reload in milliseconds. Alchemy.remote() runs any resource against the real cloud when you need it.

Terminal window
alchemy dev
 Bucket (Cloudflare.R2.Bucket) created (local)
 DB (Cloudflare.D1.Database) created (local)
 Worker (Cloudflare.Worker) created (local → workerd)
  • http://localhost:1337

Watching for changes ...

Three things happen:

  1. Emulatable services run locally — KV Namespaces, R2 Buckets, D1 Databases, and Queues are created as local simulators with dev:-prefixed ids. No cloud calls. D1 applies migrationsDir and importFiles against the local database.
  2. Workers run locally in workerd — your code executes in workerd, the same runtime used in production, with bindings wired to the local simulators.
  3. File changes hot reload — edit your code and the Worker rebuilds instantly.

Resources whose provider has no local implementation (a Hyperdrive, a Vectorize index) deploy to the real cloud, into your personal stage (dev_$USER by default), so your loop never collides with teammates or prod. A stack that mixes emulated and live-only resources just works.

Watching for changes ...

 src/worker.ts changed
  • Rebuilding worker ...
 Worker reloaded in 42ms

 src/api.ts changed
  • Rebuilding worker ...
 Worker reloaded in 38ms
  • Code changes take effect in milliseconds, not minutes
  • Resources stay running across reloads — only your application code is rebuilt

Arbitrary dev processes — Vite, Next, anything with a dev server — join the loop via Command.Dev: started by alchemy dev, restarted when its inputs change, and a no-op on deploy.

KV, R2, D1, and Queues ship a local provider. In dev they are simulators running inside workerd, the same runtime used in production, so binding behavior matches what you deploy. A local resource’s id is dev:-prefixed, which doubles as proof that no cloud call ran.

The local simulators reach beyond Worker bindings. Node-side capability clients follow the same rule: a seed script in an Action that writes to a dev: KV Namespace lands in the same local simulator your Worker reads.

Every other resource runs live in dev automatically, and Alchemy.remote() (below) runs an emulatable resource against the real cloud when local fidelity isn’t enough.

Worker-only bindings are simulated too: a send_email binding persists messages as .eml files under .alchemy/local/email instead of delivering mail, Browser Rendering drives a locally-launched headless Chrome, Images transforms run through Sharp, and Stream uploads land in a local video store. Each of these opts out the same way as a resource — pipe the binding through Alchemy.remote() (below) to hit the real service from the dev loop:

env: {
EMAIL: email, // Alchemy.remote() on the SendEmail descriptor
BROWSER: Cloudflare.Browser("BROWSER").pipe(Alchemy.remote()),
}

Because Workers run locally in workerd, you can attach a debugger:

  • Set breakpoints in your IDE
  • Inspect variables and call stacks
  • Profile performance

Resources adapt their behavior in dev mode:

  • Cloudflare Workers — run locally in workerd with a cloud proxy instead of deploying to the edge
  • Vite dev servers — integrate with Alchemy’s dev server for frontend hot reloading alongside your backend
  • D1 Databases — emulate locally, with migrationsDir and importFiles applied to the local database on every reconcile

A resource emulates locally when its provider ships a local implementation; everything else is live in dev automatically. See Local Providers to build one.

Alchemy.remote() opts a resource out of local emulation — “run this live even in dev”:

import * as Alchemy from "alchemy";
const worker = yield* Worker("Api", { ... }).pipe(Alchemy.remote());

During alchemy deploy the pin is a no-op — everything is live anyway.

yield* Effect.gen(function* () {
const api = yield* Worker("Api", { ... });
const site = yield* Worker("Site", { ... });
}).pipe(Alchemy.remote());

The policy is ambient and captured at registration (like adopt()), so everything inside the scope runs live. Resources with no local emulation (a Hyperdrive, a Vectorize index) already run live in dev — remote() on them is a no-op.

Queues support remote() in both directions: a local Worker binding a live queue produces real messages, and a local queue() handler drains the live queue through a pull consumer — with the usual local batching, retry, and dead-letter semantics.

When a resource moves between local and live — you deploy after a dev session, or add/remove remote() — it’s planned as a replacement: the new instance is created live, and the local one (the workerd instance, the dev process) is torn down.

export default Cloudflare.Worker("Worker", {
main: import.meta.url,
dev: {
port: 3000,
},
// ...
});
alchemy dev alchemy deploy
KV, R2, D1, Queues Local simulators (remote() for live) Deployed to cloud
Everything else Deployed to cloud Deployed to cloud
Application code Runs locally in workerd Deployed to cloud
File watching Hot reload on change Manual redeploy
Debugging Attach local debugger Tail logs
Speed Milliseconds Seconds to minutes

To automate the deploy side, see the CI guide.

  • CI — deploy the same stack from GitHub Actions with PR previews.
  • Stages — how dev_$USER, pr-42, and prod stay isolated.
  • Dev servers — run any framework dev server as a Command.Dev resource in the dev loop.
  • Local Providers — build the local implementation of a resource.