| 1 | --- |
| 2 | title: Migrating from v1 |
| 3 | description: Migrate your Alchemy v1 (async/await) project to Alchemy v2. |
| 4 | --- |
| 5 | |
| 6 | Alchemy v1 uses `async/await` with top-level `await` for |
| 7 | orchestration. Alchemy v2 replaces this with Effect generators for |
| 8 | type-safe error handling, composable retries, and declarative |
| 9 | resource wiring. The v1 documentation lives at |
| 10 | [v1.alchemy.run](https://v1.alchemy.run). |
| 11 | |
| 12 | Your existing `async fetch` handlers **do not need to change** — you |
| 13 | can keep them as-is and still get all the benefits of the new engine. |
| 14 | |
| 15 | ## Step 1: Replace the Stack |
| 16 | |
| 17 | In v1, you create an app with `await alchemy(...)` and finalize it |
| 18 | at the end: |
| 19 | |
| 20 | ```typescript |
| 21 | // v1 — alchemy.run.ts |
| 22 | import alchemy from "alchemy"; |
| 23 | import { Worker, Bucket } from "alchemy/cloudflare"; |
| 24 | |
| 25 | const app = await alchemy("my-app", {}); |
| 26 | |
| 27 | const bucket = await Bucket("bucket", {}); |
| 28 | const worker = await Worker("worker", { |
| 29 | entrypoint: "./src/worker.ts", |
| 30 | bindings: { BUCKET: bucket }, |
| 31 | }); |
| 32 | |
| 33 | console.log(worker.url); |
| 34 | |
| 35 | await app.finalize(); |
| 36 | ``` |
| 37 | |
| 38 | In v2, you export a default `Alchemy.Stack` and use `yield*` instead |
| 39 | of `await`: |
| 40 | |
| 41 | ```typescript |
| 42 | // v2 — alchemy.run.ts |
| 43 | import * as Alchemy from "alchemy"; |
| 44 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 45 | import * as Effect from "effect/Effect"; |
| 46 | |
| 47 | export const Bucket = Cloudflare.R2.Bucket("Bucket"); |
| 48 | |
| 49 | export const Worker = Cloudflare.Worker("Worker", { |
| 50 | main: "./src/worker.ts", |
| 51 | env: { Bucket }, |
| 52 | }); |
| 53 | |
| 54 | export default Alchemy.Stack( |
| 55 | "MyApp", |
| 56 | { providers: Cloudflare.providers(), state: Cloudflare.state() }, |
| 57 | Effect.gen(function* () { |
| 58 | const worker = yield* Worker; |
| 59 | return { url: worker.url }; |
| 60 | }), |
| 61 | ); |
| 62 | ``` |
| 63 | |
| 64 | Key differences: |
| 65 | |
| 66 | - `entrypoint` is now called `main` |
| 67 | - `await` is now `yield*` |
| 68 | - No more `finalize()` — the Stack handles lifecycle automatically |
| 69 | |
| 70 | ## Step 2: Keep your async handler |
| 71 | |
| 72 | Your existing Worker runtime code does not need to change. The async |
| 73 | pattern declares bindings on the Worker's `env` prop and uses |
| 74 | `Cloudflare.InferEnv` to type the `env` object: |
| 75 | |
| 76 | ```diff lang="typescript" |
| 77 | // alchemy.run.ts |
| 78 | +export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>; |
| 79 | |
| 80 | export const Worker = Cloudflare.Worker("Worker", { |
| 81 | main: "./src/worker.ts", |
| 82 | env: { Bucket }, |
| 83 | }); |
| 84 | ``` |
| 85 | |
| 86 | Your handler stays the same — just update the type import: |
| 87 | |
| 88 | ```diff lang="typescript" |
| 89 | // src/worker.ts |
| 90 | -import type { Env } from "../alchemy.run.ts"; |
| 91 | +import type { WorkerEnv } from "../alchemy.run.ts"; |
| 92 | |
| 93 | export default { |
| 94 | - async fetch(request: Request, env: Env) { |
| 95 | + async fetch(request: Request, env: WorkerEnv) { |
| 96 | - const object = await env.BUCKET.get("key"); |
| 97 | + const object = await env.Bucket.get("key"); |
| 98 | return new Response(object?.body ?? null); |
| 99 | }, |
| 100 | }; |
| 101 | ``` |
| 102 | |
| 103 | `Cloudflare.InferEnv` derives a fully typed `env` object from the |
| 104 | `env` declared on the Worker. You get type safety on the binding |
| 105 | names and their APIs without using Effect in your runtime code. |
| 106 | |
| 107 | :::tip |
| 108 | This is the recommended migration path. Get your infrastructure on v2 |
| 109 | first, then optionally adopt Effect for runtime code later. |
| 110 | ::: |
| 111 | |
| 112 | ## Step 3: Pin your physical names |
| 113 | |
| 114 | Your v1 state is not compatible with v2, so v2 starts from an empty |
| 115 | state store. Without extra steps, the first deploy would create |
| 116 | brand-new resources alongside your existing ones. Instead, you |
| 117 | [adopt](/cli/adopting-resources) the resources v1 already deployed — |
| 118 | no destroy, no downtime. |
| 119 | |
| 120 | Adoption works by physical name: when a resource has no prior state, |
| 121 | the engine looks it up in the cloud by name and takes ownership of |
| 122 | what it finds. But v1 and v2 derive default names differently, so |
| 123 | without pinning, v2 would look for names that don't exist. |
| 124 | |
| 125 | When you didn't set `name` explicitly, v1 derived it as: |
| 126 | |
| 127 | ``` |
| 128 | {app}-{id}-{stage} |
| 129 | ``` |
| 130 | |
| 131 | - `{app}` — the name you passed to `await alchemy("my-app")` |
| 132 | - `{id}` — the resource's logical ID, e.g. `await Bucket("bucket")` |
| 133 | - `{stage}` — the `--stage` value; defaults to `$ALCHEMY_STAGE`, then |
| 134 | your OS username (`$USER`), then `dev` |
| 135 | |
| 136 | So the v1 example at the top of this page, deployed with |
| 137 | `--stage prod`, created a bucket named `my-app-bucket-prod` and a |
| 138 | worker named `my-app-worker-prod`. (Resources created inside a nested |
| 139 | scope get the scope names between `{app}` and `{id}`, and Worker names |
| 140 | are lowercased.) |
| 141 | |
| 142 | Set the `name` prop on each v2 resource to that **exact** name: |
| 143 | |
| 144 | ```typescript |
| 145 | export const Bucket = Cloudflare.R2.Bucket("Bucket", { |
| 146 | name: "my-app-bucket-prod", // v1's {app}-{id}-{stage} |
| 147 | }); |
| 148 | |
| 149 | export const Worker = Cloudflare.Worker("Worker", { |
| 150 | main: "./src/worker.ts", |
| 151 | name: "my-app-worker-prod", // v1's {app}-{id}-{stage} |
| 152 | env: { Bucket }, |
| 153 | }); |
| 154 | ``` |
| 155 | |
| 156 | If you did set `name` in v1, carry that value over unchanged. When in |
| 157 | doubt, confirm the live names in your v1 state (`.alchemy/` by |
| 158 | default) or the Cloudflare dashboard. |
| 159 | |
| 160 | Because v1 baked the stage into the name, each stage has different |
| 161 | physical names. If you deploy several stages from the same config, |
| 162 | compute the name from the stage (e.g. an environment variable your CI |
| 163 | sets) instead of hardcoding one stage's suffix. |
| 164 | |
| 165 | ## Step 4: Deploy with `--adopt` |
| 166 | |
| 167 | ```sh |
| 168 | alchemy deploy --adopt |
| 169 | ``` |
| 170 | |
| 171 | For each resource, the engine finds the existing one by its physical |
| 172 | name and adopts it into v2 state, then reconciles it to the declared |
| 173 | config. `--adopt` tells the engine to take over resources it cannot |
| 174 | prove it owns — which is exactly the situation for everything v1 |
| 175 | created. See [Adopting Resources](/cli/adopting-resources) for the |
| 176 | full ownership rules. |
| 177 | |
| 178 | :::tip |
| 179 | Try the migration on a non-production stage first: deploy your v1 |
| 180 | project to a fresh stage, then run the v2 adoption against it and |
| 181 | verify everything reconciles cleanly before touching prod. |
| 182 | ::: |
| 183 | |
| 184 | :::caution |
| 185 | After adopting, **do not run `destroy` in your v1 project** — its |
| 186 | state still points at the same physical resources, so destroying the |
| 187 | v1 stack would delete the resources v2 now manages. Delete the v1 |
| 188 | state files (the `.alchemy/` directory, or your remote state store |
| 189 | entries) instead. |
| 190 | ::: |
| 191 | |
| 192 | From here on, deploys are normal: |
| 193 | |
| 194 | ```sh |
| 195 | alchemy deploy |
| 196 | ``` |
| 197 | |
| 198 | ## (Optional) Adopt Effect for runtime code |
| 199 | |
| 200 | When you're ready, you can switch to Effect-native Workers. This |
| 201 | gives you typed errors, composable retries, and Effect's `HttpServer` |
| 202 | integration. |
| 203 | |
| 204 | Instead of declaring `env` bindings on the resource props, you bind |
| 205 | resources in the Worker's Construction phase using `yield*`: |
| 206 | |
| 207 | ```diff lang="typescript" |
| 208 | // src/worker.ts |
| 209 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 210 | import * as Effect from "effect/Effect"; |
| 211 | +import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 212 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 213 | import { Bucket } from "./bucket.ts"; |
| 214 | |
| 215 | -export default { |
| 216 | - async fetch(request: Request, env: WorkerEnv) { |
| 217 | - const object = await env.Bucket.get("key"); |
| 218 | - return new Response(object?.body ?? null); |
| 219 | - }, |
| 220 | -}; |
| 221 | +export default Cloudflare.Worker("Worker", |
| 222 | + { main: import.meta.url }, |
| 223 | + Effect.gen(function* () { |
| 224 | + const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket); |
| 225 | + |
| 226 | + return { |
| 227 | + fetch: Effect.gen(function* () { |
| 228 | + const request = yield* HttpServerRequest; |
| 229 | + const key = request.url.split("/").pop()!; |
| 230 | + const object = yield* bucket.get(key); |
| 231 | + return object |
| 232 | + ? HttpServerResponse.text(yield* object.text()) |
| 233 | + : HttpServerResponse.text("Not found", { status: 404 }); |
| 234 | + }), |
| 235 | + }; |
| 236 | + }), |
| 237 | +); |
| 238 | ``` |
| 239 | |
| 240 | The Worker resource declaration moves from `alchemy.run.ts` into the |
| 241 | Worker file itself (using `import.meta.url` as the `main`), and the |
| 242 | Stack just `yield*`-s the imported Worker: |
| 243 | |
| 244 | ```diff lang="typescript" |
| 245 | // alchemy.run.ts |
| 246 | import * as Alchemy from "alchemy"; |
| 247 | +import * as Cloudflare from "alchemy/Cloudflare"; |
| 248 | import * as Effect from "effect/Effect"; |
| 249 | +import Worker from "./src/worker.ts"; |
| 250 | +import { Bucket } from "./src/bucket.ts"; |
| 251 | |
| 252 | -export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>; |
| 253 | - |
| 254 | -export const Worker = Cloudflare.Worker("Worker", { |
| 255 | - main: "./src/worker.ts", |
| 256 | - env: { Bucket }, |
| 257 | -}); |
| 258 | |
| 259 | export default Alchemy.Stack( |
| 260 | "MyApp", |
| 261 | { providers: Cloudflare.providers(), state: Cloudflare.state() }, |
| 262 | Effect.gen(function* () { |
| 263 | + const bucket = yield* Bucket; |
| 264 | const worker = yield* Worker; |
| 265 | return { url: worker.url }; |
| 266 | }), |
| 267 | ); |
| 268 | ``` |
| 269 | |
| 270 | ## Summary |
| 271 | |
| 272 | | | v1 (async) | v2 (async style) | v2 (Effect style) | |
| 273 | | ------------ | ----------------------------- | ----------------------------- | ---------------------------------- | |
| 274 | | Stack | `await alchemy("name")` | `Alchemy.Stack("name", ...)` | `Alchemy.Stack("name", ...)` | |
| 275 | | Resources | `await Bucket(...)` | `Cloudflare.R2.Bucket(...)` | `Cloudflare.R2.Bucket(...)` | |
| 276 | | Worker entry | `entrypoint` | `main` | `main: import.meta.url` | |
| 277 | | Bindings | `bindings: { KEY: resource }` | `env: { Key: resource }` | `yield* Binding(ref)` | |
| 278 | | Runtime code | `async fetch(req, env)` | `async fetch(req, env)` | `Effect.gen(function* () { ... })` | |
| 279 | | Lifecycle | `await app.finalize()` | automatic | automatic | |
| 280 | | Type safety | runtime errors | typed `env` via `InferEnv` | full Effect type system | |
| 281 | |
| 282 | ## Where next |
| 283 | |
| 284 | - Tutorial — build a full app on v2 from scratch, on |
| 285 | [Cloudflare](/cloudflare/tutorial/part-1) or [AWS](/aws/tutorial/part-1) |
| 286 | - [Adopting Resources](/cli/adopting-resources) — the full ownership |
| 287 | rules behind `--adopt` |
| 288 | - [Bindings](/infrastructure-as-effects/binding) — what replaced v1 `bindings:` |
| 289 | - [Cloudflare](/cloudflare) — the Cloudflare provider hub |
| 290 | - [AWS](/aws) — the AWS provider hub |
| 291 | - [v1.alchemy.run](https://v1.alchemy.run) — the v1 documentation |