Skip to content

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.

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.

beta.66 emulated KV, R2, D1, and Queues locally. beta.68 finishes the campaign (#1039) — miniflare parity and beyond:

  • Cron triggers fire in devcrons schedule real timers, and the miniflare-compatible /cdn-cgi/handler/scheduled route triggers them manually. Previously scheduled() never ran under alchemy dev.
  • Browser Renderingenv.BROWSER drives a real local Chrome over CDP; @cloudflare/puppeteer works unchanged.
  • Imagesenv.IMAGES transforms run through sharp locally.
  • Email, both directionssend_email validates and persists sent mail as .eml files, and a POST to /cdn-cgi/handler/email drives your email() handler with accept, reject, and reply semantics.
  • Secrets Store — a Worker binding a secrets_store_secret now boots in dev, with the real (Redacted-sourced) value seeded into the simulator. secret_key CryptoKey bindings boot too, and gain a user-facing resource: Cloudflare.Workers.SecretKey.
  • StreamCloudflare.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 tailConsumers prop uploads as tail_consumers metadata 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 Worker
const browser = yield* Cloudflare.Browser("BROWSER").pipe(Alchemy.remote());
// async Worker env
env: { 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 dev
const 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 loop
export 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.

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.

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.

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.

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.

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.

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.

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
}
  • Access-managed OAuth applications (#1023) — oauthConfiguration on Access.Application, including dynamic client registration for MCP-style clients. Thanks Magoz!
  • Typed Access IdP configs (#1002) — IdentityProvider props are a discriminated union on type, so an azureAD IdP missing directoryId is a compile error; plus zone scope and SAML certificate sets. Adoption of IdPs with empty display names is fixed and getIdentityProvider joins 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.Version and Alias, 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.DurableObject applies drizzle-kit migrations inside a DO, and .sql imports bundle as text modules.
  • Lambda build.install pins from lockfiles (#858) — the installed dependency graph is resolved against your lockfile (npm, pnpm, bun, yarn) via generated overrides, so deployed artifacts can’t drift. Thanks Joaquín Pérez!
  • Effect error responses reach the client (#1011) — a failed handler’s HttpServerResponse is 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 base applies to the assets manifest (#1048), Website uses the proper domain object with redirects (#1088), dev child ports are allocated ephemerally (#1090), and relative rootDirs 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/nft are optional peer dependencies (#1069, #1071) — install only what you use.