1---
2title: 2.0.0-beta.70 - Web Frameworks & Full Local Dev
3date: 2026-08-06T09:00:00Z
4category: release
5excerpt: v2.0.0-beta.70 deploys Next.js, Astro, Nuxt, SvelteKit, Waku, and Octane to Cloudflare Workers with zero Cloudflare-specific config, completes local emulation in alchemy dev (browser rendering, images, email, cron, secrets, Stream, tail consumers), adds renamedFrom() state migration for logical ID renames, plan-time data sources, cluster-agnostic Kubernetes workloads, MySQL clients, and migrates every SDK to distilled v1.
6---
7
8beta.70 deploys **Next.js, Astro, Nuxt, SvelteKit, Waku, and
9Octane** to Cloudflare Workers — programmatic builds, no
10`wrangler.json`, no adapter config. And `alchemy dev` now emulates
11effectively the entire Cloudflare surface locally: browser
12rendering, image transforms, email, cron triggers, secrets,
13Stream, and tail consumers join KV, R2, D1, and Queues. The
14release also adds plan-time data sources, moves Kubernetes
15workloads out of EKS into a cluster-agnostic namespace, and
16migrates every SDK to distilled v1.
17
18This post covers beta.67 through beta.70 — beta.67 was a small
19fix release that didn't get its own notes, and beta.68/beta.69
20are superseded by beta.70.
21
22:::caution
23**Behavior change**
24
25`Cloudflare.Website.StaticSite` no longer duplicates `env`
26resources into its own namespace
27([#1053](https://github.com/alchemy-run/alchemy/pull/1053)) — a
28shared KV namespace passed as `env` used to register a second
29physical copy at `<site-id>/…`. As part of the fix, the site's
30Worker moves from the `<id>/Worker` FQN to `<id>` — and thanks to
31the new [`renamedFrom()`](#rename-resources-with-renamedfrom)
32support, its state migrates in place on your next deploy: same
33physical name, same `workers.dev` subdomain, nothing recreated.
34Thanks [apostoli](https://github.com/apostolos-geyer)!
35:::
36
37## The Website family: six new frameworks
38
39`Cloudflare.Website` grows from Vite and StaticSite to a full
40framework family:
41[`Nextjs`](/cloudflare/frontend/nextjs)
42([#923](https://github.com/alchemy-run/alchemy/pull/923)),
43[`Astro`](/cloudflare/frontend/astro),
44[`Nuxt`](/cloudflare/frontend/nuxt),
45[`SvelteKit`](/cloudflare/frontend/sveltekit),
46[`Waku`](/cloudflare/frontend/waku)
47([#1086](https://github.com/alchemy-run/alchemy/pull/1086)), and
48[`Octane`](/cloudflare/frontend/octane)
49([#1093](https://github.com/alchemy-run/alchemy/pull/1093)).
50
51Every resource builds your project programmatically — no wrangler
52binary, no `wrangler.json`, no adapter packages to install — and
53deploys it as a Worker: server bundle plus static assets. Your
54framework's own config file (`astro.config.*`, `vite.config.ts`,
55`nuxt.config.ts`, ...) loads natively; Alchemy merges its
56Cloudflare config over it. Builds are memoized by content-hashing
57the input files, so an unchanged project skips the build and
58deploy entirely.
59
60```typescript
61const site = yield* Cloudflare.Website.Nextjs("Site", {
62 env: {
63 GREETING: "Hello from Alchemy!",
64 UPLOADS: bucket, // any binding, via getCloudflareContext().env
65 },
66 domain: "example.com",
67});
68```
69
70Each resource returns a plain `Worker`, so the entire Worker
71vocabulary applies: `env` takes any binding (KV, R2, Durable
72Objects, secrets), plus `domain`, `compatibility`, and asset
73routing. Framework specifics are handled per resource:
74
75**Next.js** builds through the OpenNext pipeline. With a KV
76incremental cache and a Durable Object queue, **writable ISR**
77fully works — time-based regeneration and on-demand
78`revalidatePath`:
79
80```typescript
81const site = yield* Cloudflare.Website.Nextjs("Site", {
82 env: {
83 NEXT_INC_CACHE_KV: incCache,
84 NEXT_TAG_CACHE_KV: tagCache,
85 NEXT_CACHE_DO_QUEUE: Cloudflare.DurableObject("NEXT_CACHE_DO_QUEUE", {
86 className: "DOQueueHandler", // ships in the OpenNext bundle
87 }),
88 },
89});
90```
91
92**Astro** defaults to server output and auto-provisions a KV
93namespace for zero-config
94[sessions](https://docs.astro.build/en/guides/sessions/), bound
95as `SESSION`:
96
97```typescript
98const site = yield* Cloudflare.Website.Astro("Site", {
99 env: { CACHE: kv, UPLOADS: bucket }, // via Astro.locals.runtime.env
100});
101```
102
103**Nuxt** builds through nitro's `cloudflare_module` preset and
104natively loads your `nuxt.config.ts` — the `nuxt` prop merges
105over it:
106
107```typescript
108const site = yield* Cloudflare.Website.Nuxt("Site", {
109 nuxt: { routeRules: { "/about": { prerender: true } } },
110});
111```
112
113**SvelteKit** uses an in-memory Cloudflare adapter — no
114`svelte.config.js` changes, no `@sveltejs/adapter-cloudflare`:
115
116```typescript
117const site = yield* Cloudflare.Website.SvelteKit("Site", {
118 env: { API_KEY: Alchemy.secret("API_KEY") }, // via platform.env
119});
120```
121
122**Waku** serves RSC SSR at request time and prerenders SSG pages
123at build time inside workerd. A custom entry can even host
124Durable Objects alongside waku's fetch handler:
125
126```typescript
127const app = yield* Cloudflare.Website.Waku("App", {
128 main: "src/worker-entry.ts", // optional custom entry
129 env: { COUNTER: Cloudflare.DurableObject("Counter") },
130});
131```
132
133**Octane** deploys [OctaneJS](https://octanejs.dev) fullstack
134apps through Octane's own Cloudflare adapter:
135
136```typescript
137const site = yield* Cloudflare.Website.Octane("Site", {
138 env: { CACHE: cache }, // via context.platform.env
139});
140```
141
142All six run under `alchemy dev` on their framework's own dev
143server — Turbopack HMR for Next.js (`nextjs: { devMode: "hmr" }`,
144or the default preview parity under workerd), Vite HMR for the
145rest — with real bindings resolved in-process.
146
147Docs: [Frontend frameworks](/cloudflare/frontend/frontends).
148
149## `alchemy dev` covers the whole surface
150
151beta.66 emulated KV, R2, D1, and Queues locally. beta.70 finishes
152the campaign
153([#1039](https://github.com/alchemy-run/alchemy/pull/1039)) —
154miniflare parity and beyond:
155
156- **Cron triggers fire in dev** — `crons` schedule real timers,
157 and the miniflare-compatible `/cdn-cgi/handler/scheduled` route
158 triggers them manually. Previously `scheduled()` never ran under
159 `alchemy dev`.
160- **Browser Rendering** — `env.BROWSER` drives a real local Chrome
161 over CDP; `@cloudflare/puppeteer` works unchanged.
162- **Images** — `env.IMAGES` transforms run through sharp locally.
163- **Email, both directions** — `send_email` validates and persists
164 sent mail as `.eml` files, and a `POST` to
165 `/cdn-cgi/handler/email` drives your `email()` handler with
166 accept, reject, and reply semantics.
167- **Secrets Store** — a Worker binding a `secrets_store_secret`
168 now boots in dev, with the real (Redacted-sourced) value seeded
169 into the simulator. `secret_key` CryptoKey bindings boot too,
170 and gain a user-facing resource: `Cloudflare.Workers.SecretKey`.
171- **Stream** — `Cloudflare.Stream.Stream("STREAM")` is a new
172 binding surface entirely (alchemy previously had none), working
173 live and against a local video-store simulator in dev.
174- **Tail consumers** — the new `tailConsumers` prop uploads as
175 `tail_consumers` metadata live, and a local producer's trace
176 events reach a local tail Worker in dev.
177- **Workflows and Hyperdrive** are now first-class dual providers,
178 so switching between dev and deploy is an engine-orchestrated
179 replacement instead of ad-hoc branching.
180
181`Alchemy.remote()` is now the single way to opt a binding onto the
182real cloud during dev
183([#1065](https://github.com/alchemy-run/alchemy/pull/1065)).
184Worker-only bindings gained `pipe()`, so the same aspect works on
185plain binding values in an async Worker's `env`:
186
187```typescript
188// Effect-native Worker
189const browser = yield* Cloudflare.Browser("BROWSER").pipe(Alchemy.remote());
190
191// async Worker env
192env: { IMAGES: Cloudflare.Images.Images("IMAGES").pipe(Alchemy.remote()) }
193```
194
195And local and live compose into **hybrid topologies**. Pin a queue
196live and everything around it stays local — the local Worker
197produces real messages into the real queue, and its local consumer
198drains the same queue through a pull loop, with the usual
199batching, retry, and dead-letter semantics:
200
201```typescript
202// the queue is real, even in dev
203const Jobs = Cloudflare.Queues.Queue("Jobs").pipe(Alchemy.remote());
204
205// the worker is local: it produces into the real queue, and its
206// consumer drains the real queue through a pull loop
207export default Cloudflare.Worker("Processor", { main: import.meta.url },
208 Effect.gen(function* () {
209 // subscribe and process messages from the real queue in the local worker
210 yield* Cloudflare.Queues.consumeQueueMessages(jobs, (stream) =>
211 Stream.runForEach(stream, (msg) => processJob(msg.body)),
212 );
213 }),
214) {}
215```
216
217Your dev session sees the same traffic as staging — messages
218produced by deployed services or a teammate's session land in
219your local handler, and your local edits process them instantly.
220
221Docs: [Local development](/environments/local-development).
222
223## Vite dev servers run in child processes
224
225Under `alchemy dev`, every Vite dev server now runs in its own
226child process
227([#1076](https://github.com/alchemy-run/alchemy/pull/1076))
228because much of Vite's plugin ecosystem resolves paths against
229`process.cwd()`. Config loaders, framework plugins, and tailwind
230all broke when the stack ran from a different directory than the
231site. Each server now gets its project root as its working
232directory, so all of that just works, and multiple sites in one
233stack no longer interfere with each other.
234
235Each server's output streams into the dev console prefixed with
236its worker name, so interleaved logs from several sites stay
237readable. The work also surfaced an upstream Vite bug in
238`server.port` handling, fixed in
239[vitejs/vite#23158](https://github.com/vitejs/vite/pull/23158).
240
241## Rename resources with `renamedFrom()`
242
243Changing a resource's logical ID used to plan a **replacement** —
244a new resource created under the new ID, the old one (and its
245data) deleted. `Alchemy.renamedFrom`
246([#1108](https://github.com/alchemy-run/alchemy/pull/1108))
247declares the id(s) a resource was previously registered under, and
248the engine migrates the persisted state row instead:
249
250```diff lang="typescript"
251-const bucket = yield* Cloudflare.R2.Bucket("Bucket");
252+const bucket = yield* Cloudflare.R2.Bucket("Assets").pipe(
253+ Alchemy.renamedFrom("Bucket"),
254+);
255```
256
257The next deploy plans a single re-branding update — same instance
258ID, same physical name, nothing created or deleted in the cloud:
259
260```text
261Plan: 1 to update
262[Assets] update (renamed from Bucket)
263```
264
265A bare string resolves against the ambient namespace exactly like
266the `id` argument; `{ fqn: "..." }` addresses a move between
267namespaces; listing several former ids covers rename chains. The
268migration is deliberately conservative — types must match,
269ambiguous claims and rename cycles fail the plan, and a leftover
270row from an interrupted move is dropped state-only, never touching
271the cloud. This is what lets the StaticSite fix above migrate
272`<id>/Worker` → `<id>` without replacing your Worker.
273
274Docs: [Renaming Resources](/infrastructure-as-code/renaming).
275
276## Data sources: plan-time `execute`
277
278Every `Binding.Service` gains `execute(...)`
279([#1037](https://github.com/alchemy-run/alchemy/pull/1037)) —
280invoke a capability at plan/deploy time and get an `Output`, the
281Terraform data-source / Pulumi invoke shape. `AWS.EC2.getAmi` is
282the first:
283
284```typescript
285const instance = yield* AWS.EC2.Instance("web", {
286 imageId: AWS.EC2.getAmi({
287 owners: ["amazon"],
288 name: ["al2023-ami-2023.*"],
289 }).ImageId.as<string>(),
290 instanceType: "t3.micro",
291 subnetId: subnet.subnetId,
292});
293```
294
295The same contract still works as a runtime binding — `yield*
296AWS.EC2.GetAmi(opts)` inside a Function grants
297`ec2:DescribeImages` and looks images up at runtime. One
298capability, two execution times.
299
300Cloudflare VPC services use the same shape
301([#821](https://github.com/alchemy-run/alchemy/pull/821), thanks
302[Filip Falcon](https://github.com/filipfalcon)!) —
303`Cloudflare.VpcService.lookup({ name })` resolves a service
304managed outside the stack, and binding it (or a managed
305`VpcService`) to a Worker's `env` yields a `Fetcher` that tunnels
306into your private network:
307
308```typescript
309const worker = yield* Cloudflare.Worker("Worker", {
310 main: "./src/worker.ts",
311 env: {
312 VPC: Cloudflare.VpcService.lookup({ name: "my-vpc-service" })
313 },
314});
315```
316
317Docs: [Bindings — data sources](/infrastructure-as-effects/binding).
318
319## Kubernetes goes cluster-agnostic
320
321The Kubernetes workloads from beta.64 move out of `AWS.EKS.*`
322into a new `alchemy/Kubernetes` namespace
323([#986](https://github.com/alchemy-run/alchemy/pull/986)):
324`Deployment`, `Job`, `Manifest`, and `HelmChart` reference a
325cluster instead of being EKS resources. Pass an `AWS.EKS.Cluster`
326and auth (SigV4), the image registry (ECR), and workload identity
327(Pod Identity) resolve through its adapter — or point at anything
328`kubectl` can reach:
329
330```typescript
331const local = Kubernetes.KubeConfig({ context: "kind-dev" });
332
333yield* Kubernetes.Deployment("Api", {
334 cluster: local,
335 image: "ghcr.io/acme/api:v3",
336});
337```
338
339Exec credential plugins (`aws eks get-token`, `kubelogin`,
340`gke-gcloud-auth-plugin`) are honored, so EKS today and AKS/GKE
341via kubeconfig immediately. Add `Kubernetes.providers()` to your
342stack; the old `AWS.EKS.Deployment`-style exports keep compiling
343for one release as deprecated aliases, and existing state migrates
344in place.
345
346## distilled v1
347
348Every cloud API alchemy talks to goes through a
349[distilled](https://github.com/alchemy-run/distilled) SDK — typed
350Effect clients with every error in the type-level union. Until
351now, each provider's SDK came out of its own hand-rolled
352generator: one for Cloudflare, one for AWS, one for each
353OpenAPI-shaped API, each with its own quirks and its own bugs.
354
355distilled v1 replaces all of that with **one compiler**
356([#823](https://github.com/alchemy-run/alchemy/pull/823)). Every
357provider's native spec format — OpenAPI (shared by 13 providers),
358Cloudflare's API docs, AWS's official Smithy models, Google
359Discovery, Azure ARM, GraphQL introspection — is first converted
360into a standard [Smithy](https://smithy.io) model, and a single
361Smithy-to-SDK codegen compiles every model into its Effect client.
362Each provider package is just data: a spec converter config, a
363small trait/pagination/protocol vocabulary (Cloudflare's is ~115
364lines), and a thin runtime protocol seam (SigV4 for AWS, a
365generic bearer-REST protocol shared by 12 providers).
366
367The result is 20 packages and ~45k operations — 429 AWS services,
368all of Azure and GCP, Cloudflare, Kubernetes, Stripe, PostHog, and
369the long tail — all built, patched, and regenerated through the
370same pipeline. Fixing a mistyped error or a wrong response schema
371is now one JSON Patch against a Smithy model, in the same dialect
372regardless of provider, and adding a whole new provider is a spec
373config rather than a new generator.
374
375The migration was verified name-for-name against the v0 surfaces
376and by alchemy's full live test tree — nearly 6,000 tests against
377real cloud APIs — with zero SDK regressions.
378
379## MySQL for effect-sql and Drizzle
380
381`SQL.MySQL` and `Drizzle.MySQL` complete the SQL client matrix
382([#1063](https://github.com/alchemy-run/alchemy/pull/1063)) — the
383missing siblings of the Postgres and D1 clients:
384
385```typescript
386import * as SQL from "alchemy/SQL/MySQL";
387
388const hd = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
389const sql = yield* SQL.MySQL({ url: hd.connectionString });
390const users = yield* sql`SELECT * FROM users`;
391```
392
393On workerd the client automatically switches to MySQL's text
394protocol (Hyperdrive's proxy has no prepared statements) and
395eval-free row parsers; Lambda and containers keep prepared
396statements. Same lazy per-request pool lifecycle as Postgres.
397Docs: [SQL](/sql).
398
399## WorkerEntrypoint bindings
400
401Bind a specific `WorkerEntrypoint` class exported by another
402Worker — with optional `ctx.props` riding along
403([#1097](https://github.com/alchemy-run/alchemy/pull/1097)):
404
405```typescript
406const caller = yield* Cloudflare.Worker("Caller", {
407 main: "./src/caller.ts",
408 env: {
409 API: Cloudflare.WorkerEntrypoint(target, "Api"),
410 VENDOR: Cloudflare.WorkerEntrypoint(vendor, {
411 entrypoint: "Vendor",
412 props: { baseUrl: site.url }, // → the target's ctx.props
413 }),
414 },
415});
416```
417
418`alchemy dev` delivers `ctx.props` to the local target today.
419
420## Pure annotations are opt-in only
421
422The bundler annotates discarded-result calls as pure so
423minification can tree-shake them — that's what keeps effect's
424unused modules out of your Worker. It used to auto-extend that
425treatment to your own package when it declared
426`sideEffects: false`, which could delete top-level registrations
427like `app.get("/", handler)` under minification: the deploy
428succeeds, unbundled unit tests stay green, and every request
429404s.
430
431Auto-detection is removed entirely
432([#1021](https://github.com/alchemy-run/alchemy/pull/1021)) —
433thanks [Daniel Gangl](https://github.com/danielgangl)! `effect`,
434`@effect/*`, and alchemy's own packages stay annotated by
435default; any other package is only touched when you list it
436deliberately:
437
438```typescript
439build: {
440 pure: { packages: ["my-lib", "@my-scope/*"] },
441 // or pure: false to disable annotation entirely
442}
443```
444
445## Also in this release
446
447- **Access-managed OAuth applications**
448 ([#1023](https://github.com/alchemy-run/alchemy/pull/1023)) —
449 `oauthConfiguration` on `Access.Application`, including dynamic
450 client registration for MCP-style clients. Thanks
451 [Magoz](https://github.com/magoz)!
452- **Typed Access IdP configs**
453 ([#1002](https://github.com/alchemy-run/alchemy/pull/1002)) —
454 `IdentityProvider` props are a discriminated union on `type`, so
455 an `azureAD` IdP missing `directoryId` is a compile error; plus
456 zone scope and SAML certificate sets. Adoption of IdPs with
457 empty display names is fixed and `getIdentityProvider` joins as
458 a data source
459 ([#1099](https://github.com/alchemy-run/alchemy/pull/1099)).
460- **SESv2 sending coverage**
461 ([#976](https://github.com/alchemy-run/alchemy/pull/976)) —
462 identity policies, contact lists, tenants, dedicated IPs, and
463 account settings: 9 resources and 5 bindings. Thanks
464 [Arya Saatvik](https://github.com/aryasaatvik) (who also
465 repaired the AWS utility subpath exports in
466 [#1033](https://github.com/alchemy-run/alchemy/pull/1033))!
467- **Declarative Lambda versions**
468 ([#993](https://github.com/alchemy-run/alchemy/pull/993)) —
469 `AWS.Lambda.Version` and `Alias`, retain-by-default for
470 production-safe Durable Function deployments. Thanks again
471 [Arya Saatvik](https://github.com/aryasaatvik)! The engine now
472 re-resolves whole-resource references at apply so downstream
473 resources see fresh upstream attributes
474 ([#1068](https://github.com/alchemy-run/alchemy/pull/1068),
475 [#1070](https://github.com/alchemy-run/alchemy/pull/1070)).
476- **`alchemy state export`**
477 ([#1043](https://github.com/alchemy-run/alchemy/pull/1043)) —
478 every state record across stacks and stages as one JSON
479 document, deterministically ordered so exports diff cleanly.
480- **Drizzle on Durable Object SQLite**
481 ([#1031](https://github.com/alchemy-run/alchemy/pull/1031)) —
482 `Drizzle.DurableObject` applies drizzle-kit migrations inside a
483 DO, and `.sql` imports bundle as text modules.
484- **Lambda `build.install` pins from lockfiles**
485 ([#858](https://github.com/alchemy-run/alchemy/pull/858)) — the
486 installed dependency graph is resolved against your lockfile
487 (npm, pnpm, bun, yarn) via generated `overrides`, so deployed
488 artifacts can't drift. Thanks
489 [Joaquín Pérez](https://github.com/jperezrealini)!
490- **Effect error responses reach the client**
491 ([#1011](https://github.com/alchemy-run/alchemy/pull/1011)) — a
492 failed handler's `HttpServerResponse` is preserved instead of
493 being flattened to a bare 500. Thanks
494 [Patrik Duksin](https://github.com/patrikduksin) (who also fixed
495 local D1 migrations in
496 [#1009](https://github.com/alchemy-run/alchemy/pull/1009))!
497- **Engine reliability** — an interrupted first deploy no longer
498 bricks later plans with a `SchemaError`
499 ([#1005](https://github.com/alchemy-run/alchemy/pull/1005)), and
500 cyclic props and class instances no longer send the engine's
501 prop walkers into infinite recursion
502 ([#1094](https://github.com/alchemy-run/alchemy/pull/1094)).
503- **OCI Helm charts parse correctly**
504 ([#1042](https://github.com/alchemy-run/alchemy/pull/1042)) —
505 thanks [Andy Jefferson](https://github.com/microagi-andy)!
506- **PlanetScale migrations split same-line breakpoint markers**
507 ([#1062](https://github.com/alchemy-run/alchemy/pull/1062)) —
508 thanks [BEEIRL](https://github.com/beeirl)!
509- **Website & Vite fixes** — the assets directory is hashed during
510 plan diff so asset-only changes deploy
511 ([#1045](https://github.com/alchemy-run/alchemy/pull/1045)),
512 Vite's `base` applies to the assets manifest
513 ([#1048](https://github.com/alchemy-run/alchemy/pull/1048)),
514 Website uses the proper domain object with redirects
515 ([#1088](https://github.com/alchemy-run/alchemy/pull/1088)), dev
516 child ports are allocated ephemerally
517 ([#1090](https://github.com/alchemy-run/alchemy/pull/1090)), and
518 relative `rootDir`s hash stably
519 ([#1079](https://github.com/alchemy-run/alchemy/pull/1079)) —
520 thanks [Rahul Mishra](https://github.com/BlankParticle), who
521 also fixed relative DNS record matching in
522 [#1017](https://github.com/alchemy-run/alchemy/pull/1017)!
523- **Prisma provider aligned with the management API**
524 ([#1012](https://github.com/alchemy-run/alchemy/pull/1012)) —
525 thanks [Aman Varshney](https://github.com/AmanVarshney01)!
526- **`pg`, `mysql2`, `mongodb`, and `@vercel/nft` are optional
527 peer dependencies**
528 ([#1069](https://github.com/alchemy-run/alchemy/pull/1069),
529 [#1071](https://github.com/alchemy-run/alchemy/pull/1071)) —
530 install only what you use.
531
532## Where to go next
533
534- [Frontend frameworks](/cloudflare/frontend/frontends)
535- [Local development](/environments/local-development)
536- [Renaming Resources](/infrastructure-as-code/renaming)
537- [Bindings](/infrastructure-as-effects/binding)
538- [EKS & Kubernetes](/aws/compute/eks)
539- [SQL](/sql)
540- [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta70)
541- [Compare v2.0.0-beta.66 → v2.0.0-beta.70](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.66...v2.0.0-beta.70)