| 1 | --- |
| 2 | title: 2.0.0-beta.58 - Platform Ergonomics |
| 3 | date: 2026-06-24T22:00:00Z |
| 4 | category: release |
| 5 | excerpt: v2.0.0-beta.58 improves the ergonomics of Layers that contain Resources, so you can bundle infrastructure into a reusable service — like an agent tool — and provide it to a Worker, Container, or Durable Object. |
| 6 | --- |
| 7 | |
| 8 | beta.58 improves the ergonomics of Layers that contain Resources, so |
| 9 | you can bundle infrastructure into a reusable service — like an agent |
| 10 | tool — and provide it to a Worker, Container, or Durable Object. |
| 11 | |
| 12 | A few changes get us there. First, a Platform tag (e.g. a Worker or |
| 13 | Container) becomes **pure identity**: its props and its runtime |
| 14 | implementation move out of the tag and into a Layer. |
| 15 | |
| 16 | ```diff lang="typescript" |
| 17 | - export class WorkerB extends Cloudflare.Worker<WorkerB>()( |
| 18 | - "WorkerB", |
| 19 | - { main: import.meta.filename }, // props on the tag |
| 20 | - ) {} |
| 21 | + export class WorkerB extends Cloudflare.Worker<WorkerB>()("WorkerB") {} |
| 22 | |
| 23 | + export default WorkerB.make( |
| 24 | + { main: import.meta.filename }, // props on the Layer |
| 25 | + Effect.gen(function* () { |
| 26 | + /* ... */ |
| 27 | + }), |
| 28 | + ); |
| 29 | ``` |
| 30 | |
| 31 | Also, `DurableObjectState` can now be yielded at the top level, |
| 32 | instead of having to be resolved inside the nested `Effect.gen`: |
| 33 | |
| 34 | ```diff lang="typescript" |
| 35 | Effect.gen(function* () { |
| 36 | + const state = yield* Cloudflare.DurableObjectState; |
| 37 | return Effect.gen(function* () { |
| 38 | - const state = yield* Cloudflare.DurableObjectState; |
| 39 | let count = (yield* state.storage.get<number>("count")) ?? 0; |
| 40 | /* ... */ |
| 41 | }); |
| 42 | }), |
| 43 | ``` |
| 44 | |
| 45 | Initialization is where resources, bindings, and dependencies are |
| 46 | declared, so a Layer can contain infrastructure and be provided to any |
| 47 | Worker, Durable Object, or Container without changing its shape. |
| 48 | |
| 49 | :::caution |
| 50 | **Breaking changes** — migration for each is below. |
| 51 | |
| 52 | - **Props move from the tag to `.make()`** on the Layer form of |
| 53 | `Worker` / `Container`. |
| 54 | - **`DurableObjectState`** is resolved in the **outer** init Effect. |
| 55 | - **Containers** replace `Cloudflare.Container.bind` / |
| 56 | `Cloudflare.start` with `yield*`-the-class + `Cloudflare.layerContainer`, |
| 57 | and `yield*`-ing the tag now returns a **running container**, not the |
| 58 | `ContainerApplication` resource. |
| 59 | - **Resource bindings** replace a resource's single `.bind` with |
| 60 | namespaced, least-privilege capabilities (`Cloudflare.R2.ReadWriteBucket`). |
| 61 | - **The `Build` module is removed** in favor of a new `Command` |
| 62 | module (`Build.Command` → `Command.Build`, `Build.DevServer` → |
| 63 | `Command.Dev`). |
| 64 | - **`DynamicWorkerLoader` is renamed** to `WorkerLoader`. |
| 65 | ::: |
| 66 | |
| 67 | ## One tag, many implementations |
| 68 | |
| 69 | Because the tag is pure identity, one tag can have **many** |
| 70 | implementations. `.make(propsA, implA)` and `.make(propsB, implB)` |
| 71 | produce two Layers for the same tag — swap a real implementation for |
| 72 | a stub in tests, or vary config per stage: |
| 73 | |
| 74 | ```typescript |
| 75 | export const SandboxProd = Sandbox.make( |
| 76 | { main: import.meta.filename, instanceType: "standard-1" }, |
| 77 | realImpl, |
| 78 | ); |
| 79 | |
| 80 | export const SandboxTest = Sandbox.make( |
| 81 | { main: import.meta.filename, instanceType: "dev" }, |
| 82 | fakeImpl, |
| 83 | ); |
| 84 | ``` |
| 85 | |
| 86 | The inline form is unchanged — pass the implementation as a third |
| 87 | argument and props stay inline: |
| 88 | |
| 89 | ```typescript |
| 90 | export default Cloudflare.Worker( |
| 91 | "Worker", |
| 92 | { main: import.meta.filename }, // still inline here |
| 93 | Effect.gen(function* () { |
| 94 | /* ... */ |
| 95 | }), |
| 96 | ); |
| 97 | ``` |
| 98 | |
| 99 | ## A tool on Durable Object storage |
| 100 | |
| 101 | Resolving `DurableObjectState` at init is what lets you capture the |
| 102 | DO's storage in a **Layer** and expose it as a service — the shape a |
| 103 | `Sql` tool takes. Resolve `state` (and anything else, like an R2 |
| 104 | bucket) at init, return the runtime methods: |
| 105 | |
| 106 | ```typescript |
| 107 | class Archive extends Context.Service<Archive, { |
| 108 | save: (key: string, body: string) => |
| 109 | Effect.Effect<void, never, Alchemy.RuntimeContext>; // <- RuntimeContext-colored |
| 110 | }>()("Archive") {} |
| 111 | |
| 112 | const ArchiveLive = Layer.effect( |
| 113 | Archive, |
| 114 | Effect.gen(function* () { |
| 115 | // Resolve both references at init. |
| 116 | const state = yield* Cloudflare.DurableObjectState; |
| 117 | const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket); |
| 118 | |
| 119 | return Archive.of({ |
| 120 | // The body runs at runtime — both calls are RuntimeContext-colored. |
| 121 | save: (key, body) => |
| 122 | Effect.gen(function* () { |
| 123 | yield* bucket.put(key, body); |
| 124 | yield* state.storage.put(`saved:${key}`, Date.now()); |
| 125 | }), |
| 126 | }); |
| 127 | }), |
| 128 | ); |
| 129 | ``` |
| 130 | |
| 131 | Provide `ArchiveLive` on the DO's init and `yield* Archive` from the |
| 132 | inner Effect: |
| 133 | |
| 134 | ```typescript |
| 135 | export class Doc extends Cloudflare.DurableObjectNamespace<Doc>()( |
| 136 | "Doc", |
| 137 | Effect.gen(function* () { |
| 138 | const archive = yield* Archive; // resolved at init |
| 139 | return Effect.gen(function* () { |
| 140 | return { |
| 141 | save: (key: string, body: string) => archive.save(key, body), |
| 142 | }; |
| 143 | }); |
| 144 | }).pipe(Effect.provide(ArchiveLive)), |
| 145 | ) {} |
| 146 | ``` |
| 147 | |
| 148 | Neither `DurableObjectState` nor R2 leaks into `Archive`'s public |
| 149 | type — consumers see only `RuntimeContext`, so the service stays |
| 150 | cloud-agnostic. |
| 151 | |
| 152 | ## A tool on a running Container |
| 153 | |
| 154 | Containers used to be a three-step dance — `bind`, then `start`, then |
| 155 | use. beta.58 collapses that: `yield*` the Container tag to get the |
| 156 | **running container instance** (not the `ContainerApplication` |
| 157 | resource), and provide `Cloudflare.layerContainer(Class, options)` to |
| 158 | configure how it runs. |
| 159 | |
| 160 | ```diff lang="typescript" |
| 161 | Effect.gen(function* () { |
| 162 | - const sandbox = yield* Cloudflare.Container.bind(Sandbox); |
| 163 | + const sandbox = yield* Sandbox; |
| 164 | |
| 165 | return Effect.gen(function* () { |
| 166 | - const container = yield* Cloudflare.start(sandbox, { |
| 167 | - enableInternet: true, |
| 168 | - }); |
| 169 | return { |
| 170 | - exec: (cmd: string) => container.exec(cmd), |
| 171 | + exec: (cmd: string) => sandbox.exec(cmd), |
| 172 | }; |
| 173 | }); |
| 174 | - }), |
| 175 | + }).pipe( |
| 176 | + Effect.provide( |
| 177 | + Cloudflare.layerContainer(Sandbox, { enableInternet: true }), |
| 178 | + ), |
| 179 | + ), |
| 180 | ``` |
| 181 | |
| 182 | `layerContainer` binds, starts, and monitors the container, then |
| 183 | satisfies the `Sandbox` tag with the running instance. Because it's a |
| 184 | plain `Layer`, **any** Effect or Layer can `yield* Sandbox` and get a |
| 185 | running container — so a `Bash` tool is just a Layer that captures the |
| 186 | container and shells out to it: |
| 187 | |
| 188 | ```typescript |
| 189 | const Bash = Layer.effect( |
| 190 | BashTool, |
| 191 | Effect.gen(function* () { |
| 192 | const sandbox = yield* Sandbox; // running instance |
| 193 | return BashTool.of({ |
| 194 | run: (cmd: string) => sandbox.exec(cmd), |
| 195 | }); |
| 196 | }), |
| 197 | ); |
| 198 | ``` |
| 199 | |
| 200 | The `Bash` Layer just *requires* `Sandbox` — `layerContainer` is |
| 201 | provided once on the host's outermost init Effect, where it satisfies |
| 202 | both `Bash` and anything else that needs the container: |
| 203 | |
| 204 | ```typescript |
| 205 | .pipe( |
| 206 | Effect.provide( |
| 207 | Layer.provideMerge(Bash, Cloudflare.layerContainer(Sandbox)), |
| 208 | ), |
| 209 | ) |
| 210 | ``` |
| 211 | |
| 212 | External and remote `Container` images (built from a `context` / |
| 213 | `dockerfile`, or pulled from a registry) have no runtime shape and no |
| 214 | `.make()` — declare their props inline on the tag and register them |
| 215 | with `Cloudflare.layerContainer` from the hosting Durable Object. |
| 216 | |
| 217 | ## RPC works in Containers |
| 218 | |
| 219 | Container RPC always existed on paper, but was never properly |
| 220 | implemented or tested. As of beta.58 it actually works. A `Container` |
| 221 | declares a typed runtime shape just like a Worker or Durable Object, |
| 222 | and those methods are callable over **RPC** — the container exposes |
| 223 | Effect-returning methods alongside its `fetch` handler: |
| 224 | |
| 225 | ```typescript |
| 226 | export class MyContainer extends Cloudflare.Container< |
| 227 | MyContainer, |
| 228 | { |
| 229 | ping: () => Effect.Effect<string>; |
| 230 | readObject: (key: string) => Effect.Effect<string | null, never, RuntimeContext>; |
| 231 | } |
| 232 | >()("MyContainer") {} |
| 233 | |
| 234 | export default MyContainer.make( |
| 235 | { main: import.meta.filename, dockerfile: "FROM oven/bun:latest" }, |
| 236 | Effect.gen(function* () { |
| 237 | const bucket = yield* Cloudflare.R2.ReadWriteBucket(Storage); |
| 238 | return { |
| 239 | ping: () => Effect.succeed("pong"), |
| 240 | readObject: (key) => bucket.get(key).pipe(/* ... */), |
| 241 | fetch: Effect.gen(function* () { |
| 242 | /* ... */ |
| 243 | }), |
| 244 | }; |
| 245 | }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketHttp)), |
| 246 | ); |
| 247 | ``` |
| 248 | |
| 249 | From the host, `yield* MyContainer` gives the running container and |
| 250 | its methods round-trip through Cloudflare's RPC machinery as typed |
| 251 | Effects — no `fetch` plumbing, no manual serialization: |
| 252 | |
| 253 | ```typescript |
| 254 | const container = yield* MyContainer; |
| 255 | const pong = yield* container.ping(); // RPC |
| 256 | const body = yield* container.readObject("key"); // RPC |
| 257 | ``` |
| 258 | |
| 259 | Use `container.getTcpPort(port)` when you do need a raw `fetch` into a |
| 260 | server running inside the container process. |
| 261 | |
| 262 | ## `WorkerLoader` |
| 263 | |
| 264 | `DynamicWorkerLoader` is renamed to `WorkerLoader`, and `load(...)` |
| 265 | now returns an Effect, so you `yield*` it |
| 266 | ([#653](https://github.com/alchemy-run/alchemy/pull/653)): |
| 267 | |
| 268 | ```diff lang="typescript" |
| 269 | - const loader = yield* Cloudflare.DynamicWorkerLoader("Loader"); |
| 270 | - const worker = loader.load({ compatibilityDate, mainModule, modules }); |
| 271 | + const loader = yield* Cloudflare.WorkerLoader("Loader"); |
| 272 | + const worker = yield* loader.load({ compatibilityDate, mainModule, modules }); |
| 273 | ``` |
| 274 | |
| 275 | ## Bindings: least-privilege, native or HTTP |
| 276 | |
| 277 | Bindings move to a **namespaced convention**: the single `.bind` on a |
| 278 | resource is gone, replaced by a capability namespaced per **access |
| 279 | level**. |
| 280 | |
| 281 | ```diff lang="typescript" |
| 282 | - const bucket = yield* Cloudflare.R2Bucket.bind(Bucket); |
| 283 | + const bucket = yield* Cloudflare.R2.ReadBucket(Bucket); // get / head / list |
| 284 | + const bucket = yield* Cloudflare.R2.WriteBucket(Bucket); // put / delete / multipart |
| 285 | + const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket); // both |
| 286 | ``` |
| 287 | |
| 288 | Pick the least privilege you need, then provide an implementation |
| 289 | Layer: the native Worker binding, or a scoped HTTP API token. |
| 290 | |
| 291 | ```typescript |
| 292 | // native Worker binding (env.BUCKET) |
| 293 | .pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)) |
| 294 | |
| 295 | // scoped HTTP API token — same client, runs anywhere (e.g. inside |
| 296 | // a Container process that has no native binding) |
| 297 | .pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketHttp)) |
| 298 | ``` |
| 299 | |
| 300 | The client interface is **identical** regardless of which Layer you |
| 301 | provide. A Worker can read a bucket over its native binding while a |
| 302 | container process reads the same bucket over a token — same code, |
| 303 | different Layer. |
| 304 | |
| 305 | The `*Http` Layer is what makes the same client work where there's |
| 306 | no native binding — inside a **Container** process, for example. |
| 307 | Providing it mints a scoped `AccountApiToken` and binds a policy with |
| 308 | only the permission groups that access level needs: |
| 309 | |
| 310 | ```typescript |
| 311 | const token = yield* AccountApiToken(`${self.LogicalId}Token`); |
| 312 | yield* token.bind`${bucket.LogicalId}`({ |
| 313 | policies: [ |
| 314 | { |
| 315 | effect: "allow", |
| 316 | permissionGroups: options.permissionGroups, // e.g. R2 read-only |
| 317 | resources: { [`com.cloudflare.api.account.${accountId}`]: "*" }, |
| 318 | }, |
| 319 | ], |
| 320 | }); |
| 321 | ``` |
| 322 | |
| 323 | The token's `value` is injected into the host's **secrets** — |
| 324 | `secret_text` on a Worker, a secret on a Container. At runtime the |
| 325 | client reads it and calls R2's REST API. No account-wide key, no |
| 326 | manual token plumbing. |
| 327 | |
| 328 | The Read / Write / ReadWrite split and `*Binding` / `*Http` |
| 329 | implementations follow the same convention across KV, Queues, and the |
| 330 | other capability resources. This namespaced convention is the |
| 331 | direction for all bindings — the full migration across AWS, |
| 332 | Cloudflare, and the other providers lands in beta.59. |
| 333 | |
| 334 | ## `Command`: run shell commands as resources |
| 335 | |
| 336 | The `Build` module is gone, replaced by a unified `Command` module |
| 337 | that models shell commands as first-class resources sharing one |
| 338 | executor ([#673](https://github.com/alchemy-run/alchemy/pull/673)). |
| 339 | |
| 340 | `Command.Build` (was `Build.Command`) runs a command that produces an |
| 341 | output asset and tracks `outdir` in state. Inputs and outputs are |
| 342 | content-hashed (opt-in via `memo`) so an unchanged project skips the |
| 343 | rebuild: |
| 344 | |
| 345 | ```typescript |
| 346 | const build = yield* Command.Build("vite-build", { |
| 347 | command: "npm run build", |
| 348 | cwd: "./frontend", |
| 349 | outdir: "dist", |
| 350 | }); |
| 351 | yield* Console.log(build.outdir); // path to dist, relative to process.cwd() |
| 352 | ``` |
| 353 | |
| 354 | `Command.Exec` is new. It runs a command purely for its side effects — |
| 355 | migrations, codegen, seeding — with no output contract: it succeeds as |
| 356 | long as the command exits `0`. `memo` re-runs it only when its inputs |
| 357 | change: |
| 358 | |
| 359 | ```typescript |
| 360 | yield* Command.Exec("migrate", { |
| 361 | command: "npm run db:migrate", |
| 362 | cwd: "./packages/api", |
| 363 | }); |
| 364 | ``` |
| 365 | |
| 366 | `Command.Dev` (was `Build.DevServer`) is a long-lived dev server scoped |
| 367 | to the stack instance. It runs in the dev sidecar so it survives |
| 368 | user-code HMR, and exposes the first `http(s)://` URL it prints as its |
| 369 | `url` output. It's a no-op during `alchemy deploy`: |
| 370 | |
| 371 | ```typescript |
| 372 | const dev = yield* Command.Dev("Frontend", { |
| 373 | command: "npm run dev", |
| 374 | }); |
| 375 | yield* Console.log(dev.url); // e.g. "http://localhost:5173" |
| 376 | ``` |
| 377 | |
| 378 | `StaticSite` (AWS + Cloudflare) and `Cloudflare.Workers.Worker` now |
| 379 | build and dev-serve through `Command`. Resource type IDs were |
| 380 | renamed, so existing state referencing `Build.Command` / |
| 381 | `Build.DevServer` won't match, and `Command.Build`'s output hash |
| 382 | changed from `{ outdir, hash }` to `{ outdir, hash: { input, output } }`. |
| 383 | |
| 384 | ## Where to go next |
| 385 | |
| 386 | - [Run a Container](/cloudflare/compute/run-a-container) |
| 387 | - [Add a Durable Object](/cloudflare/compute/durable-objects) |
| 388 | - [Cross-Worker Durable Objects](/cloudflare/compute/cross-worker-durable-object) |
| 389 | - [Runtime as a colored function](/infrastructure-as-effects/layers#the-types-hold-the-boundary) |
| 390 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta58) |
| 391 | - [Compare v2.0.0-beta.57 → v2.0.0-beta.58](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.57...v2.0.0-beta.58) |