1---
2title: 2.0.0-beta.59 - One Namespace, One Binding
3date: 2026-06-27T22:00:00Z
4category: release
5excerpt: 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
8beta.59 is the alignment release beta.58 set up. It is
9almost entirely renames: every Cloudflare service is now a real
10namespace, every binding across AWS **and** Cloudflare follows one
11`{Verb}{Resource}` convention, and the two-layer `Binding.Policy`
12split is gone — folded into a single runtime-guarded `Layer`
13([#690](https://github.com/alchemy-run/alchemy/pull/690)).
14
15There'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 —
17you'll find-and-replace your way through it.
18
19:::caution
20**Breaking changes** — this release is mostly breaking renames.
21Migration 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
48everything lived directly on `Cloudflare.*` with a redundant prefix
49baked into each name (`Cloudflare.KVNamespace`, `Cloudflare.R2Bucket`,
50`Cloudflare.D1Database`).
51
52### Resources move under their service
53
54It now mirrors the AWS standard — one `export * as <Service>` per
55service, 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
66The 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
75The common building blocks are re-exported at the root so the everyday
76path stays short — these all still resolve, no namespace required:
77
78```typescript
79yield* Cloudflare.Worker("Worker", { /* ... */ });
80yield* Cloudflare.Container("Sandbox", { /* ... */ });
81yield* Cloudflare.DurableObject("Counter", { /* ... */ });
82yield* Cloudflare.Workflow("Job", { /* ... */ });
83yield* Cloudflare.RateLimit("Limiter", { /* ... */ });
84Cloudflare.cron("0 * * * *", handler);
85```
86
87Everything else is reached through its namespace.
88
89### `DurableObjectNamespace` → `DurableObject`
90
91The 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
104A few folders were reshaped to fit: `Container/` → `Containers/`,
105`Queue` → `Queues`, and workflow code moved into `Workflows/`. There's
106also a new `Cloudflare.WebSocket` **type** for typing hibernatable
107WebSocket handlers.
108
109## `Ai*` becomes one `AI` namespace
110
111The three separate AI services — `AiGateway`, `AiSearch`, and
112`AiSecurity` — were really one product surface split across three
113folders. 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
122Symbols 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
128This is the heart of the release. AWS and Cloudflare bindings now read
129identically. A capability is the noun, prefixed by the verb (the
130access level it grants), and you call it **directly** — the `.bind`
131method 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
140Where the underlying API distinguishes access levels, the capability is
141split into `Read` / `Write` / `ReadWrite` so you request least
142privilege — and the verb leads the name:
143
144```typescript
145yield* Cloudflare.KV.ReadNamespace(Sessions); // get / list
146yield* Cloudflare.KV.WriteNamespace(Sessions); // put / delete
147yield* Cloudflare.KV.ReadWriteNamespace(Sessions); // both
148```
149
150### `*Binding` vs `*Http`
151
152Each capability ships two interchangeable implementation Layers, and
153the 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
167The split lands on AWS too, where the only transport is HTTP: every AWS
168impl layer was renamed `*Live` → `*Http` (`DynamoDB.GetItemHttp`,
169`SQS.SendMessageHttp`, `S3.GetObjectHttp`), because an AWS runtime calls
170the distilled HTTP API authenticated by the Lambda's IAM role.
171`Binding` is a Cloudflare native-worker concept — AWS never had one.
172
173The 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
182The client interface is **identical** regardless of which Layer you
183provide — a Worker reading a bucket over its native binding and a
184container reading the same bucket over a token are the same code.
185
186## More Cloudflare `*Http` bindings, one mechanism
187
188A native Worker binding only exists inside a Worker. The moment you
189need the same capability somewhere with no native binding — a Container
190process, a Durable Object reaching a foreign resource, a script running
191outside the edge — you need the HTTP transport. beta.59 fills that
192gap out: R2, KV, and Queues already had `*Http` impls, and DNS joins
193them this release, all sharing one mechanism and the verb-first naming.
194
195```typescript
196yield* Cloudflare.DNS.ReadDns(Zone); // list / get records
197yield* Cloudflare.DNS.WriteDns(Zone); // create / update / delete
198yield* Cloudflare.DNS.ReadWriteDns(Zone); // both
199```
200
201### How it works: mint a scoped `AccountApiToken`
202
203An `*Http` Layer can't lean on `env.MY_BINDING`, so it provisions its
204own credential. At deploy time it mints an `AccountApiToken` scoped to
205**only** the permission groups that access level needs, and binds that
206token'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
210const token = yield* AccountApiToken(`${self.LogicalId}Token`);
211
212if (!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
225The permission groups are what make this least-privilege: a
226`ReadNamespace` mints a read-only KV token, a `WriteNamespace` a
227write-only one. At runtime the client reads the token's `value` and
228`accountId` from its secrets and calls Cloudflare's REST API through
229distilled — no account-wide API key, no manual token plumbing, and a
230different scoped token per capability.
231
232## `Binding.Policy` is gone
233
234Bindings used to be two pieces: a runtime `Binding.Service` plus a
235separate deploy-time `Binding.Policy` (with its own `*PolicyLive`
236layer) that registered the IAM / native binding. beta.59 collapses them
237into a single `Binding.Service` whose setup Effect does the deploy-time
238wiring inline, guarded so it only runs at plan time:
239
240```typescript
241if (!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
248At plan time the guard is open and `bind` records what the function
249needs. At runtime (`__ALCHEMY_RUNTIME__` is set) the branch is skipped
250and the same call resolves to just the lightweight client, so the
251runtime bundle stays small. This is the exact pattern the capability
252bindings already used in beta.58 — now **every** binding, including
253event sources, follows it. `Binding.Policy`, `Binding.ServiceClass`,
254and the `Policy` / `PolicyLike` types are removed from the core.
255
256## Worker-only bindings take a string id
257
258The Cloudflare bindings with no backing resource — `Images`, `Browser`,
259`VersionMetadata`, `RateLimit` — now take a **string id first**, like a
260resource 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
267Their 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
277The Artifacts namespace is renamed `Store` → `Namespace`. Access stays
278explicit 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
289Event sources are bindings too, so they got the same treatment. The old
290`X(resource, props?).subscribe(handler)` shape is replaced by a single
291verb-prefixed, resource-and-event-named callable that takes the handler
292as 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
303Every event source now reads the same way — the name tells you both the
304resource 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
315Dropping `.subscribe` also drops the handler's runtime requirements at
316the source boundary, so explicit `Body` type arguments no longer break
317inference.
318
319## Also in this release
320
321Two 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)