| 1 | --- |
| 2 | title: 2.0.0-beta.59 - One Namespace, One Binding |
| 3 | date: 2026-06-27T22:00:00Z |
| 4 | category: release |
| 5 | excerpt: v2.0.0-beta.59 is a naming alignment release. Every Cloudflare service is now a proper namespace, every binding follows one {Verb}{Resource} convention across AWS and Cloudflare, and Binding.Policy is gone — collapsed into a single runtime-guarded Layer. |
| 6 | --- |
| 7 | |
| 8 | beta.59 is the alignment release beta.58 set up. It is |
| 9 | almost entirely renames: every Cloudflare service is now a real |
| 10 | namespace, every binding across AWS **and** Cloudflare follows one |
| 11 | `{Verb}{Resource}` convention, and the two-layer `Binding.Policy` |
| 12 | split is gone — folded into a single runtime-guarded `Layer` |
| 13 | ([#690](https://github.com/alchemy-run/alchemy/pull/690)). |
| 14 | |
| 15 | There's almost no new behavior here. It's a coordinated rename across |
| 16 | ~900 symbols and ~1200 files so the API reads the same everywhere — |
| 17 | you'll find-and-replace your way through it. |
| 18 | |
| 19 | :::caution |
| 20 | **Breaking changes** — this release is mostly breaking renames. |
| 21 | Migration for each is below. |
| 22 | |
| 23 | - **Every Cloudflare service is namespaced** — `Cloudflare.KVNamespace` |
| 24 | → `Cloudflare.KV.Namespace`, `Cloudflare.Worker` → |
| 25 | `Cloudflare.Workers.Worker` (common ones are re-exported at the root). |
| 26 | - **`AiGateway` / `AiSearch` / `AiSecurity` collapse into `AI`** — |
| 27 | `Cloudflare.AiGateway` → `Cloudflare.AI.Gateway`. |
| 28 | - **Bindings are now directly callable, namespaced, and |
| 29 | least-privilege** — `R2Bucket.bind(b)` → `Cloudflare.R2.ReadBucket(b)`, |
| 30 | `DynamoDB.GetItem.bind(t)` → `AWS.DynamoDB.GetItem(t)`. |
| 31 | - **`Binding.Policy` is removed** — the deploy-time wiring moves behind |
| 32 | an `if (!globalThis.__ALCHEMY_RUNTIME__)` guard in a single Layer. |
| 33 | - **Impl layers are renamed** — `*Live` → `*Binding` (Cloudflare native |
| 34 | worker binding) or `*Http` (HTTP-client binding, all AWS + the |
| 35 | Cloudflare HTTP impls). |
| 36 | - **Event sources are `consume<Resource><Event>(resource, handler)`** — |
| 37 | `Queues.messages(q).subscribe(fn)` → `Queues.consumeQueueMessages(q, fn)`. |
| 38 | - **Worker-only bindings take a string id** — `Images({ name: "X" })` → |
| 39 | `Images("X")`; their native layers drop `*Live`/`*Layer` |
| 40 | (`BrowserBindingLive` → `BrowserBinding`). |
| 41 | - **`Artifacts.Store` → `Artifacts.Namespace`** — and `ReadWriteStore` → |
| 42 | `ReadWriteNamespace` (plus `Read*` / `Write*`). |
| 43 | ::: |
| 44 | |
| 45 | ## Every Cloudflare service is a namespace |
| 46 | |
| 47 | `Cloudflare/index.ts` used to be a flat `export *` of every symbol, so |
| 48 | everything lived directly on `Cloudflare.*` with a redundant prefix |
| 49 | baked into each name (`Cloudflare.KVNamespace`, `Cloudflare.R2Bucket`, |
| 50 | `Cloudflare.D1Database`). |
| 51 | |
| 52 | ### Resources move under their service |
| 53 | |
| 54 | It now mirrors the AWS standard — one `export * as <Service>` per |
| 55 | service, with the prefix stripped from the symbol: |
| 56 | |
| 57 | ```diff lang="typescript" |
| 58 | - const kv = yield* Cloudflare.KVNamespace("Sessions"); |
| 59 | - const bucket = yield* Cloudflare.R2Bucket("Assets"); |
| 60 | - const db = yield* Cloudflare.D1Database("App"); |
| 61 | + const kv = yield* Cloudflare.KV.Namespace("Sessions"); |
| 62 | + const bucket = yield* Cloudflare.R2.Bucket("Assets"); |
| 63 | + const db = yield* Cloudflare.D1.Database("App"); |
| 64 | ``` |
| 65 | |
| 66 | The same applies to `Worker`, which now lives under `Cloudflare.Workers`: |
| 67 | |
| 68 | ```diff lang="typescript" |
| 69 | - export default Cloudflare.Worker("Worker", { /* ... */ }); |
| 70 | + export default Cloudflare.Workers.Worker("Worker", { /* ... */ }); |
| 71 | ``` |
| 72 | |
| 73 | ### Common building blocks stay at the root |
| 74 | |
| 75 | The common building blocks are re-exported at the root so the everyday |
| 76 | path stays short — these all still resolve, no namespace required: |
| 77 | |
| 78 | ```typescript |
| 79 | yield* Cloudflare.Worker("Worker", { /* ... */ }); |
| 80 | yield* Cloudflare.Container("Sandbox", { /* ... */ }); |
| 81 | yield* Cloudflare.DurableObject("Counter", { /* ... */ }); |
| 82 | yield* Cloudflare.Workflow("Job", { /* ... */ }); |
| 83 | yield* Cloudflare.RateLimit("Limiter", { /* ... */ }); |
| 84 | Cloudflare.cron("0 * * * *", handler); |
| 85 | ``` |
| 86 | |
| 87 | Everything else is reached through its namespace. |
| 88 | |
| 89 | ### `DurableObjectNamespace` → `DurableObject` |
| 90 | |
| 91 | The Durable Object base class loses its `Namespace` suffix — it's a |
| 92 | `DurableObject` now, full stop: |
| 93 | |
| 94 | ```diff lang="typescript" |
| 95 | - export class Counter extends Cloudflare.DurableObjectNamespace<Counter>()( |
| 96 | + export class Counter extends Cloudflare.DurableObject<Counter>()( |
| 97 | "Counter", |
| 98 | /* ... */ |
| 99 | ) {} |
| 100 | ``` |
| 101 | |
| 102 | ### Folder reshuffles and the new `WebSocket` type |
| 103 | |
| 104 | A few folders were reshaped to fit: `Container/` → `Containers/`, |
| 105 | `Queue` → `Queues`, and workflow code moved into `Workflows/`. There's |
| 106 | also a new `Cloudflare.WebSocket` **type** for typing hibernatable |
| 107 | WebSocket handlers. |
| 108 | |
| 109 | ## `Ai*` becomes one `AI` namespace |
| 110 | |
| 111 | The three separate AI services — `AiGateway`, `AiSearch`, and |
| 112 | `AiSecurity` — were really one product surface split across three |
| 113 | folders. They collapse into a single `Cloudflare.AI` namespace: |
| 114 | |
| 115 | ```diff lang="typescript" |
| 116 | - const gateway = yield* Cloudflare.AiGateway("Gateway"); |
| 117 | - const search = yield* Cloudflare.AiSearch("Search"); |
| 118 | + const gateway = yield* Cloudflare.AI.Gateway("Gateway"); |
| 119 | + const search = yield* Cloudflare.AI.Search("Search"); |
| 120 | ``` |
| 121 | |
| 122 | Symbols drop the redundant `Ai` prefix in the process — |
| 123 | `AiGatewayDataset` → `AI.Dataset`, `AiSearchInstance` → |
| 124 | `AI.SearchInstance`, `AiSecuritySettings` → `AI.SecuritySettings`. |
| 125 | |
| 126 | ## One binding convention, everywhere |
| 127 | |
| 128 | This is the heart of the release. AWS and Cloudflare bindings now read |
| 129 | identically. A capability is the noun, prefixed by the verb (the |
| 130 | access level it grants), and you call it **directly** — the `.bind` |
| 131 | method is gone: |
| 132 | |
| 133 | ```diff lang="typescript" |
| 134 | - const get = yield* AWS.DynamoDB.GetItem.bind(table); |
| 135 | - const bucket = yield* Cloudflare.R2Bucket.bind(Bucket); |
| 136 | + const get = yield* AWS.DynamoDB.GetItem(table); |
| 137 | + const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket); |
| 138 | ``` |
| 139 | |
| 140 | Where the underlying API distinguishes access levels, the capability is |
| 141 | split into `Read` / `Write` / `ReadWrite` so you request least |
| 142 | privilege — and the verb leads the name: |
| 143 | |
| 144 | ```typescript |
| 145 | yield* Cloudflare.KV.ReadNamespace(Sessions); // get / list |
| 146 | yield* Cloudflare.KV.WriteNamespace(Sessions); // put / delete |
| 147 | yield* Cloudflare.KV.ReadWriteNamespace(Sessions); // both |
| 148 | ``` |
| 149 | |
| 150 | ### `*Binding` vs `*Http` |
| 151 | |
| 152 | Each capability ships two interchangeable implementation Layers, and |
| 153 | the suffix tells you which transport it uses: |
| 154 | |
| 155 | - **`*Binding`** — a native **Cloudflare Worker binding** (`env.BUCKET`). |
| 156 | - **`*Http`** — a **scoped HTTP client** that works anywhere, including |
| 157 | inside a Container process that has no native binding. |
| 158 | |
| 159 | ```typescript |
| 160 | // native Worker binding |
| 161 | .pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)) |
| 162 | |
| 163 | // scoped HTTP token — same client, runs anywhere |
| 164 | .pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketHttp)) |
| 165 | ``` |
| 166 | |
| 167 | The split lands on AWS too, where the only transport is HTTP: every AWS |
| 168 | impl layer was renamed `*Live` → `*Http` (`DynamoDB.GetItemHttp`, |
| 169 | `SQS.SendMessageHttp`, `S3.GetObjectHttp`), because an AWS runtime calls |
| 170 | the distilled HTTP API authenticated by the Lambda's IAM role. |
| 171 | `Binding` is a Cloudflare native-worker concept — AWS never had one. |
| 172 | |
| 173 | The two transports do the same thing in their respective worlds: |
| 174 | |
| 175 | - **AWS `*Http`** — emits a least-privilege **IAM policy statement** on |
| 176 | the Lambda's role, then calls the distilled HTTP API with it. |
| 177 | - **Cloudflare `*Http`** — mints a scoped **`AccountApiToken`** with |
| 178 | only the permission groups that access level needs, binds the token's |
| 179 | value into the environment as a secret, and calls Cloudflare's REST |
| 180 | API through distilled. |
| 181 | |
| 182 | The client interface is **identical** regardless of which Layer you |
| 183 | provide — a Worker reading a bucket over its native binding and a |
| 184 | container reading the same bucket over a token are the same code. |
| 185 | |
| 186 | ## More Cloudflare `*Http` bindings, one mechanism |
| 187 | |
| 188 | A native Worker binding only exists inside a Worker. The moment you |
| 189 | need the same capability somewhere with no native binding — a Container |
| 190 | process, a Durable Object reaching a foreign resource, a script running |
| 191 | outside the edge — you need the HTTP transport. beta.59 fills that |
| 192 | gap out: R2, KV, and Queues already had `*Http` impls, and DNS joins |
| 193 | them this release, all sharing one mechanism and the verb-first naming. |
| 194 | |
| 195 | ```typescript |
| 196 | yield* Cloudflare.DNS.ReadDns(Zone); // list / get records |
| 197 | yield* Cloudflare.DNS.WriteDns(Zone); // create / update / delete |
| 198 | yield* Cloudflare.DNS.ReadWriteDns(Zone); // both |
| 199 | ``` |
| 200 | |
| 201 | ### How it works: mint a scoped `AccountApiToken` |
| 202 | |
| 203 | An `*Http` Layer can't lean on `env.MY_BINDING`, so it provisions its |
| 204 | own credential. At deploy time it mints an `AccountApiToken` scoped to |
| 205 | **only** the permission groups that access level needs, and binds that |
| 206 | token's value into the host as a secret — all behind the |
| 207 | `__ALCHEMY_RUNTIME__` guard so it's a deploy-time-only step: |
| 208 | |
| 209 | ```typescript |
| 210 | const token = yield* AccountApiToken(`${self.LogicalId}Token`); |
| 211 | |
| 212 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 213 | yield* token.bind`${resource.LogicalId}`({ |
| 214 | policies: [ |
| 215 | { |
| 216 | effect: "allow", |
| 217 | permissionGroups, // e.g. ["Workers KV Storage Read"] for ReadNamespace |
| 218 | resources: { [`com.cloudflare.api.account.${accountId}`]: "*" }, |
| 219 | }, |
| 220 | ], |
| 221 | }); |
| 222 | } |
| 223 | ``` |
| 224 | |
| 225 | The permission groups are what make this least-privilege: a |
| 226 | `ReadNamespace` mints a read-only KV token, a `WriteNamespace` a |
| 227 | write-only one. At runtime the client reads the token's `value` and |
| 228 | `accountId` from its secrets and calls Cloudflare's REST API through |
| 229 | distilled — no account-wide API key, no manual token plumbing, and a |
| 230 | different scoped token per capability. |
| 231 | |
| 232 | ## `Binding.Policy` is gone |
| 233 | |
| 234 | Bindings used to be two pieces: a runtime `Binding.Service` plus a |
| 235 | separate deploy-time `Binding.Policy` (with its own `*PolicyLive` |
| 236 | layer) that registered the IAM / native binding. beta.59 collapses them |
| 237 | into a single `Binding.Service` whose setup Effect does the deploy-time |
| 238 | wiring inline, guarded so it only runs at plan time: |
| 239 | |
| 240 | ```typescript |
| 241 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 242 | // deploy-time only: register IAM / native binding / env on the host |
| 243 | yield* host.bind`${resource}`(/* … */); |
| 244 | } |
| 245 | // always: return the typed runtime client |
| 246 | ``` |
| 247 | |
| 248 | At plan time the guard is open and `bind` records what the function |
| 249 | needs. At runtime (`__ALCHEMY_RUNTIME__` is set) the branch is skipped |
| 250 | and the same call resolves to just the lightweight client, so the |
| 251 | runtime bundle stays small. This is the exact pattern the capability |
| 252 | bindings already used in beta.58 — now **every** binding, including |
| 253 | event sources, follows it. `Binding.Policy`, `Binding.ServiceClass`, |
| 254 | and the `Policy` / `PolicyLike` types are removed from the core. |
| 255 | |
| 256 | ## Worker-only bindings take a string id |
| 257 | |
| 258 | The Cloudflare bindings with no backing resource — `Images`, `Browser`, |
| 259 | `VersionMetadata`, `RateLimit` — now take a **string id first**, like a |
| 260 | resource logical id, instead of a `{ name }` object: |
| 261 | |
| 262 | ```diff lang="typescript" |
| 263 | - const images = yield* Cloudflare.Images.Images({ name: "PIPELINE" }); |
| 264 | + const images = yield* Cloudflare.Images.Images("PIPELINE"); |
| 265 | ``` |
| 266 | |
| 267 | Their native layers drop the `*Live` / `*Layer` suffix to match the |
| 268 | `*Binding` convention: |
| 269 | |
| 270 | ```diff lang="typescript" |
| 271 | - .pipe(Effect.provide(Cloudflare.Workers.BrowserBindingLive)) |
| 272 | + .pipe(Effect.provide(Cloudflare.Workers.BrowserBinding)) |
| 273 | ``` |
| 274 | |
| 275 | ### `Artifacts.Store` → `Artifacts.Namespace` |
| 276 | |
| 277 | The Artifacts namespace is renamed `Store` → `Namespace`. Access stays |
| 278 | explicit through `ReadNamespace` / `WriteNamespace` / `ReadWriteNamespace`: |
| 279 | |
| 280 | ```diff lang="typescript" |
| 281 | - const Repos = Cloudflare.Artifacts.Store("Repos"); |
| 282 | - const repos = yield* Cloudflare.Artifacts.ReadWriteStore(Repos); |
| 283 | + const Repos = Cloudflare.Artifacts.Namespace("Repos"); |
| 284 | + const repos = yield* Cloudflare.Artifacts.ReadWriteNamespace(Repos); |
| 285 | ``` |
| 286 | |
| 287 | ## Event sources: `consume<Resource><Event>` |
| 288 | |
| 289 | Event sources are bindings too, so they got the same treatment. The old |
| 290 | `X(resource, props?).subscribe(handler)` shape is replaced by a single |
| 291 | verb-prefixed, resource-and-event-named callable that takes the handler |
| 292 | as its last argument: |
| 293 | |
| 294 | ```diff lang="typescript" |
| 295 | - yield* Cloudflare.Queues.messages(inbound).subscribe((records) => |
| 296 | - records.pipe(Stream.runForEach(Console.log)), |
| 297 | - ); |
| 298 | + yield* Cloudflare.Queues.consumeQueueMessages(inbound, (records) => |
| 299 | + records.pipe(Stream.runForEach(Console.log)), |
| 300 | + ); |
| 301 | ``` |
| 302 | |
| 303 | Every event source now reads the same way — the name tells you both the |
| 304 | resource and the element it yields: |
| 305 | |
| 306 | | Before | After | |
| 307 | | ------------------------------ | ----------------------------------- | |
| 308 | | `SQS.messages(q).subscribe` | `SQS.consumeQueueMessages(q, fn)` | |
| 309 | | `S3.notifications(b).subscribe` | `S3.consumeBucketEvents(b, fn)` | |
| 310 | | `SNS.notifications(t).subscribe` | `SNS.consumeTopicNotifications(t, fn)` | |
| 311 | | `Kinesis.records(s).process` | `Kinesis.consumeStreamRecords(s, fn)` | |
| 312 | | `DynamoDB.stream(t).process` | `DynamoDB.consumeTableChanges(t, fn)` | |
| 313 | | `GitHub.events(r).subscribe` | `GitHub.consumeRepositoryEvents(r, fn)` | |
| 314 | |
| 315 | Dropping `.subscribe` also drops the handler's runtime requirements at |
| 316 | the source boundary, so explicit `Body` type arguments no longer break |
| 317 | inference. |
| 318 | |
| 319 | ## Also in this release |
| 320 | |
| 321 | Two fixes shipped alongside the rename: |
| 322 | |
| 323 | - **`deleteFirst` is honored on replace** |
| 324 | ([#692](https://github.com/alchemy-run/alchemy/pull/692)) — a |
| 325 | replacement defaults to create-first (stand up the new generation, |
| 326 | then tear down the old), but some resources can't coexist with their |
| 327 | previous self (a fixed physical name, a singleton). `deleteFirst` is |
| 328 | meant to flip that ordering, and the engine now actually does: it |
| 329 | deletes the previous generation **before** creating the replacement |
| 330 | instead of leaking an old chain into the next phase. Thanks |
| 331 | **John Royal**. |
| 332 | - **Worker diff tolerates legacy custom-domain state** |
| 333 | ([#694](https://github.com/alchemy-run/alchemy/pull/694)) — |
| 334 | Alchemy ≤ beta.44 stored each Worker custom domain as a |
| 335 | `{ id, hostname, zoneId }` object; beta.45+ stores |
| 336 | `https://<hostname>` strings and the diff path calls string methods |
| 337 | on each entry. Older state now gets coerced back to strings so a |
| 338 | Worker deploy no longer throws `u.endsWith is not a function` |
| 339 | ([#546](https://github.com/alchemy-run/alchemy/issues/546)). |
| 340 | Thanks **Nicolas Fléron**. |
| 341 | |
| 342 | ## Where to go next |
| 343 | |
| 344 | - [Binding](/infrastructure-as-effects/binding) |
| 345 | - [Plantime and Runtime › The `__ALCHEMY_RUNTIME__` guard](/infrastructure-as-effects/phases#the-__alchemy_runtime__-guard) |
| 346 | - [Queue Consumer](/cloudflare/messaging/queues) |
| 347 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta59) |
| 348 | - [Compare v2.0.0-beta.58 → v2.0.0-beta.59](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.58...v2.0.0-beta.59) |