1---
2title: 2.0.0-beta.66 - Local Emulation & Gradual Deployments
3date: 2026-07-30T20:00:00Z
4category: release
5excerpt: v2.0.0-beta.66 emulates KV, R2, D1, and Queues locally in alchemy dev with Alchemy.remote() opt-outs, ships Worker versions with gradual deployments and sticky sessions, redesigns Worker URLs around workersDev and a domain object, and adds OpenTelemetry export, a Prisma provider, and a Bedrock LanguageModel binding.
6---
7
8beta.66 makes `alchemy dev` fully local by default: KV, R2, D1, and
9Queues are emulated on your machine, and `Alchemy.remote()` opts any
10resource back onto the real cloud. Workers gain versions, gradual
11deployments, and sticky sessions. The release also redesigns how
12Worker URLs and domains are modeled, and adds OpenTelemetry export,
13a Prisma provider, and a Bedrock `LanguageModel` binding.
14
15:::caution
16**Breaking change**
17
18Worker URL configuration is redesigned
19([#977](https://github.com/alchemy-run/alchemy/pull/977)). The
20`url` and `subdomain` props are replaced by `workersDev`, `domain`
21takes an object form, and the `domains` output is replaced by
22`urls` + `domain`. This applies to `Cloudflare.Worker` and
23`Cloudflare.Website.*`.
24
25```diff lang="typescript"
26 const worker = yield* Cloudflare.Worker("Api", {
27 main: "./src/api.ts",
28- url: false,
29- subdomain: { previewsEnabled: true },
30- domain: ["example.com", "www.example.com"],
31+ workersDev: { enabled: false, previewsEnabled: true },
32+ domain: {
33+ name: "example.com",
34+ aliases: ["www.example.com"],
35+ },
36 });
37
38-worker.domains; // string[]
39+worker.url; // "https://example.com" — always urls[0]
40+worker.urls; // every URL serving the Worker, most significant first
41+worker.domain; // { name, aliases, redirects } | undefined
42```
43
44- `url: true` (the default) becomes `workersDev: true` — omit it.
45- `domain: "example.com"` still works as a string.
46- `domain: []` (explicit detach-all) becomes `domain: null`; an
47 omitted `domain` leaves attachments unmanaged.
48- Persisted state migrates automatically on the first redeploy.
49
50See [Worker URLs redesigned](#worker-urls-redesigned) below for the
51new model.
52:::
53
54## Local emulation in `alchemy dev`
55
56Provider mode (local vs live) is now a first-class engine concept
57([#963](https://github.com/alchemy-run/alchemy/pull/963)). KV
58Namespaces, R2 Buckets, D1 Databases, and Queues are emulated
59locally during `alchemy dev` — they fabricate `dev:`-prefixed ids
60with no cloud calls, and Worker bindings lower onto local workerd
61simulators. D1 applies `migrationsDir` and `importFiles` against
62the local database.
63
64`Alchemy.remote()` opts a resource (or a whole scope) back onto the
65real cloud during dev:
66
67```typescript
68// emulated locally in dev
69const db = yield* Cloudflare.D1.Database("DB", {});
70
71// real queue, even in dev
72const jobs = yield* Cloudflare.Queues.Queue("Jobs", {}).pipe(
73 Alchemy.remote(),
74);
75```
76
77Remote queues work in both directions: a local Worker can produce
78to a live queue, and a local `queue()` handler drains a live queue
79through an `http_pull` consumer.
80
81The State Store now records whether each resource is local or
82live. Switching between the two plans a replacement: `alchemy
83deploy` right after `alchemy dev` replaces local instances with
84real cloud resources instead of updating over them. Node-side
85capability clients (e.g. in Actions) reach the same local
86simulators the Workers see, so a seed script writes to the same
87local KV your Worker reads.
88
89Docs: [Local development](/environments/local-development).
90
91## Worker versions & gradual deployments
92
93The `version` prop maps Cloudflare's [versions & gradual
94deployments](https://developers.cloudflare.com/workers/configuration/versions-and-deployments/)
95onto alchemy stages
96([#948](https://github.com/alchemy-run/alchemy/pull/948)).
97
98Upload one stage's code as a zero-traffic version of another
99stage's Worker — the PR-preview workflow:
100
101```typescript
102const parent = yield* Cloudflare.Worker.ref("Api", { stage: "staging" });
103
104yield* Cloudflare.Worker("Api", {
105 main: "./src/api.ts",
106 version: { parent, message: `PR #${process.env.PR_NUMBER}` },
107});
108```
109
110The version's `url` is a stable aliased preview URL that re-points
111at each newly uploaded version. Add `traffic` to canary a share of
112the parent's traffic, or roll out a Worker's own deploy gradually:
113
114```typescript
115version: { parent, traffic: 10 } // canary: 10% of staging's traffic
116version: { traffic: 25 } // self-rollout: new version at 25%
117```
118
119`version.affinity` pins each user to one version for the duration
120of a rollout
121([#985](https://github.com/alchemy-run/alchemy/pull/985)),
122requested by [Eric Clemmons](https://github.com/ericclemmons):
123
124```typescript
125version: {
126 traffic: 10,
127 // sticky by session cookie, falling back to sticky IP
128 affinity: { cookie: "session_id", ip: true },
129}
130```
131
132Docs: [Gradual deployments](/cloudflare/compute/gradual-deployments).
133
134## Worker URLs redesigned
135
136`workersDev` controls the `workers.dev` surface (and is actually
137reconciled — the old `subdomain` prop was silently ignored), and
138`domain` grows aliases and redirects
139([#977](https://github.com/alchemy-run/alchemy/pull/977)):
140
141```typescript
142const worker = yield* Cloudflare.Worker("Api", {
143 main: "./src/api.ts",
144 workersDev: false,
145 domain: {
146 name: "example.com",
147 aliases: ["www.example.com"], // also serve the Worker
148 redirects: ["old.example.com"], // 301 → https://example.com
149 },
150});
151```
152
153Each redirect hostname gets managed DNS and an edge certificate,
154plus a zone redirect rule that 301s (path and query preserved)
155before Workers run — redirected requests never invoke the Worker.
156
157`urls` lists every URL that serves the Worker, most significant
158first, and `url` is always `urls[0]`:
159
160```typescript
161// workers.dev only (the default)
162["https://api.my-team.workers.dev"]
163
164// custom domain — outranks workers.dev
165["https://example.com", "https://api.my-team.workers.dev"]
166
167// domain with aliases, workersDev: false
168["https://example.com", "https://www.example.com"]
169
170// version worker (version.parent) — the stable aliased preview
171// URL first, then this upload's per-version URL
172[
173 "https://pr-42-api.my-team.workers.dev",
174 "https://a1b2c3d4-api.my-team.workers.dev",
175]
176
177// gradual rollout (version: { traffic }) — the uploaded version's
178// preview URL trails
179[
180 "https://example.com",
181 "https://api.my-team.workers.dev",
182 "https://a1b2c3d4-api.my-team.workers.dev",
183]
184
185// alchemy dev — the dev server's actual surface
186["http://localhost:5173", "http://192.168.1.23:5173"]
187```
188
189Redirect hostnames never appear in `urls` — they serve no content.
190That makes `urls` the CORS allow-list:
191
192```typescript
193const site = yield* Cloudflare.Website.Vite("Site", { ... });
194
195// allow the site — on every URL that serves it — to call the API
196const api = yield* Cloudflare.Worker("Api", {
197 main: "./src/api.ts",
198 env: { ALLOWED_ORIGINS: site.urls },
199});
200```
201
202In production that allow-list is the site's custom domain, aliases,
203and `workers.dev` URL. Under `alchemy dev` it is the Vite dev
204server's Local and Network URLs — `localhost` plus the LAN address
205(`http://192.168…`) — so a phone on your Wi-Fi can load the site
206and call the API.
207
208Docs: [Custom domains](/cloudflare/networking/custom-domains).
209
210## OpenTelemetry export
211
212Deployed apps export traces, logs, and metrics over OTLP from every
213runtime — Worker, Durable Object, Workflow, Lambda, Containers, ECS
214Task ([#860](https://github.com/alchemy-run/alchemy/pull/860)).
215Telemetry is a Layer, composed into the Function/Worker's
216`Effect.provide` like any other binding:
217
218```typescript
219Effect.provide(
220 Layer.mergeAll(
221 Cloudflare.R2.ReadWriteBucketBinding,
222 Axiom.Telemetry({ token: Ingest, traces: Traces, logs: Logs }),
223 Alchemy.Telemetry.layerOtlp({
224 url: "https://api.honeycomb.io",
225 headers: { "x-honeycomb-team": apiKey },
226 }),
227 ),
228)
229```
230
231`Axiom.Telemetry` is sugar over the generic
232`Alchemy.Telemetry.layerOtlp`, which speaks to any OTLP backend.
233Exporters compose: merge several and every destination receives the
234telemetry with agreeing trace ids. Without a telemetry layer,
235Effect's no-op tracer keeps instrumentation free.
236
237Docs: [Telemetry](/infrastructure-as-effects/telemetry).
238
239## Prisma provider
240
241`alchemy/Prisma` provisions Prisma Postgres and Prisma Compute
242([#416](https://github.com/alchemy-run/alchemy/pull/416)) — thanks
243[Aman Varshney](https://github.com/AmanVarshney01)!
244
245```typescript
246const project = yield* Prisma.Project("Project", { createDatabase: false });
247const postgres = yield* Prisma.Postgres("Postgres", { project });
248const connection = yield* Prisma.Connection("Connection", {
249 database: postgres,
250});
251
252const app = yield* Prisma.Compute("App", {
253 project,
254 path: "./app",
255 build: { command: "bun run build", outdir: ".output" },
256 env: { DATABASE_URL: connection.databaseUrl },
257});
258```
259
260`Prisma.Compute` is also an effectful runtime, like a Cloudflare
261Worker or a Lambda: pass `main` and an Effect body, and the app is
262bundled from your Stack file with the `Connect` binding wiring the
263database in:
264
265```typescript
266export default Prisma.Compute(
267 "Api",
268 { project, main: import.meta.filename },
269 Effect.gen(function* () {
270 const db = yield* Prisma.Connect(connection);
271 const sql = yield* SQL.Postgres({ url: db.databaseUrl });
272
273 return {
274 fetch: Effect.gen(function* () {
275 const users = yield* sql`SELECT * FROM users`;
276 return yield* HttpServerResponse.json(users);
277 }),
278 };
279 }).pipe(Effect.provide(Prisma.ConnectBinding)),
280);
281```
282
283Resources reconcile from observed API state, recover from
284interrupted deployments, and never persist Prisma credentials as
285plaintext.
286
287Docs: [Prisma](/prisma).
288
289## Bedrock LanguageModel
290
291`AWS.Bedrock.LanguageModel` turns any Bedrock model into an
292`effect/unstable/ai` `LanguageModel` Layer
293([#919](https://github.com/alchemy-run/alchemy/pull/919)), so
294`generateText`, `streamText`, toolkits, and `Chat` run against
295Bedrock unchanged:
296
297```typescript
298const model = yield* AWS.Bedrock.LanguageModel([
299 "us.anthropic.claude-sonnet-4-20250514-v1:0",
300]);
301
302return {
303 fetch: Effect.gen(function* () {
304 const response = yield* LanguageModel.generateText({ prompt });
305 return yield* HttpServerResponse.json({ text: response.text });
306 }).pipe(Effect.provide(model)),
307};
308```
309
310The binding is the IAM boundary: which models the Function may call
311is fixed at deploy time. Model choice and inference parameters are
312per-call decisions via `AWS.Bedrock.withModelParameters`. The
313adapter speaks Bedrock's Converse API directly.
314
315Docs: [Bedrock](/aws/ai/bedrock).
316
317## Also in this release
318
319- **Assets-only Workers**
320 ([#969](https://github.com/alchemy-run/alchemy/pull/969),
321 [#970](https://github.com/alchemy-run/alchemy/pull/970)) — a
322 Worker with only an `assets` directory deploys with no script at
323 all, so static requests are free and never invoke a Worker. The
324 class form works without an impl.
325- **Headless OAuth login**
326 ([#962](https://github.com/alchemy-run/alchemy/pull/962)) —
327 `alchemy login` now works over SSH and in containers via a hosted
328 relay page: paste the code when the browser can't reach the CLI.
329 Thanks [BlankParticle](https://github.com/BlankParticle) (who
330 also fixed prompt cancellation in
331 [#968](https://github.com/alchemy-run/alchemy/pull/968))!
332- **Lambda Layers**
333 ([#971](https://github.com/alchemy-run/alchemy/pull/971)) —
334 `AWS.Lambda.LayerVersion` plus a `layers` prop on `Function`.
335 Thanks [Arya Saatvik](https://github.com/aryasaatvik)!
336- **SES email receiving**
337 ([#939](https://github.com/alchemy-run/alchemy/pull/939)) —
338 receipt rule sets, rules, IP filters, and a `SendBounce` binding;
339 guide at [/aws/email/receiving](/aws/email/receiving). Thanks
340 again [Arya Saatvik](https://github.com/aryasaatvik)!
341- **Docker Swarm**
342 ([#972](https://github.com/alchemy-run/alchemy/pull/972)) —
343 `Docker.Swarm`, `Docker.Context`, and `Docker.Service`, including
344 effectful services bundled and deployed from an Effect impl.
345 Thanks [Sergey Bekrin](https://github.com/sbekrin)!
346- **Container-backed Durable Objects on async Workers**
347 ([#956](https://github.com/alchemy-run/alchemy/pull/956)) — host
348 npm-packaged container DO classes like `@cloudflare/sandbox`'s
349 `Sandbox` via `DurableObject("Sandbox", { container })`.
350- **`Worker.URL`**
351 ([#964](https://github.com/alchemy-run/alchemy/pull/964)) — bind
352 a Worker's own public URL onto itself; resolves to the local dev
353 URL under `alchemy dev`.
354- **Destroy aggregates failures**
355 ([#973](https://github.com/alchemy-run/alchemy/pull/973)) — a
356 failing delete no longer aborts the destroy. Every delete is
357 attempted and one typed `DestroyError` reports what failed and
358 what was blocked by it.
359- **Dev Worker handoff is make-before-break**
360 ([#863](https://github.com/alchemy-run/alchemy/pull/863)) — the
361 previous workerd instance keeps serving until its replacement
362 starts, closing the 503 window on slow restarts (e.g. container
363 image prep).
364- **`Output` env secrets upload encrypted**
365 ([#943](https://github.com/alchemy-run/alchemy/pull/943)) — an
366 `Output<Redacted>` in a Worker's `env` now uploads as
367 `secret_text` instead of readable text, and `InferEnv` types are
368 preserved. Thanks [Seth Carlton](https://github.com/sethcarlton)!
369- **OAuth credentials refresh on expiry**
370 ([#966](https://github.com/alchemy-run/alchemy/pull/966)) — a dev
371 session that outlives the access token re-resolves credentials
372 instead of failing until restart (the "dev breaks after machine
373 sleep" hang).
374- **Worker read fan-out cut**
375 ([#932](https://github.com/alchemy-run/alchemy/pull/932),
376 [#944](https://github.com/alchemy-run/alchemy/pull/944),
377 [#975](https://github.com/alchemy-run/alchemy/pull/975)) —
378 subdomain lookups are memoized, route discovery is scoped to
379 known zones, and unmanaged custom domains converge to a `noop`
380 plan — fixing 429 rate-limit errors on accounts with many zones.
381- **Plain Workers default to `nodejs_compat`**
382 ([#796](https://github.com/alchemy-run/alchemy/pull/796)) — the
383 default previously reached only Effect-native Workers. The same
384 PR resolves `Config`/`Output` env values in `StaticSite` builds.
385- **ECS** — service reconciliation waits for rollout, drain, and
386 healthy ALB targets
387 ([#945](https://github.com/alchemy-run/alchemy/pull/945), thanks
388 [samnan-kradle](https://github.com/samnan-kradle)!),
389 `taskRoleManagedPolicyArns` attach on the `Service` form
390 ([#936](https://github.com/alchemy-run/alchemy/pull/936), thanks
391 [bweis](https://github.com/bweis)!), and `environmentFiles` joins
392 the shared task-definition surface
393 ([#904](https://github.com/alchemy-run/alchemy/pull/904)).
394- **`send_email` stubs locally in dev**
395 ([#865](https://github.com/alchemy-run/alchemy/pull/865)) —
396 `send()` logs to the dev console instead of delivering mail; pipe
397 the binding through `Alchemy.remote()` to send for real.
398- **Destroy removes the persisted stack output**
399 ([#965](https://github.com/alchemy-run/alchemy/pull/965)) —
400 cross-stack refs to a destroyed stage now fail with the typed
401 `InvalidReferenceError` instead of reading an empty husk.
402- **PlanetScale metadata-only updates no longer replace roles**
403 ([#957](https://github.com/alchemy-run/alchemy/pull/957)).
404- **CloudFront updates merge observed config**
405 ([#935](https://github.com/alchemy-run/alchemy/pull/935)) — ends
406 `IllegalUpdate` failures on members the props don't express.
407- **CLI** — the version-update warning prints before interactive
408 prompts ([#959](https://github.com/alchemy-run/alchemy/pull/959)),
409 and rolldown's native binding loads lazily so non-Cloudflare
410 stacks deploy without it
411 ([#955](https://github.com/alchemy-run/alchemy/pull/955)).
412- **Local dev fixes** — a Worker consuming a queue with a
413 dead-letter queue starts again
414 ([#1000](https://github.com/alchemy-run/alchemy/pull/1000)), and
415 `Container` env bindings no longer break Vite builds
416 ([#999](https://github.com/alchemy-run/alchemy/pull/999)).
417
418## Where to go next
419
420- [Local development](/environments/local-development)
421- [Gradual deployments](/cloudflare/compute/gradual-deployments)
422- [Custom domains](/cloudflare/networking/custom-domains)
423- [Telemetry](/infrastructure-as-effects/telemetry)
424- [Prisma](/prisma)
425- [Bedrock](/aws/ai/bedrock)
426- [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta66)
427- [Compare v2.0.0-beta.65 → v2.0.0-beta.66](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.65...v2.0.0-beta.66)