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.
Step 1: Replace the Stack
Section titled “Step 1: Replace the Stack”In v1, you create an app with await alchemy(...) and finalize it
at the end:
// v1 — alchemy.run.tsimport 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.tsimport * 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:
entrypointis now calledmainawaitis nowyield*- No more
finalize()— the Stack handles lifecycle automatically
Step 2: Keep your async handler
Section titled “Step 2: Keep your async handler”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:
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:
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.
Step 3: Pin your physical names
Section titled “Step 3: Pin your physical names”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 toawait alchemy("my-app"){id}— the resource’s logical ID, e.g.await Bucket("bucket"){stage}— the--stagevalue; defaults to$ALCHEMY_STAGE, then your OS username ($USER), thendev
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.
Step 4: Deploy with --adopt
Section titled “Step 4: Deploy with --adopt”alchemy deploy --adoptFor 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:
alchemy deploy(Optional) Adopt Effect for runtime code
Section titled “(Optional) Adopt Effect for runtime code”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*:
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:
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 }; }),);Summary
Section titled “Summary”| 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 |
Where next
Section titled “Where next”- Tutorial — build a full app on v2 from scratch, on Cloudflare or AWS
- Adopting Resources — the full ownership
rules behind
--adopt - Bindings — what replaced v1
bindings: - Cloudflare — the Cloudflare provider hub
- AWS — the AWS provider hub
- v1.alchemy.run — the v1 documentation