2.0.0-beta.68 - Web Frameworks & Full Local Dev
beta.68 deploys Next.js, Astro, Nuxt, SvelteKit, Waku, and
Octane to Cloudflare Workers — programmatic builds, no
wrangler.json, no adapter config. And alchemy dev now emulates
effectively the entire Cloudflare surface locally: browser
rendering, image transforms, email, cron triggers, secrets,
Stream, and tail consumers join KV, R2, D1, and Queues. The
release also adds plan-time data sources, moves Kubernetes
workloads out of EKS into a cluster-agnostic namespace, and
migrates every SDK to distilled v1.
This post also folds in beta.67, a small fix release that didn’t get its own notes.
The Website family: six new frameworks
Section titled “The Website family: six new frameworks”Cloudflare.Website grows from Vite and StaticSite to a full
framework family:
Nextjs
(#923),
Astro,
Nuxt,
SvelteKit,
Waku
(#1086), and
Octane
(#1093).
Every resource builds your project programmatically — no wrangler
binary, no wrangler.json, no adapter packages to install or
config files to edit — and deploys it as a Worker: server bundle
plus static assets. Builds are memoized by content-hashing the
input files, so an unchanged project skips the build and deploy
entirely.
const site = yield* Cloudflare.Website.Nextjs("Site", { env: { GREETING: "Hello from Alchemy!", UPLOADS: bucket, // any binding, via getCloudflareContext().env }, domain: "example.com",});Each resource returns a plain Worker, so the entire Worker
vocabulary applies: env takes any binding (KV, R2, Durable
Objects, secrets), plus domain, compatibility, and asset
routing. Framework specifics are handled per resource:
Next.js builds through the OpenNext pipeline. With a KV
incremental cache and a Durable Object queue, writable ISR
fully works — time-based regeneration and on-demand
revalidatePath:
const site = yield* Cloudflare.Website.Nextjs("Site", { env: { NEXT_INC_CACHE_KV: incCache, NEXT_TAG_CACHE_KV: tagCache, NEXT_CACHE_DO_QUEUE: Cloudflare.DurableObject("NEXT_CACHE_DO_QUEUE", { className: "DOQueueHandler", // ships in the OpenNext bundle }), },});Astro defaults to server output and auto-provisions a KV
namespace for zero-config
sessions, bound
as SESSION:
const site = yield* Cloudflare.Website.Astro("Site", { env: { CACHE: kv, UPLOADS: bucket }, // via Astro.locals.runtime.env});Nuxt builds through nitro’s cloudflare_module preset and
natively loads your nuxt.config.ts — the nuxt prop merges
over it:
const site = yield* Cloudflare.Website.Nuxt("Site", { nuxt: { routeRules: { "/about": { prerender: true } } },});SvelteKit uses an in-memory Cloudflare adapter — no
svelte.config.js changes, no @sveltejs/adapter-cloudflare:
const site = yield* Cloudflare.Website.SvelteKit("Site", { env: { API_KEY: Alchemy.secret("API_KEY") }, // via platform.env});Waku serves RSC SSR at request time and prerenders SSG pages at build time inside workerd. A custom entry can even host Durable Objects alongside waku’s fetch handler:
const app = yield* Cloudflare.Website.Waku("App", { main: "src/worker-entry.ts", // optional custom entry env: { COUNTER: Cloudflare.DurableObject("Counter") },});Octane deploys OctaneJS fullstack apps through Octane’s own Cloudflare adapter:
const site = yield* Cloudflare.Website.Octane("Site", { env: { CACHE: cache }, // via context.platform.env});All six run under alchemy dev on their framework’s own dev
server — Turbopack HMR for Next.js (nextjs: { devMode: "hmr" },
or the default preview parity under workerd), Vite HMR for the
rest — with real bindings resolved in-process.
Docs: Frontend frameworks.
alchemy dev covers the whole surface
Section titled “alchemy dev covers the whole surface”beta.66 emulated KV, R2, D1, and Queues locally. beta.68 finishes the campaign (#1039) — miniflare parity and beyond:
- Cron triggers fire in dev —
cronsschedule real timers, and the miniflare-compatible/cdn-cgi/handler/scheduledroute triggers them manually. Previouslyscheduled()never ran underalchemy dev. - Browser Rendering —
env.BROWSERdrives a real local Chrome over CDP;@cloudflare/puppeteerworks unchanged. - Images —
env.IMAGEStransforms run through sharp locally. - Email, both directions —
send_emailvalidates and persists sent mail as.emlfiles, and aPOSTto/cdn-cgi/handler/emaildrives youremail()handler with accept, reject, and reply semantics. - Secrets Store — a Worker binding a
secrets_store_secretnow boots in dev, with the real (Redacted-sourced) value seeded into the simulator.secret_keyCryptoKey bindings boot too, and gain a user-facing resource:Cloudflare.Workers.SecretKey. - Stream —
Cloudflare.Stream.Stream("STREAM")is a new binding surface entirely (alchemy previously had none), working live and against a local video-store simulator in dev. - Tail consumers — the new
tailConsumersprop uploads astail_consumersmetadata live, and a local producer’s trace events reach a local tail Worker in dev. - Workflows and Hyperdrive are now first-class dual providers, so switching between dev and deploy is an engine-orchestrated replacement instead of ad-hoc branching.
Alchemy.remote() is now the single way to opt a binding onto the
real cloud during dev
(#1065).
Worker-only bindings gained pipe(), so the same aspect works on
plain binding values in an async Worker’s env:
// Effect-native Workerconst browser = yield* Cloudflare.Browser("BROWSER").pipe(Alchemy.remote());
// async Worker envenv: { IMAGES: Cloudflare.Images.Images("IMAGES").pipe(Alchemy.remote()) }And local and live compose into hybrid topologies. Pin a queue live and everything around it stays local — the local Worker produces real messages into the real queue, and its local consumer drains the same queue through a pull loop, with the usual batching, retry, and dead-letter semantics:
// the queue is real, even in devconst Jobs = Cloudflare.Queues.Queue("Jobs").pipe(Alchemy.remote());
// the worker is local: it produces into the real queue, and its// consumer drains the real queue through a pull loopexport default Cloudflare.Worker("Processor", { main: import.meta.url }, Effect.gen(function* () { // subscribe and process messages from the real queue in the local worker yield* Cloudflare.Queues.consumeQueueMessages(jobs, (stream) => Stream.runForEach(stream, (msg) => processJob(msg.body)), ); }),) {}Your dev session sees the same traffic as staging — messages produced by deployed services or a teammate’s session land in your local handler, and your local edits process them instantly.
Docs: Local development.
Vite dev servers run in child processes
Section titled “Vite dev servers run in child processes”Under alchemy dev, every Vite dev server now runs in its own
child process
(#1076)
because much of Vite’s plugin ecosystem resolves paths against
process.cwd(). Config loaders, framework plugins, and tailwind
all broke when the stack ran from a different directory than the
site. Each server now gets its project root as its working
directory, so all of that just works, and multiple sites in one
stack no longer interfere with each other.
Each server’s output streams into the dev console prefixed with
its worker name, so interleaved logs from several sites stay
readable. The work also surfaced an upstream Vite bug in
server.port handling, fixed in
vitejs/vite#23158.
Data sources: plan-time execute
Section titled “Data sources: plan-time execute”Every Binding.Service gains execute(...)
(#1037) —
invoke a capability at plan/deploy time and get an Output, the
Terraform data-source / Pulumi invoke shape. AWS.EC2.getAmi is
the first:
const instance = yield* AWS.EC2.Instance("web", { imageId: AWS.EC2.getAmi({ owners: ["amazon"], name: ["al2023-ami-2023.*"], }).ImageId.as<string>(), instanceType: "t3.micro", subnetId: subnet.subnetId,});The same contract still works as a runtime binding — yield* AWS.EC2.GetAmi(opts) inside a Function grants
ec2:DescribeImages and looks images up at runtime. One
capability, two execution times.
Cloudflare VPC services use the same shape
(#821, thanks
Filip Falcon!) —
Cloudflare.VpcService.lookup({ name }) resolves a service
managed outside the stack, and binding it (or a managed
VpcService) to a Worker’s env yields a Fetcher that tunnels
into your private network:
const worker = yield* Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { VPC: Cloudflare.VpcService.lookup({ name: "my-vpc-service" }) },});Docs: Bindings — data sources.
Kubernetes goes cluster-agnostic
Section titled “Kubernetes goes cluster-agnostic”The Kubernetes workloads from beta.64 move out of AWS.EKS.*
into a new alchemy/Kubernetes namespace
(#986):
Deployment, Job, Manifest, and HelmChart reference a
cluster instead of being EKS resources. Pass an AWS.EKS.Cluster
and auth (SigV4), the image registry (ECR), and workload identity
(Pod Identity) resolve through its adapter — or point at anything
kubectl can reach:
const local = Kubernetes.KubeConfig({ context: "kind-dev" });
yield* Kubernetes.Deployment("Api", { cluster: local, image: "ghcr.io/acme/api:v3",});Exec credential plugins (aws eks get-token, kubelogin,
gke-gcloud-auth-plugin) are honored, so EKS today and AKS/GKE
via kubeconfig immediately. Add Kubernetes.providers() to your
stack; the old AWS.EKS.Deployment-style exports keep compiling
for one release as deprecated aliases, and existing state migrates
in place.
distilled v1
Section titled “distilled v1”Every cloud API alchemy talks to goes through a distilled SDK — typed Effect clients with every error in the type-level union. Until now, each provider’s SDK came out of its own hand-rolled generator: one for Cloudflare, one for AWS, one for each OpenAPI-shaped API, each with its own quirks and its own bugs.
distilled v1 replaces all of that with one compiler (#823). Every provider’s native spec format — OpenAPI (shared by 13 providers), Cloudflare’s API docs, AWS’s official Smithy models, Google Discovery, Azure ARM, GraphQL introspection — is first converted into a standard Smithy model, and a single Smithy-to-SDK codegen compiles every model into its Effect client. Each provider package is just data: a spec converter config, a small trait/pagination/protocol vocabulary (Cloudflare’s is ~115 lines), and a thin runtime protocol seam (SigV4 for AWS, a generic bearer-REST protocol shared by 12 providers).
The result is 20 packages and ~45k operations — 429 AWS services, all of Azure and GCP, Cloudflare, Kubernetes, Stripe, PostHog, and the long tail — all built, patched, and regenerated through the same pipeline. Fixing a mistyped error or a wrong response schema is now one JSON Patch against a Smithy model, in the same dialect regardless of provider, and adding a whole new provider is a spec config rather than a new generator.
The migration was verified name-for-name against the v0 surfaces and by alchemy’s full live test tree — nearly 6,000 tests against real cloud APIs — with zero SDK regressions.
MySQL for effect-sql and Drizzle
Section titled “MySQL for effect-sql and Drizzle”SQL.MySQL and Drizzle.MySQL complete the SQL client matrix
(#1063) — the
missing siblings of the Postgres and D1 clients:
import * as SQL from "alchemy/SQL/MySQL";
const hd = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);const sql = yield* SQL.MySQL({ url: hd.connectionString });const users = yield* sql`SELECT * FROM users`;On workerd the client automatically switches to MySQL’s text protocol (Hyperdrive’s proxy has no prepared statements) and eval-free row parsers; Lambda and containers keep prepared statements. Same lazy per-request pool lifecycle as Postgres. Docs: SQL.
WorkerEntrypoint bindings
Section titled “WorkerEntrypoint bindings”Bind a specific WorkerEntrypoint class exported by another
Worker — with optional ctx.props riding along
(#1097):
const caller = yield* Cloudflare.Worker("Caller", { main: "./src/caller.ts", env: { API: Cloudflare.WorkerEntrypoint(target, "Api"), VENDOR: Cloudflare.WorkerEntrypoint(vendor, { entrypoint: "Vendor", props: { baseUrl: site.url }, // → the target's ctx.props }), },});alchemy dev delivers ctx.props to the local target today.
Pure annotations are opt-in only
Section titled “Pure annotations are opt-in only”The bundler annotates discarded-result calls as pure so
minification can tree-shake them — that’s what keeps effect’s
unused modules out of your Worker. It used to auto-extend that
treatment to your own package when it declared
sideEffects: false, which could delete top-level registrations
like app.get("/", handler) under minification: the deploy
succeeds, unbundled unit tests stay green, and every request
404s.
Auto-detection is removed entirely
(#1021) —
thanks Daniel Gangl! effect,
@effect/*, and alchemy’s own packages stay annotated by
default; any other package is only touched when you list it
deliberately:
build: { pure: { packages: ["my-lib", "@my-scope/*"] }, // or pure: false to disable annotation entirely}Also in this release
Section titled “Also in this release”- Access-managed OAuth applications
(#1023) —
oauthConfigurationonAccess.Application, including dynamic client registration for MCP-style clients. Thanks Magoz! - Typed Access IdP configs
(#1002) —
IdentityProviderprops are a discriminated union ontype, so anazureADIdP missingdirectoryIdis a compile error; plus zone scope and SAML certificate sets. Adoption of IdPs with empty display names is fixed andgetIdentityProviderjoins as a data source (#1099). - SESv2 sending coverage (#976) — identity policies, contact lists, tenants, dedicated IPs, and account settings: 9 resources and 5 bindings. Thanks Arya Saatvik (who also repaired the AWS utility subpath exports in #1033)!
- Declarative Lambda versions
(#993) —
AWS.Lambda.VersionandAlias, retain-by-default for production-safe Durable Function deployments. Thanks again Arya Saatvik! The engine now re-resolves whole-resource references at apply so downstream resources see fresh upstream attributes (#1068, #1070). alchemy state export(#1043) — every state record across stacks and stages as one JSON document, deterministically ordered so exports diff cleanly.- Drizzle on Durable Object SQLite
(#1031) —
Drizzle.DurableObjectapplies drizzle-kit migrations inside a DO, and.sqlimports bundle as text modules. - Lambda
build.installpins from lockfiles (#858) — the installed dependency graph is resolved against your lockfile (npm, pnpm, bun, yarn) via generatedoverrides, so deployed artifacts can’t drift. Thanks Joaquín Pérez! - Effect error responses reach the client
(#1011) — a
failed handler’s
HttpServerResponseis preserved instead of being flattened to a bare 500. Thanks Patrik Duksin (who also fixed local D1 migrations in #1009)! - Engine reliability — an interrupted first deploy no longer
bricks later plans with a
SchemaError(#1005), and cyclic props and class instances no longer send the engine’s prop walkers into infinite recursion (#1094). - OCI Helm charts parse correctly (#1042) — thanks Andy Jefferson!
- PlanetScale migrations split same-line breakpoint markers (#1062) — thanks BEEIRL!
- Website & Vite fixes — the assets directory is hashed during
plan diff so asset-only changes deploy
(#1045),
Vite’s
baseapplies to the assets manifest (#1048), Website uses the proper domain object with redirects (#1088), dev child ports are allocated ephemerally (#1090), and relativerootDirs hash stably (#1079) — thanks Rahul Mishra, who also fixed relative DNS record matching in #1017! - Prisma provider aligned with the management API (#1012) — thanks Aman Varshney!
pg,mysql2,mongodb, and@vercel/nftare optional peer dependencies (#1069, #1071) — install only what you use.