2.0.0-beta.59 - One Namespace, One Binding
beta.59 is the alignment release beta.58 set up. It is
almost entirely renames: every Cloudflare service is now a real
namespace, every binding across AWS and Cloudflare follows one
{Verb}{Resource} convention, and the two-layer Binding.Policy
split is gone — folded into a single runtime-guarded Layer
(#690).
There’s almost no new behavior here. It’s a coordinated rename across ~900 symbols and ~1200 files so the API reads the same everywhere — you’ll find-and-replace your way through it.
Every Cloudflare service is a namespace
Section titled “Every Cloudflare service is a namespace”Cloudflare/index.ts used to be a flat export * of every symbol, so
everything lived directly on Cloudflare.* with a redundant prefix
baked into each name (Cloudflare.KVNamespace, Cloudflare.R2Bucket,
Cloudflare.D1Database).
Resources move under their service
Section titled “Resources move under their service”It now mirrors the AWS standard — one export * as <Service> per
service, with the prefix stripped from the symbol:
const kv = yield* Cloudflare.KVNamespace("Sessions");const bucket = yield* Cloudflare.R2Bucket("Assets");const db = yield* Cloudflare.D1Database("App");const kv = yield* Cloudflare.KV.Namespace("Sessions");const bucket = yield* Cloudflare.R2.Bucket("Assets");const db = yield* Cloudflare.D1.Database("App");The same applies to Worker, which now lives under Cloudflare.Workers:
export default Cloudflare.Worker("Worker", { /* ... */ });export default Cloudflare.Workers.Worker("Worker", { /* ... */ });Common building blocks stay at the root
Section titled “Common building blocks stay at the root”The common building blocks are re-exported at the root so the everyday path stays short — these all still resolve, no namespace required:
yield* Cloudflare.Worker("Worker", { /* ... */ });yield* Cloudflare.Container("Sandbox", { /* ... */ });yield* Cloudflare.DurableObject("Counter", { /* ... */ });yield* Cloudflare.Workflow("Job", { /* ... */ });yield* Cloudflare.RateLimit("Limiter", { /* ... */ });Cloudflare.cron("0 * * * *", handler);Everything else is reached through its namespace.
DurableObjectNamespace → DurableObject
Section titled “DurableObjectNamespace → DurableObject”The Durable Object base class loses its Namespace suffix — it’s a
DurableObject now, full stop:
export class Counter extends Cloudflare.DurableObjectNamespace<Counter>()(export class Counter extends Cloudflare.DurableObject<Counter>()( "Counter", /* ... */) {}Folder reshuffles and the new WebSocket type
Section titled “Folder reshuffles and the new WebSocket type”A few folders were reshaped to fit: Container/ → Containers/,
Queue → Queues, and workflow code moved into Workflows/. There’s
also a new Cloudflare.WebSocket type for typing hibernatable
WebSocket handlers.
Ai* becomes one AI namespace
Section titled “Ai* becomes one AI namespace”The three separate AI services — AiGateway, AiSearch, and
AiSecurity — were really one product surface split across three
folders. They collapse into a single Cloudflare.AI namespace:
const gateway = yield* Cloudflare.AiGateway("Gateway");const search = yield* Cloudflare.AiSearch("Search");const gateway = yield* Cloudflare.AI.Gateway("Gateway");const search = yield* Cloudflare.AI.Search("Search");Symbols drop the redundant Ai prefix in the process —
AiGatewayDataset → AI.Dataset, AiSearchInstance →
AI.SearchInstance, AiSecuritySettings → AI.SecuritySettings.
One binding convention, everywhere
Section titled “One binding convention, everywhere”This is the heart of the release. AWS and Cloudflare bindings now read
identically. A capability is the noun, prefixed by the verb (the
access level it grants), and you call it directly — the .bind
method is gone:
const get = yield* AWS.DynamoDB.GetItem.bind(table);const bucket = yield* Cloudflare.R2Bucket.bind(Bucket);const get = yield* AWS.DynamoDB.GetItem(table);const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);Where the underlying API distinguishes access levels, the capability is
split into Read / Write / ReadWrite so you request least
privilege — and the verb leads the name:
yield* Cloudflare.KV.ReadNamespace(Sessions); // get / listyield* Cloudflare.KV.WriteNamespace(Sessions); // put / deleteyield* Cloudflare.KV.ReadWriteNamespace(Sessions); // both*Binding vs *Http
Section titled “*Binding vs *Http”Each capability ships two interchangeable implementation Layers, and the suffix tells you which transport it uses:
*Binding— a native Cloudflare Worker binding (env.BUCKET).*Http— a scoped HTTP client that works anywhere, including inside a Container process that has no native binding.
// native Worker binding.pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding))
// scoped HTTP token — same client, runs anywhere.pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketHttp))The split lands on AWS too, where the only transport is HTTP: every AWS
impl layer was renamed *Live → *Http (DynamoDB.GetItemHttp,
SQS.SendMessageHttp, S3.GetObjectHttp), because an AWS runtime calls
the distilled HTTP API authenticated by the Lambda’s IAM role.
Binding is a Cloudflare native-worker concept — AWS never had one.
The two transports do the same thing in their respective worlds:
- AWS
*Http— emits a least-privilege IAM policy statement on the Lambda’s role, then calls the distilled HTTP API with it. - Cloudflare
*Http— mints a scopedAccountApiTokenwith only the permission groups that access level needs, binds the token’s value into the environment as a secret, and calls Cloudflare’s REST API through distilled.
The client interface is identical regardless of which Layer you provide — a Worker reading a bucket over its native binding and a container reading the same bucket over a token are the same code.
More Cloudflare *Http bindings, one mechanism
Section titled “More Cloudflare *Http bindings, one mechanism”A native Worker binding only exists inside a Worker. The moment you
need the same capability somewhere with no native binding — a Container
process, a Durable Object reaching a foreign resource, a script running
outside the edge — you need the HTTP transport. beta.59 fills that
gap out: R2, KV, and Queues already had *Http impls, and DNS joins
them this release, all sharing one mechanism and the verb-first naming.
yield* Cloudflare.DNS.ReadDns(Zone); // list / get recordsyield* Cloudflare.DNS.WriteDns(Zone); // create / update / deleteyield* Cloudflare.DNS.ReadWriteDns(Zone); // bothHow it works: mint a scoped AccountApiToken
Section titled “How it works: mint a scoped AccountApiToken”An *Http Layer can’t lean on env.MY_BINDING, so it provisions its
own credential. At deploy time it mints an AccountApiToken scoped to
only the permission groups that access level needs, and binds that
token’s value into the host as a secret — all behind the
__ALCHEMY_RUNTIME__ guard so it’s a deploy-time-only step:
const token = yield* AccountApiToken(`${self.LogicalId}Token`);
if (!globalThis.__ALCHEMY_RUNTIME__) { yield* token.bind`${resource.LogicalId}`({ policies: [ { effect: "allow", permissionGroups, // e.g. ["Workers KV Storage Read"] for ReadNamespace resources: { [`com.cloudflare.api.account.${accountId}`]: "*" }, }, ], });}The permission groups are what make this least-privilege: a
ReadNamespace mints a read-only KV token, a WriteNamespace a
write-only one. At runtime the client reads the token’s value and
accountId from its secrets and calls Cloudflare’s REST API through
distilled — no account-wide API key, no manual token plumbing, and a
different scoped token per capability.
Binding.Policy is gone
Section titled “Binding.Policy is gone”Bindings used to be two pieces: a runtime Binding.Service plus a
separate deploy-time Binding.Policy (with its own *PolicyLive
layer) that registered the IAM / native binding. beta.59 collapses them
into a single Binding.Service whose setup Effect does the deploy-time
wiring inline, guarded so it only runs at plan time:
if (!globalThis.__ALCHEMY_RUNTIME__) { // deploy-time only: register IAM / native binding / env on the host yield* host.bind`${resource}`(/* … */);}// always: return the typed runtime clientAt plan time the guard is open and bind records what the function
needs. At runtime (__ALCHEMY_RUNTIME__ is set) the branch is skipped
and the same call resolves to just the lightweight client, so the
runtime bundle stays small. This is the exact pattern the capability
bindings already used in beta.58 — now every binding, including
event sources, follows it. Binding.Policy, Binding.ServiceClass,
and the Policy / PolicyLike types are removed from the core.
Worker-only bindings take a string id
Section titled “Worker-only bindings take a string id”The Cloudflare bindings with no backing resource — Images, Browser,
VersionMetadata, RateLimit — now take a string id first, like a
resource logical id, instead of a { name } object:
const images = yield* Cloudflare.Images.Images({ name: "PIPELINE" });const images = yield* Cloudflare.Images.Images("PIPELINE");Their native layers drop the *Live / *Layer suffix to match the
*Binding convention:
.pipe(Effect.provide(Cloudflare.Workers.BrowserBindingLive)).pipe(Effect.provide(Cloudflare.Workers.BrowserBinding))Artifacts.Store → Artifacts.Namespace
Section titled “Artifacts.Store → Artifacts.Namespace”The Artifacts namespace is renamed Store → Namespace. Access stays
explicit through ReadNamespace / WriteNamespace / ReadWriteNamespace:
const Repos = Cloudflare.Artifacts.Store("Repos");const repos = yield* Cloudflare.Artifacts.ReadWriteStore(Repos);const Repos = Cloudflare.Artifacts.Namespace("Repos");const repos = yield* Cloudflare.Artifacts.ReadWriteNamespace(Repos);Event sources: consume<Resource><Event>
Section titled “Event sources: consume<Resource><Event>”Event sources are bindings too, so they got the same treatment. The old
X(resource, props?).subscribe(handler) shape is replaced by a single
verb-prefixed, resource-and-event-named callable that takes the handler
as its last argument:
yield* Cloudflare.Queues.messages(inbound).subscribe((records) => records.pipe(Stream.runForEach(Console.log)),);yield* Cloudflare.Queues.consumeQueueMessages(inbound, (records) => records.pipe(Stream.runForEach(Console.log)),);Every event source now reads the same way — the name tells you both the resource and the element it yields:
| Before | After |
|---|---|
SQS.messages(q).subscribe | SQS.consumeQueueMessages(q, fn) |
S3.notifications(b).subscribe | S3.consumeBucketEvents(b, fn) |
SNS.notifications(t).subscribe | SNS.consumeTopicNotifications(t, fn) |
Kinesis.records(s).process | Kinesis.consumeStreamRecords(s, fn) |
DynamoDB.stream(t).process | DynamoDB.consumeTableChanges(t, fn) |
GitHub.events(r).subscribe | GitHub.consumeRepositoryEvents(r, fn) |
Dropping .subscribe also drops the handler’s runtime requirements at
the source boundary, so explicit Body type arguments no longer break
inference.
Also in this release
Section titled “Also in this release”Two fixes shipped alongside the rename:
deleteFirstis honored on replace (#692) — a replacement defaults to create-first (stand up the new generation, then tear down the old), but some resources can’t coexist with their previous self (a fixed physical name, a singleton).deleteFirstis meant to flip that ordering, and the engine now actually does: it deletes the previous generation before creating the replacement instead of leaking an old chain into the next phase. Thanks John Royal.- Worker diff tolerates legacy custom-domain state
(#694) —
Alchemy ≤ beta.44 stored each Worker custom domain as a
{ id, hostname, zoneId }object; beta.45+ storeshttps://<hostname>strings and the diff path calls string methods on each entry. Older state now gets coerced back to strings so a Worker deploy no longer throwsu.endsWith is not a function(#546). Thanks Nicolas Fléron.