Skip to content

Migrating from v1

Alchemy v1 uses async/await with top-level await for orchestration. Alchemy v2 replaces this with Effect generators for type-safe error handling, composable retries, and declarative resource wiring.

Your existing async fetch handlers do not need to change — you can keep them as-is and still get all the benefits of the new engine.

In v1, you create an app with await alchemy(...) and finalize it at the end:

// v1 — alchemy.run.ts
import alchemy from "alchemy";
import { Worker, R2Bucket } from "alchemy/cloudflare";
const app = await alchemy("my-app", {});
const bucket = await R2Bucket("bucket", {});
const worker = await Worker("worker", {
entrypoint: "./src/worker.ts",
bindings: { BUCKET: bucket },
});
console.log(worker.url);
await app.finalize();

In v2, you export a default Alchemy.Stack and use yield* instead of await:

// v2 — alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
export const Bucket = Cloudflare.R2Bucket("Bucket");
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
bindings: { Bucket },
});
export default Alchemy.Stack(
"MyApp",
{ providers: Cloudflare.providers() },
Effect.gen(function* () {
const worker = yield* Worker;
return { url: worker.url };
}),
);

Key differences:

  • await alchemy("name") + await app.finalize()Alchemy.Stack("name", { providers }, effect)
  • await R2Bucket("name", {})Cloudflare.R2Bucket("name")
  • await Worker("name", { entrypoint })Cloudflare.Worker("name", { main })
  • entrypoint is now called main
  • Resources are declared at the top level, then yield*-ed inside the Stack
  • No more finalize() — the Stack handles lifecycle automatically

Your existing Worker runtime code does not need to change. The async pattern passes bindings as props on the Worker resource and uses Cloudflare.InferEnv to type the env object:

alchemy.run.ts
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
bindings: { Bucket },
});

Your handler stays the same — just update the type import:

src/worker.ts
import type { Env } from "../alchemy.run.ts";
import type { WorkerEnv } from "../alchemy.run.ts";
export default {
async fetch(request: Request, env: Env) {
async fetch(request: Request, env: WorkerEnv) {
const object = await env.BUCKET.get("key");
const object = await env.Bucket.get("key");
return new Response(object?.body ?? null);
},
};

Cloudflare.InferEnv derives a fully typed env object from the bindings you declared on the Worker. You get type safety on the binding names and their APIs without using Effect in your runtime code.

The CLI commands are the same:

Terminal window
alchemy deploy
alchemy destroy

Your v1 state is not compatible with v2. On your first deploy, Alchemy creates new resources. You should destroy your v1 stack first, then deploy with v2.

When you’re ready, you can switch to Effect-native Workers. This gives you typed errors, composable retries, and Effect’s HttpServer integration.

Instead of passing bindings as props, you bind resources in the Worker’s Init phase using yield*:

src/worker.ts
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
import { Bucket } from "./bucket.ts";
export default {
async fetch(request: Request, env: WorkerEnv) {
const object = await env.Bucket.get("key");
return new Response(object?.body ?? null);
},
};
export default Cloudflare.Worker("Worker",
{ main: import.meta.path },
Effect.gen(function* () {
const bucket = yield* Cloudflare.R2Bucket.bind(Bucket);
return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const key = request.url.split("/").pop()!;
const object = yield* bucket.get(key);
return object
? HttpServerResponse.text(yield* object.text())
: HttpServerResponse.text("Not found", { status: 404 });
}),
};
}),
);

The Worker resource declaration moves from alchemy.run.ts into the Worker file itself (using import.meta.path as the main), and the Stack just yield*-s the imported Worker:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import Worker from "./src/worker.ts";
import { Bucket } from "./src/bucket.ts";
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
bindings: { Bucket },
});
export default Alchemy.Stack(
"MyApp",
{ providers: Cloudflare.providers() },
Effect.gen(function* () {
const bucket = yield* Bucket;
const worker = yield* Worker;
return { url: worker.url };
}),
);
v1 (async)v2 (async style)v2 (Effect style)
Stackawait alchemy("name")Alchemy.Stack("name", ...)Alchemy.Stack("name", ...)
Resourcesawait R2Bucket(...)Cloudflare.R2Bucket(...)Cloudflare.R2Bucket(...)
Worker entryentrypointmainmain: import.meta.path
Bindingsbindings: { KEY: resource }bindings: { Key: resource }yield* Resource.bind(ref)
Runtime codeasync fetch(req, env)async fetch(req, env)Effect.gen(function* () { ... })
Lifecycleawait app.finalize()automaticautomatic
Type safetyruntime errorstyped env via InferEnvfull Effect type system