2.0.0-beta.66 - Local Emulation & Gradual Deployments
beta.66 makes alchemy dev fully local by default: KV, R2, D1, and
Queues are emulated on your machine, and Alchemy.remote() opts any
resource back onto the real cloud. Workers gain versions, gradual
deployments, and sticky sessions. The release also redesigns how
Worker URLs and domains are modeled, and adds OpenTelemetry export,
a Prisma provider, and a Bedrock LanguageModel binding.
Local emulation in alchemy dev
Section titled “Local emulation in alchemy dev”Provider mode (local vs live) is now a first-class engine concept
(#963). KV
Namespaces, R2 Buckets, D1 Databases, and Queues are emulated
locally during alchemy dev — they fabricate dev:-prefixed ids
with no cloud calls, and Worker bindings lower onto local workerd
simulators. D1 applies migrationsDir and importFiles against
the local database.
Alchemy.remote() opts a resource (or a whole scope) back onto the
real cloud during dev:
// emulated locally in devconst db = yield* Cloudflare.D1.Database("DB", {});
// real queue, even in devconst jobs = yield* Cloudflare.Queues.Queue("Jobs", {}).pipe( Alchemy.remote(),);Remote queues work in both directions: a local Worker can produce
to a live queue, and a local queue() handler drains a live queue
through an http_pull consumer.
The State Store now records whether each resource is local or
live. Switching between the two plans a replacement: alchemy deploy right after alchemy dev replaces local instances with
real cloud resources instead of updating over them. Node-side
capability clients (e.g. in Actions) reach the same local
simulators the Workers see, so a seed script writes to the same
local KV your Worker reads.
Docs: Local development.
Worker versions & gradual deployments
Section titled “Worker versions & gradual deployments”The version prop maps Cloudflare’s versions & gradual
deployments
onto alchemy stages
(#948).
Upload one stage’s code as a zero-traffic version of another stage’s Worker — the PR-preview workflow:
const parent = yield* Cloudflare.Worker.ref("Api", { stage: "staging" });
yield* Cloudflare.Worker("Api", { main: "./src/api.ts", version: { parent, message: `PR #${process.env.PR_NUMBER}` },});The version’s url is a stable aliased preview URL that re-points
at each newly uploaded version. Add traffic to canary a share of
the parent’s traffic, or roll out a Worker’s own deploy gradually:
version: { parent, traffic: 10 } // canary: 10% of staging's trafficversion: { traffic: 25 } // self-rollout: new version at 25%version.affinity pins each user to one version for the duration
of a rollout
(#985),
requested by Eric Clemmons:
version: { traffic: 10, // sticky by session cookie, falling back to sticky IP affinity: { cookie: "session_id", ip: true },}Docs: Gradual deployments.
Worker URLs redesigned
Section titled “Worker URLs redesigned”workersDev controls the workers.dev surface (and is actually
reconciled — the old subdomain prop was silently ignored), and
domain grows aliases and redirects
(#977):
const worker = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", workersDev: false, domain: { name: "example.com", aliases: ["www.example.com"], // also serve the Worker redirects: ["old.example.com"], // 301 → https://example.com },});Each redirect hostname gets managed DNS and an edge certificate, plus a zone redirect rule that 301s (path and query preserved) before Workers run — redirected requests never invoke the Worker.
urls lists every URL that serves the Worker, most significant
first, and url is always urls[0]:
// workers.dev only (the default)["https://api.my-team.workers.dev"]
// custom domain — outranks workers.dev["https://example.com", "https://api.my-team.workers.dev"]
// domain with aliases, workersDev: false["https://example.com", "https://www.example.com"]
// version worker (version.parent) — the stable aliased preview// URL first, then this upload's per-version URL[ "https://pr-42-api.my-team.workers.dev", "https://a1b2c3d4-api.my-team.workers.dev",]
// gradual rollout (version: { traffic }) — the uploaded version's// preview URL trails[ "https://example.com", "https://api.my-team.workers.dev", "https://a1b2c3d4-api.my-team.workers.dev",]
// alchemy dev — the dev server's actual surface["http://localhost:5173", "http://192.168.1.23:5173"]Redirect hostnames never appear in urls — they serve no content.
That makes urls the CORS allow-list:
const site = yield* Cloudflare.Website.Vite("Site", { ... });
// allow the site — on every URL that serves it — to call the APIconst api = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", env: { ALLOWED_ORIGINS: site.urls },});In production that allow-list is the site’s custom domain, aliases,
and workers.dev URL. Under alchemy dev it is the Vite dev
server’s Local and Network URLs — localhost plus the LAN address
(http://192.168…) — so a phone on your Wi-Fi can load the site
and call the API.
Docs: Custom domains.
OpenTelemetry export
Section titled “OpenTelemetry export”Deployed apps export traces, logs, and metrics over OTLP from every
runtime — Worker, Durable Object, Workflow, Lambda, Containers, ECS
Task (#860).
Telemetry is a Layer, composed into the Function/Worker’s
Effect.provide like any other binding:
Effect.provide( Layer.mergeAll( Cloudflare.R2.ReadWriteBucketBinding, Axiom.Telemetry({ token: Ingest, traces: Traces, logs: Logs }), Alchemy.Telemetry.layerOtlp({ url: "https://api.honeycomb.io", headers: { "x-honeycomb-team": apiKey }, }), ),)Axiom.Telemetry is sugar over the generic
Alchemy.Telemetry.layerOtlp, which speaks to any OTLP backend.
Exporters compose: merge several and every destination receives the
telemetry with agreeing trace ids. Without a telemetry layer,
Effect’s no-op tracer keeps instrumentation free.
Docs: Telemetry.
Prisma provider
Section titled “Prisma provider”alchemy/Prisma provisions Prisma Postgres and Prisma Compute
(#416) — thanks
Aman Varshney!
const project = yield* Prisma.Project("Project", { createDatabase: false });const postgres = yield* Prisma.Postgres("Postgres", { project });const connection = yield* Prisma.Connection("Connection", { database: postgres,});
const app = yield* Prisma.Compute("App", { project, path: "./app", build: { command: "bun run build", outdir: ".output" }, env: { DATABASE_URL: connection.databaseUrl },});Prisma.Compute is also an effectful runtime, like a Cloudflare
Worker or a Lambda: pass main and an Effect body, and the app is
bundled from your Stack file with the Connect binding wiring the
database in:
export default Prisma.Compute( "Api", { project, main: import.meta.filename }, Effect.gen(function* () { const db = yield* Prisma.Connect(connection); const sql = yield* SQL.Postgres({ url: db.databaseUrl });
return { fetch: Effect.gen(function* () { const users = yield* sql`SELECT * FROM users`; return yield* HttpServerResponse.json(users); }), }; }).pipe(Effect.provide(Prisma.ConnectBinding)),);Resources reconcile from observed API state, recover from interrupted deployments, and never persist Prisma credentials as plaintext.
Docs: Prisma.
Bedrock LanguageModel
Section titled “Bedrock LanguageModel”AWS.Bedrock.LanguageModel turns any Bedrock model into an
effect/unstable/ai LanguageModel Layer
(#919), so
generateText, streamText, toolkits, and Chat run against
Bedrock unchanged:
const model = yield* AWS.Bedrock.LanguageModel([ "us.anthropic.claude-sonnet-4-20250514-v1:0",]);
return { fetch: Effect.gen(function* () { const response = yield* LanguageModel.generateText({ prompt }); return yield* HttpServerResponse.json({ text: response.text }); }).pipe(Effect.provide(model)),};The binding is the IAM boundary: which models the Function may call
is fixed at deploy time. Model choice and inference parameters are
per-call decisions via AWS.Bedrock.withModelParameters. The
adapter speaks Bedrock’s Converse API directly.
Docs: Bedrock.
Also in this release
Section titled “Also in this release”- Assets-only Workers
(#969,
#970) — a
Worker with only an
assetsdirectory deploys with no script at all, so static requests are free and never invoke a Worker. The class form works without an impl. - Headless OAuth login
(#962) —
alchemy loginnow works over SSH and in containers via a hosted relay page: paste the code when the browser can’t reach the CLI. Thanks BlankParticle (who also fixed prompt cancellation in #968)! - Lambda Layers
(#971) —
AWS.Lambda.LayerVersionplus alayersprop onFunction. Thanks Arya Saatvik! - SES email receiving
(#939) —
receipt rule sets, rules, IP filters, and a
SendBouncebinding; guide at /aws/email/receiving. Thanks again Arya Saatvik! - Docker Swarm
(#972) —
Docker.Swarm,Docker.Context, andDocker.Service, including effectful services bundled and deployed from an Effect impl. Thanks Sergey Bekrin! - Container-backed Durable Objects on async Workers
(#956) — host
npm-packaged container DO classes like
@cloudflare/sandbox’sSandboxviaDurableObject("Sandbox", { container }). Worker.URL(#964) — bind a Worker’s own public URL onto itself; resolves to the local dev URL underalchemy dev.- Destroy aggregates failures
(#973) — a
failing delete no longer aborts the destroy. Every delete is
attempted and one typed
DestroyErrorreports what failed and what was blocked by it. - Dev Worker handoff is make-before-break (#863) — the previous workerd instance keeps serving until its replacement starts, closing the 503 window on slow restarts (e.g. container image prep).
Outputenv secrets upload encrypted (#943) — anOutput<Redacted>in a Worker’senvnow uploads assecret_textinstead of readable text, andInferEnvtypes are preserved. Thanks Seth Carlton!- OAuth credentials refresh on expiry (#966) — a dev session that outlives the access token re-resolves credentials instead of failing until restart (the “dev breaks after machine sleep” hang).
- Worker read fan-out cut
(#932,
#944,
#975) —
subdomain lookups are memoized, route discovery is scoped to
known zones, and unmanaged custom domains converge to a
noopplan — fixing 429 rate-limit errors on accounts with many zones. - Plain Workers default to
nodejs_compat(#796) — the default previously reached only Effect-native Workers. The same PR resolvesConfig/Outputenv values inStaticSitebuilds. - ECS — service reconciliation waits for rollout, drain, and
healthy ALB targets
(#945, thanks
samnan-kradle!),
taskRoleManagedPolicyArnsattach on theServiceform (#936, thanks bweis!), andenvironmentFilesjoins the shared task-definition surface (#904). send_emailstubs locally in dev (#865) —send()logs to the dev console instead of delivering mail; setdev: { remote: true }to send for real.- Destroy removes the persisted stack output
(#965) —
cross-stack refs to a destroyed stage now fail with the typed
InvalidReferenceErrorinstead of reading an empty husk. - PlanetScale metadata-only updates no longer replace roles (#957).
- CloudFront updates merge observed config
(#935) — ends
IllegalUpdatefailures on members the props don’t express. - CLI — the version-update warning prints before interactive prompts (#959), and rolldown’s native binding loads lazily so non-Cloudflare stacks deploy without it (#955).
- Local dev fixes — a Worker consuming a queue with a
dead-letter queue starts again
(#1000), and
Containerenv bindings no longer break Vite builds (#999).