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. The v1 documentation lives at v1.alchemy.run.

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, Bucket } from "alchemy/cloudflare";
const app = await alchemy("my-app", {});
const bucket = await Bucket("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.R2.Bucket("Bucket");
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
env: { Bucket },
});
export default Alchemy.Stack(
"MyApp",
{ providers: Cloudflare.providers(), state: Cloudflare.state() },
Effect.gen(function* () {
const worker = yield* Worker;
return { url: worker.url };
}),
);

Key differences:

  • entrypoint is now called main
  • await is now yield*
  • No more finalize() — the Stack handles lifecycle automatically

Your existing Worker runtime code does not need to change. The async pattern declares bindings on the Worker’s env prop 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",
env: { 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 env declared on the Worker. You get type safety on the binding names and their APIs without using Effect in your runtime code.

Your v1 state is not compatible with v2, so v2 starts from an empty state store. Without extra steps, the first deploy would create brand-new resources alongside your existing ones. Instead, you adopt the resources v1 already deployed — no destroy, no downtime.

Adoption works by physical name: when a resource has no prior state, the engine looks it up in the cloud by name and takes ownership of what it finds. But v1 and v2 derive default names differently, so without pinning, v2 would look for names that don’t exist.

When you didn’t set name explicitly, v1 derived it as:

{app}-{id}-{stage}
  • {app} — the name you passed to await alchemy("my-app")
  • {id} — the resource’s logical ID, e.g. await Bucket("bucket")
  • {stage} — the --stage value; defaults to $ALCHEMY_STAGE, then your OS username ($USER), then dev

So the v1 example at the top of this page, deployed with --stage prod, created a bucket named my-app-bucket-prod and a worker named my-app-worker-prod. (Resources created inside a nested scope get the scope names between {app} and {id}, and Worker names are lowercased.)

Set the name prop on each v2 resource to that exact name:

export const Bucket = Cloudflare.R2.Bucket("Bucket", {
name: "my-app-bucket-prod", // v1's {app}-{id}-{stage}
});
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
name: "my-app-worker-prod", // v1's {app}-{id}-{stage}
env: { Bucket },
});

If you did set name in v1, carry that value over unchanged. When in doubt, confirm the live names in your v1 state (.alchemy/ by default) or the Cloudflare dashboard.

Because v1 baked the stage into the name, each stage has different physical names. If you deploy several stages from the same config, compute the name from the stage (e.g. an environment variable your CI sets) instead of hardcoding one stage’s suffix.

Terminal window
alchemy deploy --adopt

For each resource, the engine finds the existing one by its physical name and adopts it into v2 state, then reconciles it to the declared config. --adopt tells the engine to take over resources it cannot prove it owns — which is exactly the situation for everything v1 created. See Adopting Resources for the full ownership rules.

From here on, deploys are normal:

Terminal window
alchemy deploy

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 declaring env bindings on the resource 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.url },
Effect.gen(function* () {
const bucket = yield* Cloudflare.R2.ReadWriteBucket(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.url 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",
env: { Bucket },
});
export default Alchemy.Stack(
"MyApp",
{ providers: Cloudflare.providers(), state: Cloudflare.state() },
Effect.gen(function* () {
const bucket = yield* Bucket;
const worker = yield* Worker;
return { url: worker.url };
}),
);
v1 (async) v2 (async style) v2 (Effect style)
Stack await alchemy("name") Alchemy.Stack("name", ...) Alchemy.Stack("name", ...)
Resources await Bucket(...) Cloudflare.R2.Bucket(...) Cloudflare.R2.Bucket(...)
Worker entry entrypoint main main: import.meta.url
Bindings bindings: { KEY: resource } env: { Key: resource } yield* Binding(ref)
Runtime code async fetch(req, env) async fetch(req, env) Effect.gen(function* () { ... })
Lifecycle await app.finalize() automatic automatic
Type safety runtime errors typed env via InferEnv full Effect type system