Skip to content

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

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.

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 Worker
const limit = env.MAX_ITEMS; // ^? number — 100
const debug = env.DEBUG; // ^? boolean — true
const 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.

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.

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.

Terminal window
alchemy cloudflare state ls # list state entries
alchemy aws state get <key> # read a state entry
  • Auth providers moved into layers. The R requirement on stack effects is never again — 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 dev fixed, including profile handling for AWS/Cloudflare credentials.
  • QueueConsumer.listConsumers paginates. 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 generate run.
  • Vite builder.sharedConfigBuild disabled. Fixes a class of build failures when bundling Workers via the Vite plugin.
  • Distilled Cloudflare runtime bumped to 0.3.1.

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)