What's new in beta.35
v2.0.0-beta.35 is out. The headline addition is an
Effect-native, Stream-shaped consumer API for Cloudflare
Queues — write a queue handler the same way you’d write
any other Effect pipeline. Plus R2 bucket custom domains,
non-string Worker env bindings, Neon logical replication,
and a handful of CLI polish. Community contributors get
inline shoutouts; full credits in the
Contributors section.
Cloudflare.messages(queue).subscribe(...) — Effect-native queue consumer
Section titled “Cloudflare.messages(queue).subscribe(...) — Effect-native queue consumer”Process Cloudflare Queue batches as a typed Effect Stream,
with ack/retry exposed per message. The handler returns
when the batch is done; failed messages get retried up to
maxRetries. Same Worker, same Effect runtime, same
bindings — the queue consumer is just another yield* in
the init phase.
A real producer-and-consumer pair: a Worker accepts POSTs
to /queue/send, the consumer writes each message to R2
as JSON so the result is observable. Bindings already on
the Worker (bucket, queue) are in scope inside the
stream handler — the consumer is a peer to the Worker’s
init phase, not a separate file.
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as Stream from "effect/Stream";import { Bucket } from "./Bucket.ts";import { Queue } from "./Queue.ts";
interface QueueMessageBody { id: string; text: string; sentAt: number;}
export default class Api extends Cloudflare.Worker<Api>()( "Api", { main: import.meta.filename }, Effect.gen(function* () { const bucket = yield* Cloudflare.R2Bucket.bind(Bucket); const queueResource = yield* Queue; const queue = yield* Cloudflare.QueueBinding.bind(queueResource);
// Consumer side. Each batch arrives as a Stream of QueueMessage<Body>; // success ack()s every message, failure retry()s. Crash mid-batch and // unprocessed messages are redelivered. yield* Cloudflare.messages<QueueMessageBody>(queueResource, { batchSize: 25, maxRetries: 3, }).subscribe((stream) => Stream.runForEach(stream, (msg) => bucket .put(`/queue/${msg.body.id}`, JSON.stringify(msg.body), { httpMetadata: { contentType: "application/json" }, }) .pipe(Effect.asVoid), ), );
return { // Producer side: POST /queue/send adds a message. fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; if (request.url === "/queue/send" && request.method === "POST") { const text = yield* request.text; const msg: QueueMessageBody = { id: crypto.randomUUID(), text, sentAt: Date.now(), }; yield* queue.send(msg); return yield* HttpServerResponse.json({ sent: msg }, { status: 202 }); } return HttpServerResponse.text("ok"); }), }; }).pipe( Effect.provide( Layer.mergeAll( Cloudflare.R2BucketBindingLive, Cloudflare.QueueBindingLive, Cloudflare.QueueEventSourceLive, // required for `messages(...)` to dispatch ), ), ),) {}The full round-trip lives in
examples/cloudflare-worker/src/Api.ts.
Tutorial › Queue consumer
R2 bucket custom domains
Section titled “R2 bucket custom domains”Attach custom hostnames to an R2 bucket directly from the resource declaration. Add or remove entries by editing the array — Alchemy reconciles the live attachments on the next deploy.
const Assets = yield* Cloudflare.R2Bucket("Assets", { customDomains: [ "assets.example.com", "cdn.example.com", ],});Each domain points at the bucket’s public origin; pair
with a Cloudflare Zone if you want the DNS record
provisioned in the same stack.
— Thanks to Alex (#241) for the contribution.
Non-string env bindings on Workers
Section titled “Non-string env bindings on Workers”Worker bindings now accept non-string values directly —
numbers, booleans, objects — without manual JSON encoding
on either side. Alchemy serializes at deploy and the typed
runtime binding gives you the original shape back.
const Api = yield* Cloudflare.Worker("Api", { main: "./src/worker.ts", bindings: { MAX_ITEMS: 100, // number DEBUG: true, // boolean FEATURES: { beta: true, alpha: false }, // object },});
// inside the Workerconst limit = env.MAX_ITEMS; // ^? number — 100const debug = env.DEBUG; // ^? boolean — trueconst features = env.FEATURES; // ^? { beta: boolean; alpha: boolean }Previously you’d round-trip through JSON.stringify at
the binding site and JSON.parse on the runtime side
with an as cast for typing. Now it’s just env.MAX_ITEMS.
— Thanks to Michael K (#269) for the contribution.
Neon logical replication
Section titled “Neon logical replication”Opt-in flag on Neon.Project for enabling logical
replication — needed for downstream CDC tools, change-data
pipelines, and some replicas. Defaults to off.
const Project = yield* Neon.Project("App", { region: "aws-us-east-1", enableLogicalReplication: true,});Toggling the flag on an existing project applies in place — no replacement needed.
— Thanks to Baptiste Arnaud (#268) for the contribution.
CLI: version-on-npm warning
Section titled “CLI: version-on-npm warning”alchemy now checks the latest version on npm at startup
and prints a one-line nudge when you’re behind. Skips in
CI / non-TTY automatically.
$ alchemy deployℹ A newer version of alchemy (2.0.0-beta.36) is available. Run `bun add alchemy@latest` to upgrade.CLI: plain-text renderer in non-interactive terminals
Section titled “CLI: plain-text renderer in non-interactive terminals”The interactive deploy UI now downgrades to a plain-text progress renderer when run outside a TTY (CI logs, piped output). No more ANSI control sequences cluttering CI logs.
CLI: alchemy cloudflare / alchemy aws namespaces
Section titled “CLI: alchemy cloudflare / alchemy aws namespaces”Cloud-scoped subcommands are now grouped under provider
namespaces — e.g. alchemy cloudflare state for inspecting
the Cloudflare-backed state store. Top-level commands
(deploy, destroy, plan, dev, test) are unchanged.
alchemy cloudflare state ls # list state entriesalchemy aws state get <key> # read a state entryFixes worth knowing about
Section titled “Fixes worth knowing about”- Auth providers moved into layers. The
Rrequirement on stack effects isneveragain — credential resolution is internal to the provider layer instead of leaking into the user-facing stack type. - Dev mode Node compatibility & profiles. Several
Node-specific runtime issues in
alchemy devfixed, including profile handling for AWS/Cloudflare credentials. QueueConsumer.listConsumerspaginates. Previously truncated at the API’s default page size when a Worker was listed as a consumer on many queues. Now paginates and surfaces conflicting consumer registrations with a clear error.- R2 custom domain interface simplified to a plain string array (was a verbose object shape).
- Drizzle schema flag tightened — only marked as
updated when migrations actually drift, not on every
drizzle-kit generaterun. - Vite
builder.sharedConfigBuilddisabled. Fixes a class of build failures when bundling Workers via the Vite plugin. - Distilled Cloudflare runtime bumped to 0.3.1.
Contributors
Section titled “Contributors”Big thank-you to everyone who shipped code in this beta:
- Alex — R2 bucket custom domains (#241)
- Michael K — non-string env bindings on Workers (#269)
- Baptiste Arnaud — Neon
enableLogicalReplication(#268)