Skip to content

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.

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).

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", { /* ... */ });

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.

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/, QueueQueues, and workflow code moved into Workflows/. There’s also a new Cloudflare.WebSocket type for typing hibernatable WebSocket handlers.

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 — AiGatewayDatasetAI.Dataset, AiSearchInstanceAI.SearchInstance, AiSecuritySettingsAI.SecuritySettings.

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 / list
yield* Cloudflare.KV.WriteNamespace(Sessions); // put / delete
yield* Cloudflare.KV.ReadWriteNamespace(Sessions); // both

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 scoped AccountApiToken with 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 records
yield* Cloudflare.DNS.WriteDns(Zone); // create / update / delete
yield* Cloudflare.DNS.ReadWriteDns(Zone); // both

How 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.

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 client

At 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.

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))

The Artifacts namespace is renamed StoreNamespace. 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 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:

BeforeAfter
SQS.messages(q).subscribeSQS.consumeQueueMessages(q, fn)
S3.notifications(b).subscribeS3.consumeBucketEvents(b, fn)
SNS.notifications(t).subscribeSNS.consumeTopicNotifications(t, fn)
Kinesis.records(s).processKinesis.consumeStreamRecords(s, fn)
DynamoDB.stream(t).processDynamoDB.consumeTableChanges(t, fn)
GitHub.events(r).subscribeGitHub.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.

Two fixes shipped alongside the rename:

  • deleteFirst is 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). deleteFirst is 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+ stores https://<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 throws u.endsWith is not a function (#546). Thanks Nicolas Fléron.