Skip to content

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.

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 dev
const db = yield* Cloudflare.D1.Database("DB", {});
// real queue, even in dev
const 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.

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 traffic
version: { 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.

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 API
const 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.

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.

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.

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.

  • Assets-only Workers (#969, #970) — a Worker with only an assets directory 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 login now 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.LayerVersion plus a layers prop on Function. Thanks Arya Saatvik!
  • SES email receiving (#939) — receipt rule sets, rules, IP filters, and a SendBounce binding; guide at /aws/email/receiving. Thanks again Arya Saatvik!
  • Docker Swarm (#972) — Docker.Swarm, Docker.Context, and Docker.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’s Sandbox via DurableObject("Sandbox", { container }).
  • Worker.URL (#964) — bind a Worker’s own public URL onto itself; resolves to the local dev URL under alchemy dev.
  • Destroy aggregates failures (#973) — a failing delete no longer aborts the destroy. Every delete is attempted and one typed DestroyError reports 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).
  • Output env secrets upload encrypted (#943) — an Output<Redacted> in a Worker’s env now uploads as secret_text instead of readable text, and InferEnv types 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 noop plan — 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 resolves Config/Output env values in StaticSite builds.
  • ECS — service reconciliation waits for rollout, drain, and healthy ALB targets (#945, thanks samnan-kradle!), taskRoleManagedPolicyArns attach on the Service form (#936, thanks bweis!), and environmentFiles joins the shared task-definition surface (#904).
  • send_email stubs locally in dev (#865) — send() logs to the dev console instead of delivering mail; set dev: { 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 InvalidReferenceError instead of reading an empty husk.
  • PlanetScale metadata-only updates no longer replace roles (#957).
  • CloudFront updates merge observed config (#935) — ends IllegalUpdate failures 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 Container env bindings no longer break Vite builds (#999).