| 1 | --- |
| 2 | title: 2.0.0-beta.35 - Queue Streams & R2 Domains |
| 3 | date: 2026-05-08 |
| 4 | category: release |
| 5 | excerpt: A Stream-shaped Effect consumer for Cloudflare Queues, R2 bucket custom domains, non-string env bindings on Workers, Neon logical replication, and a round of CLI quality-of-life. |
| 6 | --- |
| 7 | |
| 8 | `v2.0.0-beta.35` is out. The headline addition is an |
| 9 | Effect-native, `Stream`-shaped consumer API for Cloudflare |
| 10 | Queues — write a queue handler the same way you'd write |
| 11 | any other Effect pipeline. Plus R2 bucket custom domains, |
| 12 | non-string Worker env bindings, Neon logical replication, |
| 13 | and a handful of CLI polish. Community contributors get |
| 14 | inline shoutouts; full credits in the |
| 15 | [Contributors section](#contributors). |
| 16 | |
| 17 | ## `Cloudflare.Queues.messages(queue).subscribe(...)` — Effect-native queue consumer |
| 18 | |
| 19 | Process Cloudflare Queue batches as a typed Effect `Stream`, |
| 20 | with `ack`/`retry` exposed per message. The handler returns |
| 21 | when the batch is done; failed messages get retried up to |
| 22 | `maxRetries`. Same Worker, same Effect runtime, same |
| 23 | bindings — the queue consumer is just another `yield*` in |
| 24 | the init phase. |
| 25 | |
| 26 | A real producer-and-consumer pair: a Worker accepts POSTs |
| 27 | to `/queue/send`, the consumer writes each message to R2 |
| 28 | as JSON so the result is observable. Bindings already on |
| 29 | the Worker (`bucket`, `queue`) are in scope inside the |
| 30 | stream handler — the consumer is a peer to the Worker's |
| 31 | init phase, not a separate file. |
| 32 | |
| 33 | ```typescript |
| 34 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 35 | import * as Effect from "effect/Effect"; |
| 36 | import * as Stream from "effect/Stream"; |
| 37 | import { Bucket } from "./Bucket.ts"; |
| 38 | import { Queue } from "./Queue.ts"; |
| 39 | |
| 40 | interface QueueMessageBody { |
| 41 | id: string; |
| 42 | text: string; |
| 43 | sentAt: number; |
| 44 | } |
| 45 | |
| 46 | export default class Api extends Cloudflare.Worker<Api>()( |
| 47 | "Api", |
| 48 | { main: import.meta.filename }, |
| 49 | Effect.gen(function* () { |
| 50 | const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket); |
| 51 | const queueResource = yield* Queue; |
| 52 | const queue = yield* Cloudflare.QueueBinding.bind(queueResource); |
| 53 | |
| 54 | // Consumer side. Each batch arrives as a Stream of Message<Body>; |
| 55 | // success ack()s every message, failure retry()s. Crash mid-batch and |
| 56 | // unprocessed messages are redelivered. |
| 57 | yield* Cloudflare.Queues.messages<QueueMessageBody>(queueResource, { |
| 58 | batchSize: 25, |
| 59 | maxRetries: 3, |
| 60 | }).subscribe((stream) => |
| 61 | Stream.runForEach(stream, (msg) => |
| 62 | bucket |
| 63 | .put(`/queue/${msg.body.id}`, JSON.stringify(msg.body), { |
| 64 | httpMetadata: { contentType: "application/json" }, |
| 65 | }) |
| 66 | .pipe(Effect.asVoid), |
| 67 | ), |
| 68 | ); |
| 69 | |
| 70 | return { |
| 71 | // Producer side: POST /queue/send adds a message. |
| 72 | fetch: Effect.gen(function* () { |
| 73 | const request = yield* HttpServerRequest; |
| 74 | if (request.url === "/queue/send" && request.method === "POST") { |
| 75 | const text = yield* request.text; |
| 76 | const msg: QueueMessageBody = { |
| 77 | id: crypto.randomUUID(), |
| 78 | text, |
| 79 | sentAt: Date.now(), |
| 80 | }; |
| 81 | yield* queue.send(msg); |
| 82 | return yield* HttpServerResponse.json({ sent: msg }, { status: 202 }); |
| 83 | } |
| 84 | return HttpServerResponse.text("ok"); |
| 85 | }), |
| 86 | }; |
| 87 | }).pipe( |
| 88 | Effect.provide( |
| 89 | Layer.mergeAll( |
| 90 | Cloudflare.R2.ReadWriteBucketBinding, |
| 91 | Cloudflare.QueueBindingLive, |
| 92 | Cloudflare.Queues.EventSourceLive, // required for `messages(...)` to dispatch |
| 93 | ), |
| 94 | ), |
| 95 | ), |
| 96 | ) {} |
| 97 | ``` |
| 98 | |
| 99 | The full round-trip lives in |
| 100 | [`examples/cloudflare-worker/src/Api.ts`](https://github.com/alchemy-run/alchemy/blob/main/examples/cloudflare-worker/src/Api.ts). |
| 101 | [Tutorial › Queue consumer](/cloudflare/messaging/queues) |
| 102 | |
| 103 | ## R2 bucket custom domains |
| 104 | |
| 105 | Attach custom hostnames to an R2 bucket directly from the |
| 106 | resource declaration. Add or remove entries by editing the |
| 107 | array — Alchemy reconciles the live attachments on the |
| 108 | next deploy. |
| 109 | |
| 110 | ```typescript |
| 111 | const Assets = yield* Cloudflare.R2.Bucket("Assets", { |
| 112 | customDomains: [ |
| 113 | "assets.example.com", |
| 114 | "cdn.example.com", |
| 115 | ], |
| 116 | }); |
| 117 | ``` |
| 118 | |
| 119 | Each domain points at the bucket's public origin; pair |
| 120 | with a Cloudflare `Zone` if you want the DNS record |
| 121 | provisioned in the same stack. |
| 122 | |
| 123 | — *Thanks to **Alex** ([#241](https://github.com/alchemy-run/alchemy/pull/241)) |
| 124 | for the contribution.* |
| 125 | |
| 126 | ## Non-string env bindings on Workers |
| 127 | |
| 128 | Worker `bindings` now accept non-string values directly — |
| 129 | numbers, booleans, objects — without manual JSON encoding |
| 130 | on either side. Alchemy serializes at deploy and the typed |
| 131 | runtime binding gives you the original shape back. |
| 132 | |
| 133 | ```typescript |
| 134 | const Api = yield* Cloudflare.Worker("Api", { |
| 135 | main: "./src/worker.ts", |
| 136 | bindings: { |
| 137 | MAX_ITEMS: 100, // number |
| 138 | DEBUG: true, // boolean |
| 139 | FEATURES: { beta: true, alpha: false }, // object |
| 140 | }, |
| 141 | }); |
| 142 | |
| 143 | // inside the Worker |
| 144 | const limit = env.MAX_ITEMS; // ^? number — 100 |
| 145 | const debug = env.DEBUG; // ^? boolean — true |
| 146 | const features = env.FEATURES; // ^? { beta: boolean; alpha: boolean } |
| 147 | ``` |
| 148 | |
| 149 | Previously you'd round-trip through `JSON.stringify` at |
| 150 | the binding site and `JSON.parse` on the runtime side |
| 151 | with an `as` cast for typing. Now it's just `env.MAX_ITEMS`. |
| 152 | |
| 153 | — *Thanks to **Michael K** |
| 154 | ([#269](https://github.com/alchemy-run/alchemy/pull/269)) |
| 155 | for the contribution.* |
| 156 | |
| 157 | ## Neon logical replication |
| 158 | |
| 159 | Opt-in flag on `Neon.Project` for enabling logical |
| 160 | replication — needed for downstream CDC tools, change-data |
| 161 | pipelines, and some replicas. Defaults to off. |
| 162 | |
| 163 | ```typescript |
| 164 | const Project = yield* Neon.Project("App", { |
| 165 | region: "aws-us-east-1", |
| 166 | enableLogicalReplication: true, |
| 167 | }); |
| 168 | ``` |
| 169 | |
| 170 | Toggling the flag on an existing project applies in place — |
| 171 | no replacement needed. |
| 172 | |
| 173 | — *Thanks to **Baptiste Arnaud** |
| 174 | ([#268](https://github.com/alchemy-run/alchemy/pull/268)) |
| 175 | for the contribution.* |
| 176 | |
| 177 | ## CLI: version-on-npm warning |
| 178 | |
| 179 | `alchemy` now checks the latest version on npm at startup |
| 180 | and prints a one-line nudge when you're behind. Skips in |
| 181 | CI / non-TTY automatically. |
| 182 | |
| 183 | ```text |
| 184 | $ alchemy deploy |
| 185 | ℹ A newer version of alchemy (2.0.0-beta.36) is available. |
| 186 | Run `bun add alchemy@latest` to upgrade. |
| 187 | ``` |
| 188 | |
| 189 | ## CLI: plain-text renderer in non-interactive terminals |
| 190 | |
| 191 | The interactive deploy UI now downgrades to a plain-text |
| 192 | progress renderer when run outside a TTY (CI logs, piped |
| 193 | output). No more ANSI control sequences cluttering CI logs. |
| 194 | |
| 195 | ## CLI: `alchemy cloudflare` / `alchemy aws` namespaces |
| 196 | |
| 197 | Cloud-scoped subcommands are now grouped under provider |
| 198 | namespaces — e.g. `alchemy cloudflare state` for inspecting |
| 199 | the Cloudflare-backed state store. Top-level commands |
| 200 | (`deploy`, `destroy`, `plan`, `dev`, `test`) are unchanged. |
| 201 | |
| 202 | ```sh |
| 203 | alchemy cloudflare state ls # list state entries |
| 204 | alchemy aws state get <key> # read a state entry |
| 205 | ``` |
| 206 | |
| 207 | ## Fixes worth knowing about |
| 208 | |
| 209 | - **Auth providers moved into layers.** The `R` requirement |
| 210 | on stack effects is `never` again — credential resolution |
| 211 | is internal to the provider layer instead of leaking into |
| 212 | the user-facing stack type. |
| 213 | - **Dev mode Node compatibility & profiles.** Several |
| 214 | Node-specific runtime issues in `alchemy dev` fixed, |
| 215 | including profile handling for AWS/Cloudflare credentials. |
| 216 | - **`Consumer.listConsumers` paginates.** Previously |
| 217 | truncated at the API's default page size when a Worker |
| 218 | was listed as a consumer on many queues. Now paginates |
| 219 | and surfaces conflicting consumer registrations with a |
| 220 | clear error. |
| 221 | - **R2 custom domain interface simplified** to a plain |
| 222 | string array (was a verbose object shape). |
| 223 | - **Drizzle schema flag tightened** — only marked as |
| 224 | updated when migrations actually drift, not on every |
| 225 | `drizzle-kit generate` run. |
| 226 | - **Vite `builder.sharedConfigBuild` disabled.** Fixes a |
| 227 | class of build failures when bundling Workers via the |
| 228 | Vite plugin. |
| 229 | - **Distilled Cloudflare runtime bumped to 0.3.1.** |
| 230 | |
| 231 | ## Contributors |
| 232 | |
| 233 | Big thank-you to everyone who shipped code in this beta: |
| 234 | |
| 235 | - **Alex** — R2 bucket custom domains ([#241](https://github.com/alchemy-run/alchemy/pull/241)) |
| 236 | - **Michael K** — non-string env bindings on Workers ([#269](https://github.com/alchemy-run/alchemy/pull/269)) |
| 237 | - **Baptiste Arnaud** — Neon `enableLogicalReplication` ([#268](https://github.com/alchemy-run/alchemy/pull/268)) |
| 238 | |
| 239 | ## Where to go next |
| 240 | |
| 241 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta35) |
| 242 | - [Compare v2.0.0-beta.34 → v2.0.0-beta.35](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.34...v2.0.0-beta.35) |