Cloudflare.Workers reference
AccountSetting
Section titled “AccountSetting”Source:
src/Cloudflare/Workers/AccountSetting.ts
The account-wide Workers settings singleton
(/accounts/{account_id}/workers/account-settings): the default usage
model for new Workers and the Green Compute flag for scheduled Workers.
This is a singleton — it always exists on every account with Cloudflare
defaults, so this resource never creates or deletes anything physical.
Reconcile PUTs the settings when the observed values differ from the
desired ones; destroy restores the values the account had before Alchemy
first managed it (captured as initialDefaultUsageModel /
initialGreenCompute).
AccountSetting: Managing account settings
Section titled “AccountSetting: Managing account settings”Enable Green Compute for scheduled Workers
yield* Cloudflare.Workers.AccountSetting("GreenCompute", { greenCompute: true,});Pin the default usage model
yield* Cloudflare.Workers.AccountSetting("UsageModel", { defaultUsageModel: "standard", greenCompute: false,});Source:
src/Cloudflare/Workers/AI.ts
The native Cloudflare Workers AI binding — run inference on Workers AI
models directly from a Worker, with no AI Gateway (or any other cloud
resource) required. This is the plain { type: "ai" } Worker binding: the
runtime value is the same env.AI handle you would declare in
wrangler.json.
AI is a single value that is at once the Binding.Service tag, the
callable that produces an AIBinding, and the type. Declare it on a
Worker’s env (it flows through InferEnv → the runtime Ai handle) or
yield* it inside an Effect-native Worker to attach the binding and obtain
the AIClient.
Use Cloudflare.AI.Gateway + QueryGateway instead when you want requests
routed through an AI Gateway (caching, rate limiting, logs); use AI when
you just want to call Workers AI models.
AI: Effect-style Worker (recommended)
Section titled “AI: Effect-style Worker (recommended)”Cloudflare.Worker("AiWorker", { main: import.meta.url }, Effect.gen(function* () { const ai = yield* Cloudflare.Workers.AI(); return { fetch: Effect.gen(function* () { const result = yield* ai.run("@cf/meta/llama-3.3-70b-instruct-fp8-fast", { prompt: "What is the origin of the phrase Hello, World?", }).pipe(Effect.orDie); return yield* HttpServerResponse.json(result); }), }; }).pipe(Effect.provide(Cloudflare.Workers.AIBinding)),);AI: Effect AI LanguageModel
Section titled “AI: Effect AI LanguageModel”model(options) produces a Layer<LanguageModel, never, RuntimeContext>
that translates LanguageModel.generateText / streamText calls
(including tool calls) into ai.run(...) against the bound Workers AI
model — the same adapter AI Gateway’s QueryGateway uses, minus the
gateway routing.
const ai = yield* Cloudflare.Workers.AI();
const languageModel = ai.model({ model: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", parameters: { temperature: 0.7, maxTokens: 1024 },});
const response = yield* LanguageModel.generateText({ prompt }).pipe( Effect.provide(languageModel),);AI: Binding to an Async Worker
Section titled “AI: Binding to an Async Worker”export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { AI: Cloudflare.Workers.AI() },});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;// { AI: Ai }Browser
Section titled “Browser”Source:
src/Cloudflare/Workers/Browser.ts
A Cloudflare Browser Rendering binding for launching headless browser sessions from Workers — a Worker-only binding with no backing cloud resource.
Browser is a single value that is at once the Binding.Service tag, the
callable that produces a BrowserBinding, and the type. Declare it on a
Worker’s env (it flows through InferEnv → cf.BrowserRun) or yield* it
inside an Effect-native Worker to attach the binding and obtain the
BrowserClient.
Browser: Effect-style Worker (recommended)
Section titled “Browser: Effect-style Worker (recommended)”import * as Effect from "effect/Effect";
Cloudflare.Worker( "BrowserWorker", { main: import.meta.url }, Effect.gen(function* () { const browser = yield* Cloudflare.Browser("BROWSER");
return { fetch: Effect.gen(function* () { return yield* browser.markdown({ url: "https://example.com" }); }), }; }).pipe(Effect.provide(Cloudflare.Workers.BrowserBinding)),);Browser: Worker binding metadata
Section titled “Browser: Worker binding metadata”export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { BROWSER: Cloudflare.Browser() },});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;// { BROWSER: BrowserRun }Browser: Local development
Section titled “Browser: Local development”// Default: a real headless Chrome is launched locally and driven over// CDP under `alchemy dev`. Alchemy.remote() opts the binding into the// real Browser Rendering service instead — in an Effect-native Worker:const browser = yield* Cloudflare.Browser("BROWSER").pipe(Alchemy.remote());
// or declared on an async Worker's env:env: { BROWSER: Cloudflare.Browser("BROWSER").pipe(Alchemy.remote()) }Source:
src/Cloudflare/Workers/Cache.ts
Enable Workers Cache on the surrounding Worker and get the runtime cache client.
Yielding cache() in an Effect-native Worker’s init phase turns the cache
on at deploy time (the equivalent of setting cache: { enabled: true } on
the Worker’s props) and returns a client whose purge drives the runtime
ctx.cache API from your handlers.
What gets cached is controlled by standard response headers —
Cache-Control (including stale-while-revalidate), Cache-Tag for
tag-based purging, and Vary for content negotiation.
For async (non-Effect) Workers, set the cache prop on the Worker
instead.
cache: Examples
Section titled “cache: Examples”Effect.gen(function* () { // init: enable Workers Cache on this Worker const { purge } = yield* Cloudflare.cache();
return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; if (request.url.startsWith("/invalidate")) { yield* purge({ tags: ["products"] }); return HttpServerResponse.text("purged"); } return HttpServerResponse.text("hello", { headers: { "Cache-Control": "public, max-age=300", "Cache-Tag": "products", }, }); }), };})Source:
src/Cloudflare/Workers/CronEventSource.ts
Subscribe to Cloudflare Cron Triggers with an Effect handler.
A single call wires both pieces of a scheduled Worker:
-
Deploy-time: attaches the cron expression to the host Worker’s Cron Triggers.
-
Runtime: registers a
scheduledlistener that runs your Effect on each fire. The handler receives Cloudflare’sScheduledController, which has three members:controller.scheduledTime— the fire time in milliseconds since the Unix epoch.controller.cron— the cron expression that fired.controller.noRetry()— opts the invocation out of Cloudflare’s retry-on-failure. Only meaningful when the scheduled invocation can actually fail — see the failure & retry section below.
Each member has its own section below with an Effect example first and an async example second.
Requires CronEventSourceLive provided on the Worker’s Effect.
Failure & retry semantics: a failing handler won’t crash the Worker —
the event source catches the failure and moves on. That also means
Cloudflare never observes a failed invocation, so its platform-level
retry (and controller.noRetry()) never comes into play here. Express
retry declaratively with Effect.retry inside the handler, and log or
report errors if you need visibility into failed runs. In async Workers
the opposite holds: a scheduled handler that throws (or rejects) marks
the invocation failed and Cloudflare may retry it — call
controller.noRetry() before rethrowing to suppress that.
Async (non-Effect) Workers don’t use cron — they attach schedules with
the Worker’s crons prop and export their own scheduled handler from
the entry module (each section below includes the async variant). Pass
crons: [] to remove all Cron Triggers from a Worker.
cron: Declare a schedule
Section titled “cron: Declare a schedule”Effect-native Worker (recommended)
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
export default Cloudflare.Worker( "Worker", { main: import.meta.url }, Effect.gen(function* () { yield* Cloudflare.Workers.cron("0 12 * * *", () => Effect.log("cron fired"), );
return { fetch: Effect.succeed(HttpServerResponse.text("ok")), }; }).pipe(Effect.provide(Cloudflare.Workers.CronEventSourceLive)),);Async Worker — crons prop + exported scheduled handler
// alchemy.run.ts — attach the cron expressions at deploy timeexport const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", crons: ["0 12 * * *"],});
// src/worker.ts — the entry module handles the fires itselfexport default { async scheduled(controller: ScheduledController) { console.log("cron fired"); },};cron: controller.scheduledTime — the fire time
Section titled “cron: controller.scheduledTime — the fire time”Effect: record each fire on a Durable Object
export default class Worker extends Cloudflare.Worker<Worker>()( "Worker", { main: import.meta.url }, Effect.gen(function* () { const counters = yield* CronCounter;
yield* Cloudflare.Workers.cron("0 * * * *", (controller) => counters.getByName("default").record(controller.scheduledTime), );
return { fetch: Effect.gen(function* () { const { times } = yield* counters.getByName("default").snapshot(); return yield* HttpServerResponse.json({ times }); }), }; }).pipe(Effect.provide(Cloudflare.Workers.CronEventSourceLive)),) {}Async: use the fire time as an idempotency key
// scheduledTime is the time the fire was *scheduled* for (not when it// ran), so it is stable across retries of the same fire — a natural// idempotency key for at-most-once side effects.export default { async scheduled(controller: ScheduledController, env: WorkerEnv) { const key = `run:${controller.scheduledTime}`; if (await env.RUNS.get(key)) return; await syncFeeds(); await env.RUNS.put(key, "done"); },};cron: controller.cron — dispatch multiple schedules
Section titled “cron: controller.cron — dispatch multiple schedules”Effect: one handler per expression
// Each handler only runs for fires of its own expression — the listener// checks controller.cron, so a midnight fire never runs the hourly handler.yield* Cloudflare.Workers.cron("0 * * * *", () => syncFeeds);yield* Cloudflare.Workers.cron("0 0 * * *", () => purgeExpired);Async: switch on controller.cron
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", crons: ["0 * * * *", "0 0 * * *"],});
// src/worker.ts — one scheduled handler receives every fireexport default { async scheduled(controller: ScheduledController) { switch (controller.cron) { case "0 * * * *": await syncFeeds(); break; case "0 0 * * *": await purgeExpired(); break; } },};cron: controller.noRetry() — failure & retry control
Section titled “cron: controller.noRetry() — failure & retry control”Effect: bound retries with Effect.retry
import * as Schedule from "effect/Schedule";
// The event source reports the invocation as successful even when the// handler fails, so Cloudflare's platform retry rarely engages for Effect// handlers — Effect.retry is the primary retry control. Calling// controller.noRetry() once retries are exhausted defensively covers// anything that can still mark the invocation failed (e.g. a failing// waitUntil task).yield* Cloudflare.Workers.cron("0 * * * *", (controller) => syncFeeds.pipe( Effect.retry({ schedule: Schedule.exponential("1 second"), times: 3 }), Effect.tapError((error) => Effect.logError("syncFeeds failed permanently", error).pipe( Effect.andThen(Effect.sync(() => controller.noRetry())), ), ), ),);Async: suppress retry for permanent failures
// src/worker.ts — a thrown error marks the invocation failed and// Cloudflare may retry it; noRetry() opts this fire out of that.export default { async scheduled(controller: ScheduledController) { try { await syncFeeds(); } catch (error) { if (isPermanentFailure(error)) { controller.noRetry(); } throw error; // still recorded as a failed invocation } },};DurableObject
Section titled “DurableObject”Source:
src/Cloudflare/Workers/DurableObject.ts
A Cloudflare Durable Object namespace that manages globally unique, stateful instances with WebSocket hibernation support.
A Durable Object uses a two-phase pattern with two nested Effect.gen
blocks. The outer Effect resolves shared dependencies (other DOs,
containers, etc.) and the instance state reference
(Cloudflare.DurableObjectState). The inner Effect returns the
object’s public methods and WebSocket handlers — and is the only
place the state’s RuntimeContext-colored methods (storage.get,
storage.put, …) can actually run.
Effect.gen(function* () { // Phase 1: resolve shared dependencies + the instance state ref const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase); const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () { // Phase 2: per-instance setup and public API. `state`'s methods // (storage.get/put, …) are RuntimeContext-colored, so the actual // I/O happens here, in the runtime closure. return { save: (data: string) => db.exec("INSERT ..."), fetch: Effect.gen(function* () { ... }), webSocketMessage: Effect.fn(function* (ws, msg) { ... }), }; });})There are two ways to define a Durable Object. See the Runtime page for the full explanation.
- Inline — Effect implementation passed directly, single file.
- Modular — class and implementation in separate files for tree-shaking.
DurableObject: Inline Durable Objects
Section titled “DurableObject: Inline Durable Objects”Pass the Effect implementation as the second argument. This is the simplest approach — everything lives in one file. Convenient when the DO doesn’t need to be referenced by other Workers or DOs that would pull in its runtime dependencies.
export default class Counter extends Cloudflare.DurableObject<Counter>()( "Counter", Effect.gen(function* () { // init: bind resources + resolve the instance state ref const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase); const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () { // runtime: state's storage methods are RuntimeContext-colored, // so the reads/writes live here const count = (yield* state.storage.get<number>("count")) ?? 0;
return { increment: () => Effect.gen(function* () { const next = count + 1; yield* state.storage.put("count", next); return next; }), get: () => Effect.succeed(count), }; }); }),) {}DurableObject: Modular Durable Objects
Section titled “DurableObject: Modular Durable Objects”When a Worker and a DO reference each other, or multiple Workers
bind the same DO, define the class separately from its .make()
call. The class is a lightweight identifier; .make() provides
the runtime implementation as an export default. Rolldown treats
.make() as pure, so the bundler tree-shakes it and all its
runtime dependencies out of any consumer’s bundle.
The class and .make() can live in the same file. This is the
same pattern used by Worker and Container.
Modular Durable Object (class + .make() in one file)
export class Counter extends Cloudflare.DurableObject<Counter>()( "Counter",) {}
export default Counter.make( Effect.gen(function* () { // init: bind resources + resolve the instance state ref const db = yield* Cloudflare.D1.QueryDatabase(MyDatabase); const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () { // runtime: state's storage methods are RuntimeContext-colored, // so the reads/writes live here const count = (yield* state.storage.get<number>("count")) ?? 0;
return { increment: () => Effect.gen(function* () { const next = count + 1; yield* state.storage.put("count", next); yield* db.prepare("INSERT INTO logs (count) VALUES (?)").bind(next).run(); return next; }), get: () => Effect.succeed(count), }; }); }),);Binding a modular DO from a Worker
// imports Counter; bundler tree-shakes .make()import Counter from "./Counter.ts";
// initconst counters = yield* Counter;
return { fetch: Effect.gen(function* () { const counter = counters.getByName("user-123"); return HttpServerResponse.text(String(yield* counter.get())); }),};DurableObject: Cross-Worker Binding
Section titled “DurableObject: Cross-Worker Binding”A Durable Object is hosted by exactly one Worker, but any
number of other Workers can bind to the same DO. This is how
you share state across Workers: one Worker hosts the DO, every
other Worker addresses it by scriptName and gets a typed stub.
To make this type-safe, the host Worker must declare the DO
as part of its public contract via the third type argument to
Cloudflare.Worker<Self, Bindings, Deps>(). Deps is the set
of DO classes (or other Workers) the script exposes for other
scripts to bind to.
Host Worker declares the DO in its contract
// workerA.ts — hosts Counterimport { Counter, CounterLive } from "./object.ts";
// ^^^^^^^ declared as part of WorkerA's public contractexport class WorkerA extends Cloudflare.Worker<WorkerA, {}, Counter>()( "WorkerA", { main: import.meta.url },) {}
// WorkerA's Layer also provides the DO's Live implementation.export default WorkerA.make( Effect.gen(function* () { const counter = yield* Counter; return { fetch: Effect.gen(function* () { ... }) }; }).pipe(Effect.provide(CounterLive)),);Consumer Worker binds the DO via Counter.from(WorkerA)
// workerB.ts — binds to the same Counter, hosted by WorkerAimport { Counter } from "./object.ts";import { WorkerA } from "./workerA.ts";
export default class WorkerB extends Cloudflare.Worker<WorkerB>()( "WorkerB", { main: import.meta.url }, Effect.gen(function* () { // ^^^^^^^^^^^^ scriptName-bound stub of WorkerA's Counter const counter = yield* Counter.from(WorkerA); return { fetch: Effect.gen(function* () { const value = yield* counter.getByName("shared").get(); return HttpServerResponse.text(String(value)); }), }; }),) {}Only the host Worker’s Stack provides CounterLive — the
consumer Worker just imports the Counter class as a typed
identifier. Rolldown tree-shakes CounterLive (and its
dependencies) out of WorkerB’s bundle.
DurableObject: Using .from(Self) Inside the Host
Section titled “DurableObject: Using .from(Self) Inside the Host”Inside the host Worker, yield* Counter and
yield* Counter.from(Self) resolve to the same local namespace.
The .from(Self) form is preferred — especially in code that
may be extracted into a reusable Layer — because it makes the
scriptName explicit and lets the same Layer shape work whether
the consumer is the host or another script.
Counter.from(WorkerA) inside WorkerA itself
// workerA.ts — host uses `.from(Self)` instead of bare `yield* Counter`export default WorkerA.make( Effect.gen(function* () { const counter = yield* Counter.from(WorkerA); // same as `yield* Counter` return { fetch: Effect.gen(function* () { ... }) }; }).pipe(Effect.provide(CounterLive)),);A Worker can also host its own isolated namespace this way.
If a second host Worker declares Counter in its contract and
provides CounterLive, the DO instances under that script are
separate from the original host’s — same class, two namespaces.
Two hosts, two isolated namespaces
// workerC.ts — another host of Counter, isolated from WorkerAexport class WorkerC extends Cloudflare.Worker<WorkerC, {}, Counter>()( "WorkerC", { main: import.meta.url },) {}
export default WorkerC.make( Effect.gen(function* () { // .from(WorkerC) binds to WorkerC's own Counter namespace — // writes here are NOT visible from WorkerA's Counter. const counter = yield* Counter.from(WorkerC); return { fetch: Effect.gen(function* () { ... }) }; }).pipe(Effect.provide(CounterLive)),);DurableObject: RPC Methods
Section titled “DurableObject: RPC Methods”Any function you return from the inner Effect becomes an RPC method
that Workers can call through a stub. Methods must return an Effect.
The caller gets a fully typed stub — if your DO returns increment
and get, the stub exposes counter.increment() and counter.get().
return { increment: () => Effect.succeed(++count), get: () => Effect.succeed(count), reset: () => Effect.sync(() => { count = 0; }),};DurableObject: Returning Streams from RPC
Section titled “DurableObject: Returning Streams from RPC”RPC methods can return an Effect Stream and the caller will see
the chunks as they’re produced. Combine with Stream.schedule to
pace emission, or with Stream.fromQueue to bridge an inbound
subscription.
Streaming sequential numbers
import * as Schedule from "effect/Schedule";import * as Stream from "effect/Stream";
return { tick: (n: number) => Stream.iterate(0, (i) => i + 1).pipe( Stream.take(n), Stream.schedule(Schedule.spaced("100 millis")), ),};Forwarding the stream as a chunked HTTP response
// in a Worker fetch handlerconst counter = counters.getByName("tick");const stream = counter.tick(5).pipe( Stream.map((i) => `${i}\n`), Stream.encodeText,);return HttpServerResponse.stream(stream, { headers: { "content-type": "text/plain" },});DurableObject: Worker → DO HTTP forwarding
Section titled “DurableObject: Worker → DO HTTP forwarding”In addition to RPC methods, the typed stub exposes a fetch
method that forwards an HttpServerRequest straight to the DO.
The DO’s own fetch Effect produces the response — useful for
WebSocket upgrades and other request-shaped interactions.
const room = rooms.getByName(roomId);return yield* room.fetch(request);DurableObject: Placing a Durable Object in a Region
Section titled “DurableObject: Placing a Durable Object in a Region”A Durable Object is created wherever its first-ever request came from, and it stays there for life. That default is right for an instance whose traffic all comes from the user who created it, and wrong for one whose name is derived from something other than geography (a shard index, a hash, a fixed roster) — a Sydney user routed onto an instance that some Frankfurt request happened to create first pays a cross-planet round trip on every call.
Pass a locationHint to place the instance near the traffic you
expect instead. It only applies to creation: an instance that
already exists is unaffected, so a hint can’t move a live DO.
// Both the name and the hint derive from the caller's region, so// each shard is created in the region whose users address it.const region = hintFor(request.cf?.continent); // "apac", "weur", …const shard = shards.getByName(`pool:${region}:${index}`, { locationHint: region,});DurableObject: Accessing Instance State
Section titled “DurableObject: Accessing Instance State”Each Durable Object instance has its own transactional key-value
storage via Cloudflare.DurableObjectState. Resolve the state
reference in the outer (init) Effect, but call its methods —
storage.get, storage.put, … — only from the inner (runtime)
Effect: those methods are RuntimeContext-colored, so the type
system only allows them inside the runtime closure.
// inner (runtime) Effect — `state` was resolved in the outer Effectyield* state.storage.put("counter", 42);const value = yield* state.storage.get("counter");DurableObject: Background Work & Scopes
Section titled “DurableObject: Background Work & Scopes”Every RPC call and fetch into a Durable Object gets its own Effect
Scope. When the method finishes, the bridge closes that scope and
registers the close promise with workerd’s state.waitUntil — so
finalizers added with Effect.addFinalizer inside a method run after
the result is returned to the caller, without blocking it, and the
object stays alive until they settle.
For ad-hoc background work, state.waitUntil(effect) forks an Effect
with the caller’s full context and keeps the object alive until it
settles.
Attach cleanup to method scopes, not the constructor (init) closure —
the constructor runs once per in-memory instance under
blockConcurrencyWhile, and its scope is not tied to any call.
return { record: Effect.fn(function* (entry: string) { // runs after `record` returns; the DO stays alive until it settles yield* Effect.addFinalizer(() => state.storage.put(`audit:${entry}`, Date.now()).pipe(Effect.ignore), ); return "accepted" as const; }),
refresh: Effect.fn(function* () { // same idea, explicit form yield* state.waitUntil(recomputeExpensiveView()); return "scheduled" as const; }),};DurableObject: WebSocket Hibernation
Section titled “DurableObject: WebSocket Hibernation”Durable Objects support WebSocket hibernation — the runtime can
evict the object from memory while keeping connections open. Use
Cloudflare.upgrade() to accept a connection, and return
webSocketMessage / webSocketClose / webSocketError handlers to
process events when the object wakes back up.
Accepting a WebSocket connection
return { fetch: Effect.gen(function* () { const [response, socket] = yield* Cloudflare.upgrade(); socket.serializeAttachment({ id: crypto.randomUUID() }); return response; }),};Handling messages and close events
return { webSocketMessage: Effect.fn(function* ( socket: Cloudflare.WebSocket, message: string | Uint8Array, ) { const text = typeof message === "string" ? message : new TextDecoder().decode(message); // process the message }), webSocketClose: Effect.fn(function* ( ws: Cloudflare.WebSocket, code: number, reason: string, ) { yield* ws.close(code, reason); }), webSocketError: Effect.fn(function* ( ws: Cloudflare.WebSocket, error: unknown, ) { // the runtime closes the socket afterwards; clear its session here ws.serializeAttachment(null); }),};Recovering sessions after hibernation
Resolve the state reference in the outer Effect, but place the
rehydration loop (state.getWebSockets() is RuntimeContext-colored)
inside the inner Effect.gen so it runs every time the DO
instance is reconstructed (including after Cloudflare wakes the DO
from hibernation).
Effect.gen(function* () { const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () { const sessions = new Map<string, Cloudflare.WebSocket>();
// Rehydrate the in-memory session map after hibernation. for (const socket of yield* state.getWebSockets()) { const data = socket.deserializeAttachment<{ id: string }>(); if (data) sessions.set(data.id, socket); }
return { fetch: Effect.gen(function* () { const [response, socket] = yield* Cloudflare.upgrade(); const id = crypto.randomUUID(); socket.serializeAttachment({ id }); sessions.set(id, socket); return response; }), webSocketMessage: Effect.fn(function* (socket, message) { const text = typeof message === "string" ? message : new TextDecoder().decode(message); for (const peer of sessions.values()) { yield* peer.send(text); } }), }; });});DurableObject: Durable Callbacks
Section titled “DurableObject: Durable Callbacks”Register Alchemy.makeCallback handlers in the inner, per-instance Effect.
Durable Objects supply callback registration on their instance RuntimeContext
using SQLite and native alarms. Scheduling participates in the current storage
transaction, and each job is acknowledged only after its handler succeeds.
No explicit alarm handler is needed.
const state = yield* Cloudflare.DurableObjectState;return Effect.gen(function* () { const onArchive = yield* Alchemy.makeCallback( "archive", Effect.fn(function* (payload: { key: string; body: string }) { yield* archive.put(payload.key, payload.body); }), ); return { save: Effect.fn(function* (id: string, body: string) { yield* state.storage.transaction( Effect.gen(function* () { yield* state.storage.put(id, body); yield* onArchive.schedule(id, { after: "30 seconds", payload: { key: id, body }, }); }), ); }), };});Callbacks receive JSON-serializable payloads and deliver at least once, so
external writes must be idempotent. A recovery wake is persisted before each
attempt; configure its delay with the third argument, { retry: { delay: "1 minute" } }. Scheduling the same callback name and ID replaces the pending
job; onArchive.cancel(id) cancels it. Retain handlers for old callback names
while their jobs are pending. Each native alarm processes up to 100 due jobs;
direct setAlarm/deleteAlarm calls bypass the scheduler’s coordination.
Leave native alarm retries enabled when aborting an instance. Passing
{ retryAlarm: false } removes the automatic-recovery guarantee: Cloudflare
can suppress a replacement wake even after its timestamp is persisted. Jobs
remain stored, but may need an explicitly rearmed native alarm.
The scheduler migrates its original unversioned SQLite schema to version 1
atomically, preserving existing events. Old events still use the explicit
alarm handler below; their rows have no callback name to infer. Both APIs
coordinate the same native alarm. Unknown newer schema versions fail closed.
DurableObject: Scheduled Alarms
Section titled “DurableObject: Scheduled Alarms”Each Durable Object can have a single alarm timestamp. Alchemy
layers a small SQLite-backed scheduler on top via
Cloudflare.Workers.scheduleEvent and Cloudflare.Workers.processScheduledEvents,
so you can register many named events with arbitrary payloads and
fire them from a single alarm handler.
// schedule from a request or message handleryield* Cloudflare.Workers.scheduleEvent( "reminder-1", new Date(Date.now() + 60_000), { message: "your meeting starts in a minute" },);
return { alarm: () => Effect.gen(function* () { const fired = yield* Cloudflare.Workers.processScheduledEvents; for (const event of fired) { const payload = event.payload as { message: string }; // dispatch / broadcast / persist... } }),};DurableObject: Aborting a Durable Object
Section titled “DurableObject: Aborting a Durable Object”state.abort(reason?, options?) forcibly resets the isolate. By
default an in-progress alarm retries after the reset. Pass
{ retryAlarm: false } when the alarm should stop instead — for
example an alarm that deletes storage so the constructor does not
recreate it.
export class CleanupTask extends Cloudflare.DurableObject<CleanupTask>()( "CleanupTask", Effect.gen(function* () { const state = yield* Cloudflare.DurableObjectState;
return Effect.gen(function* () { // This won't be re-run after the alarm is aborted yield* state.storage.sql.exec(` CREATE TABLE IF NOT EXISTS foo ( id INTEGER PRIMARY KEY ) `);
return { alarm: () => Effect.gen(function* () { yield* state.storage.sql.exec("DROP TABLE foo"); yield* state.abort("Cleanup complete", { retryAlarm: false }); }), }; }); }),) {}DurableObject: Using from a Worker
Section titled “DurableObject: Using from a Worker”Yield the DO class in your Worker’s init phase to get a namespace
handle. Call getByName or getById to get a typed stub, then
call any RPC method or forward an HTTP request with fetch.
Calling RPC methods
// initconst counters = yield* Counter;
return { fetch: Effect.gen(function* () { const counter = counters.getByName("user-123"); yield* counter.increment(); const value = yield* counter.get(); return HttpServerResponse.text(String(value)); }),};Forwarding an HTTP request
// initconst rooms = yield* Room;
return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const room = rooms.getByName(roomId); return yield* room.fetch(request); }),};DurableObject: Binding in an Async Worker
Section titled “DurableObject: Binding in an Async Worker”When using an Async Worker (plain async fetch handler, no Effect
runtime), declare Durable Objects in the bindings prop of the
Worker resource. Pass a DurableObject reference with a
className matching the exported DurableObject subclass in your
worker source file. If className is omitted, it defaults to the
namespace name. Use Cloudflare.InferEnv to get a fully typed
env object that includes the namespace.
Declaring a DO binding in the stack
import type { Counter } from "./src/worker.ts";
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", bindings: { Counter: Cloudflare.DurableObject<Counter>("Counter"), },});Using the DO from a plain async handler
import { DurableObject } from "cloudflare:workers";import type { WorkerEnv } from "../alchemy.run.ts";
export default { async fetch(request: Request, env: WorkerEnv) { const counter = env.Counter.getByName("my-counter"); const count = await counter.increment(); return new Response(JSON.stringify({ count })); },};
export class Counter extends DurableObject { private counter = 0; async increment() { return ++this.counter; }}DurableObject: Cross-Script Binding in an Async Worker
Section titled “DurableObject: Cross-Script Binding in an Async Worker”Async Workers can also bind to a Durable Object hosted by another
Worker script. The host Worker declares and exports the DO class. The
consumer Worker declares a DurableObject with scriptName
set to the host Worker’s script name.
Cross-script async bindings are references only: the consumer uploads the binding metadata, but Alchemy does not drive class migrations for the foreign class. Deploy the host first so Cloudflare can verify that the target script exports the requested class.
Host Worker owns the Durable Object class
const host = yield* Cloudflare.Worker("Host", { main: "./src/host.ts", bindings: { Counter: Cloudflare.DurableObject<Counter>("Counter"), },});Consumer Worker binds to the host script
const consumer = yield* Cloudflare.Worker("Consumer", { main: "./src/consumer.ts", bindings: { Counter: Cloudflare.DurableObject<Counter>("Counter", { scriptName: host.workerName, }), },});Binding to a different exported class name
const consumer = yield* Cloudflare.Worker("Consumer", { main: "./src/consumer.ts", bindings: { Counter: Cloudflare.DurableObject<Counter>("Counter", { className: "CounterV2", scriptName: host.workerName, }), },});DurableObject: Container-Backed Durable Objects in an Async Worker
Section titled “DurableObject: Container-Backed Durable Objects in an Async Worker”A container-backed class is declared by binding a Cloudflare.Container
directly in the async Worker’s env — the Container is the Durable
Object binding plus its ContainerApplication. See the Async Workers
section on Container for the full walkthrough.
// `Sandbox` is the container-backed DO class exported by the worker// script (extends `@cloudflare/containers`' `Container`).import type { Sandbox } from "./src/worker.ts";
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { Sandbox: Cloudflare.Container<Sandbox>("Sandbox", { image: "docker.io/cloudflare/sandbox:0.1.3", }), },});DurableObject: Moving a Class Between Workers
Section titled “DurableObject: Moving a Class Between Workers”A Durable Object class can move from one Worker to another with its
data intact. The move is always declared — a class that disappears
from one worker and appears on another is otherwise ambiguous between
“transfer the data” and “delete it, start fresh”, so Alchemy never
guesses (removing a DO deletes it; that is the default). Declare
transferredFrom on the Durable Object at its new host, naming the
former host, and the new host’s deploy ships Cloudflare’s
data-preserving transferred_classes migration. The former host’s
deploy converges on its own — no delete migration is emitted for a
class that moved away.
Each transferredFrom entry is either the former host’s Worker
logical id (same stack + stage, resolved via alchemy’s ownership
tags) or its physical script name (required for cross-stack moves).
When no listed host holds the namespace — a fresh stage, or the
transfer already completed — the declaration is inert, so it is safe to
leave in place indefinitely.
Move a class from WorkerB to WorkerA
// BEFORE: worker-b hosts the classconst b = yield* Cloudflare.Worker("WorkerB", { main: "./src/worker-b.ts", // exports MyDOClass bindings: { MyDO: Cloudflare.DurableObject("MyDO", { className: "MyDOClass" }), },});
// AFTER: worker-a hosts it and declares where it came from;// worker-b keeps a cross-script reference.const a = yield* Cloudflare.Worker("WorkerA", { main: "./src/worker-a.ts", // now exports MyDOClass bindings: { MyDO: Cloudflare.DurableObject("MyDO", { className: "MyDOClass", transferredFrom: "WorkerB", // logical id (or its script name) }), },});const b = yield* Cloudflare.Worker("WorkerB", { main: "./src/worker-b.ts", // no longer exports MyDOClass bindings: { MyDO: Cloudflare.DurableObject("MyDO", { className: "MyDOClass", scriptName: a.workerName, }), },});Forgetting the declaration is safe: when the former host still
references the class cross-script, its deploy fails before any upload
with DurableObjectTransferRequired, telling you exactly what to
declare — data is never silently destroyed or forked.
Chained moves keep the host history
// The class moved WorkerB → WorkerA last release and WorkerA →// WorkerC this release. Keep the full history so a stage that lagged// behind (or skipped the intermediate release) still transfers from// wherever its namespace currently lives.Cloudflare.DurableObject("MyDO", { className: "MyDOClass", transferredFrom: ["WorkerB", "WorkerA"],})Two rules for multi-worker migrations:
- Pure moves (the former host drops the DO entirely, keeping no
cross-script reference) must be two deploys: first add the class to
the new host with
transferredFromand deploy; then remove it from the former host and deploy. In a single deploy nothing orders the transfer before the former host’s delete. - Cross-stack moves deploy the new host’s stack first, naming the former host by physical script name; the former host’s stack deploys after and converges.
DurableObject: Adopting an Existing Durable Object
Section titled “DurableObject: Adopting an Existing Durable Object”When you adopt a Worker that already exists on Cloudflare — created
outside Alchemy via Wrangler, the dashboard, or the raw API — its
Durable Object classes are adopted along with it. You opt in to the
takeover the same way you adopt any foreign resource: with
adopt(true) (or the --adopt CLI flag), since Worker.read reports
a worker with no Alchemy ownership tags as Unowned.
Alchemy normally tracks which class backs each binding through an
alchemy:dos: metadata tag it writes on the script (mapping each
binding’s logical id to its class name). A
foreign worker has no such tag, so on the adopting deploy Alchemy
falls back to matching your binding to the live class by binding
name. The class is then reused in place — not recreated — so
Cloudflare’s migration engine doesn’t reject the upload for creating a
class that already exists.
The consequence is a one-time constraint: on the adopting deploy the
binding’s className must match the class that already exists on the
worker. You cannot rename the class in the same deploy that adopts
it. Once the deploy completes, Alchemy owns the worker and has written
the alchemy:dos: tag, so subsequent renames are driven by logical id
and work normally.
Adopting a worker whose Counter class already exists
// The worker + `Counter` class were created outside Alchemy.// `className` must match the existing class on this first deploy.const worker = yield* Cloudflare.Worker("Worker", { name: "existing-worker", main: "./src/worker.ts", bindings: { Counter: Cloudflare.DurableObject<Counter>("Counter"), },}).pipe(adopt(true));Renaming the class — only after adoption
// A SECOND deploy, after the one above. Alchemy now owns the worker// and maps the binding by logical id, so the class can be renamed.const worker = yield* Cloudflare.Worker("Worker", { name: "existing-worker", main: "./src/worker.ts", bindings: { Counter: Cloudflare.DurableObject<Counter>("Counter", { className: "CounterV2", }), },});DurableObjectChatPersistence
Section titled “DurableObjectChatPersistence”Source:
src/Cloudflare/Workers/DurableObjectChatPersistence.ts
A BackingPersistence layer (Effect AI persistence module) backed
by the surrounding Durable Object’s state.storage. Drop-in storage
for Persistence.layerResultPersisted({ storeId }) so chat history,
cached AI responses, or any other persisted state lives in the DO
SQLite store with ${storeId}: key namespacing.
Multiple storeIds can coexist within a single Durable Object —
keys are namespaced with a ${storeId}: prefix so they don’t
collide.
DurableObjectChatPersistence: Wiring it into a chat-backing DO
Section titled “DurableObjectChatPersistence: Wiring it into a chat-backing DO”Persistence.layerResultPersisted({ storeId }) is the seam Effect
AI exposes for cached/replayable AI calls. Layer
DurableObjectChatPersistence underneath and every entry is stored
in the DO’s state.storage under the alchemy.chat: prefix.
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import { Chat, LanguageModel } from "effect/unstable/ai";import { Persistence } from "effect/unstable/persistence";
export default class ChatBackend extends Cloudflare.DurableObject<ChatBackend>()( "ChatBackend", Effect.gen(function* () { return Effect.gen(function* () { const persistence = yield* Persistence.layerResultPersisted({ storeId: "alchemy.chat", }).pipe(Layer.provide(Cloudflare.AI.DurableObjectChatPersistence));
return { send: (threadId: string, prompt: string) => LanguageModel.generateText({ prompt }).pipe(Effect.provide(persistence)), }; }); }),) {}DurableObjectChatPersistence: Multiple stores in the same DO
Section titled “DurableObjectChatPersistence: Multiple stores in the same DO”Different storeIds namespace their keys with ${storeId}:, so
one DO can keep, say, chat history and an audit log in separate
stores without colliding.
const aiPersistence = yield* Persistence.layerResultPersisted({ storeId: "alchemy.chat",}).pipe(Layer.provide(Cloudflare.AI.DurableObjectChatPersistence));
const auditPersistence = yield* Persistence.layerResultPersisted({ storeId: "alchemy.audit",}).pipe(Layer.provide(Cloudflare.AI.DurableObjectChatPersistence));Source:
src/Cloudflare/Workers/EmailEventSource.ts
Subscribe to Cloudflare Email Worker events with an Effect handler.
Wires both halves of the consumer in one call:
- Runtime: registers an
emailevent listener on the Worker. The handler receives aForwardableEmailMessagewhose action methods (forward,reply,setReject) returnEffects. - Deploy-time (when
zoneis set): yields aCloudflare.Email.Routingtoggle on the zone plus the routing resource whoseactions: [{ type: "worker", … }]targets this Worker —Cloudflare.Email.CatchAllfor a catch-all subscription,Cloudflare.Email.Rulefor anything more specific. No manual wiring needed inalchemy.run.ts.
Requires EmailEventSourceLive provided on the Worker’s Effect.
Failure semantics: a failing handler is logged and the failure is
re-raised. Cloudflare turns that into a temporary SMTP failure, so the
sending server keeps the message and retries later — mail is never
accepted and then silently dropped. Handle the failures you consider
final inside the handler (Effect.retry, Effect.catchTag, or
message.setReject(...) to bounce permanently); anything you let
escape becomes a retryable delivery failure.
email: Subscribing to Inbound Mail
Section titled “email: Subscribing to Inbound Mail”Catch-all on a zone — auto-creates routing + catch-all
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";
export default Cloudflare.Worker( "Inbox", { main: import.meta.url }, Effect.gen(function* () { yield* Cloudflare.email({ zone: "example.com" }).subscribe( (message) => message.forward("ops@example.com"), ); return {}; }).pipe(Effect.provide(Cloudflare.EmailEventSourceLive)),);Match a specific address
yield* Cloudflare.email({ zone: "example.com", matchers: [{ type: "literal", field: "to", value: "hello@example.com" }],}).subscribe((message) => message.forward("ops@example.com"));Reject (bounce) a message
yield* Cloudflare.email({ zone: "example.com" }).subscribe((message) => message.setReject("Mailbox closed"),);Bring-your-own routing — no zone, no auto-create
// Manage `Email.Routing` / `Email.Rule` yourself in alchemy.run.ts.yield* Cloudflare.email().subscribe((message) => Effect.log(`from ${message.from}`),);GitHubRepositoryEventSourceLive
Section titled “GitHubRepositoryEventSourceLive”Source:
src/Cloudflare/Workers/GitHubRepositoryEventSource.ts
GitHub event source for Cloudflare Workers.
Deploy-time: provisions a Webhook on the repository whose delivery
URL points at this Worker (at a deterministic per-repo path). The webhook
secret is bound onto the Worker via an Output accessor so the runtime can
verify delivery signatures.
Runtime: registers a fetch listener that claims requests on the
repository’s delivery path, verifies the HMAC-SHA256 signature against the
bound secret, and forwards each delivery to the subscriber. Requests on any
other path fall through to the Worker’s own fetch handler.
ObservabilityDestination
Section titled “ObservabilityDestination”Source:
src/Cloudflare/Workers/ObservabilityDestination.ts
A Workers observability destination — an account-level OTLP export of Workers Logs telemetry (traces, logs, or metrics) pushed to an external HTTPS collector via Logpush.
A destination is identified by its Cloudflare-derived slug (stable,
computed from the name at creation). The endpoint URL, headers, and
enabled flag are mutable in place; name and logpushDataset force a
replacement.
Cloudflare preflights the endpoint with a POST on create (skippable
via skipPreflightCheck) and on every in-place update (not skippable),
so the collector must answer 2xx for updates to converge.
Safety: destinations carry no ownership markers and Cloudflare enforces
one destination per name. When there is no prior state, read scans the
account for a destination with the same name and reports it as
Unowned, so the engine refuses to take it over unless --adopt (or
adopt(true)) is set.
ObservabilityDestination: Exporting Workers traces
Section titled “ObservabilityDestination: Exporting Workers traces”const traces = yield* Cloudflare.Workers.ObservabilityDestination("Traces", { url: "https://otel.example.com/v1/traces", headers: { authorization: secret }, logpushDataset: "opentelemetry-traces",});ObservabilityDestination: Exporting Workers logs
Section titled “ObservabilityDestination: Exporting Workers logs”const logs = yield* Cloudflare.Workers.ObservabilityDestination("Logs", { name: "my-app-logs", url: "https://collector.example.com/v1/logs", logpushDataset: "opentelemetry-logs", skipPreflightCheck: true,});ObservabilityDestination: Pausing an export
Section titled “ObservabilityDestination: Pausing an export”yield* Cloudflare.Workers.ObservabilityDestination("Logs", { name: "my-app-logs", url: "https://collector.example.com/v1/logs", logpushDataset: "opentelemetry-logs", enabled: false,});RateLimit
Section titled “RateLimit”Source:
src/Cloudflare/Workers/RateLimit.ts
A Cloudflare Rate Limit binding for counting arbitrary keys inside Workers — a Worker-only binding with no backing cloud resource.
RateLimit is a single value that is at once the Binding.Service tag, the
callable that produces a RateLimitBinding, and the type. Declare it on
a Worker’s env (it flows through InferEnv → the native cf.RateLimit) or
yield* it inside an Effect-native Worker to attach the binding and obtain
the RateLimitClient.
RateLimit: Declaring on a Worker’s env
Section titled “RateLimit: Declaring on a Worker’s env”export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { THROTTLE: Cloudflare.RateLimit("THROTTLE", { namespaceId: 1001, simple: { limit: 10, period: 60 }, }), },});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;// { THROTTLE: RateLimit } — the native Cloudflare binding
// worker.tsexport default { fetch: async (req: Request, env: WorkerEnv) => { const { success } = await env.THROTTLE.limit({ key: "ip" }); return new Response(success ? "ok" : "rate limited"); },};RateLimit: Binding inside an Effect-native Worker
Section titled “RateLimit: Binding inside an Effect-native Worker”Cloudflare.Worker("Worker", { main: "./src/worker.ts" }, Effect.gen(function* () { // Attaches the binding to this Worker AND returns the runtime client. const throttle = yield* Cloudflare.RateLimit("THROTTLE", { namespaceId: 1001, simple: { limit: 10, period: 60 }, });
return { fetch: Effect.gen(function* () { const { success } = yield* throttle.limit({ key: "ip" }); return HttpServerResponse.text(success ? "ok" : "rate limited"); }), }; }).pipe(Effect.provide(Cloudflare.Workers.RateLimitBinding)),);WorkerRoute
Section titled “WorkerRoute”Source:
src/Cloudflare/Workers/Route.ts
A Workers Route — a zone-level mapping from a URL pattern to a Worker script.
Routes are the classic way to serve a Worker on a zone hostname or
path. The matched hostname must resolve through Cloudflare’s proxy,
so pair the route with a proxied DNS record (an AAAA 100::
placeholder is the conventional choice when the Worker is the only
origin).
Safety: routes carry no ownership markers, and Cloudflare enforces
one route per pattern per zone. When there is no prior state, read
scans the zone for an existing route with the same pattern and
reports it as Unowned, so the engine refuses to take it over unless
--adopt (or adopt(true)) is set.
WorkerRoute: Routing a hostname to a Worker
Section titled “WorkerRoute: Routing a hostname to a Worker”const worker = yield* Cloudflare.Worker("Api", { main: "./src/api.ts",});
yield* Cloudflare.Workers.WorkerRoute("ApiRoute", { zoneId: zone.zoneId, pattern: "api.example.com/*", script: worker.workerName,});
// Workers only run on proxied hostnames — give the host an origin.yield* Cloudflare.DNS.Record("ApiPlaceholder", { zoneId: zone.zoneId, name: "api.example.com", type: "AAAA", content: "100::", proxied: true,});WorkerRoute: Disabling Workers on a path
Section titled “WorkerRoute: Disabling Workers on a path”// No `script` — matching requests bypass Workers entirely.yield* Cloudflare.Workers.WorkerRoute("AssetsBypass", { zoneId: zone.zoneId, pattern: "example.com/assets/*",});RpcDurableObject
Section titled “RpcDurableObject”Source:
src/Cloudflare/Workers/RpcDurableObject.ts
RpcDurableObject is sugar over DurableObject
for Durable Objects whose surface is a typed Effect RpcGroup. The
inner Effect returns the group’s handler Layer, automatically enabling
HTTP RPC with NDJSON and hibernating WebSocket RPC with JSON. Incoming
requests select the transport. Consumers see namespace.getByName(id)
as a typed HTTP RpcClient.
Existing implementations returning RpcServer.toHttpEffect(group) remain
supported for HTTP.
Use this over alchemy’s built-in DO method bridge whenever values
crossing the DO boundary contain Schema.Class instances. The
built-in bridge JSON.stringifys every method return value, which
strips class identity (e.g. an effect/ai Response.Usage instance
becomes a plain struct on the consumer side). With
RpcDurableObject, both ends go through the same
RpcSerialization codec, so Schema.decode reconstructs class
instances correctly.
RpcDurableObject: Defining the rpc group
Section titled “RpcDurableObject: Defining the rpc group”The DO instance is the session, so the group payloads typically don’t include any per-session identifier — only the per-call inputs.
import * as Schema from "effect/Schema";import { Rpc, RpcGroup } from "effect/unstable/rpc";
const setTitle = Rpc.make("setTitle", { success: Schema.Void, payload: { title: Schema.String },});
const getTitle = Rpc.make("getTitle", { success: Schema.String, payload: {},});
export class CounterRpcs extends RpcGroup.make(setTitle, getTitle) {}RpcDurableObject: Implementing the Durable Object
Section titled “RpcDurableObject: Implementing the Durable Object”Mirrors Cloudflare.DurableObject<Self>()(...) — same
outer/inner Effect pattern. The outer Effect resolves shared deps;
the per-instance inner Effect returns the RPC handler Layer.
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import { CounterRpcs } from "./rpcs.ts";
export default class Counter extends Cloudflare.RpcDurableObject<Counter>()( "Counter", { schema: CounterRpcs }, Effect.gen(function* () { // outer init: shared deps + the instance state reference const state = yield* Cloudflare.DurableObjectState; return Effect.gen(function* () { // inner (runtime): state.storage is RuntimeContext-colored, so // the handler closures that call it live here return CounterRpcs.toLayer({ setTitle: ({ title }) => state.storage.put("title", title), getTitle: () => Effect.map(state.storage.get<string>("title"), (t) => t ?? ""), }); }); }),) {}RpcDurableObject: Calling the DO from a Worker
Section titled “RpcDurableObject: Calling the DO from a Worker”yield* Counter resolves to a value whose getByName(id) returns
an Effect<RpcClient<CounterRpcs>>. Each rpc method is a typed
Effect/Stream factory — no RpcClient.make setup needed. Yield
the client inside a per-request Effect.scoped handler so it’s
freed with the request.
import Counter from "./counter.ts";
Effect.gen(function* () { const counters = yield* Counter; const client = yield* counters.getByName("global"); yield* client.setTitle({ title: "Hello" }); const title = yield* client.getTitle({}); return title;}).pipe(Effect.scoped);RpcDurableObject: WebSocket RPC
Section titled “RpcDurableObject: WebSocket RPC”Handler-Layer implementations accept WebSocket upgrades automatically; ordinary HTTP RPC remains available on the same object.
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
const counters = yield* Counter;return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const path = new URL(request.url, "https://worker").pathname; const name = /^\/counters\/([a-zA-Z0-9_-]+)$/.exec(path)?.[1]; if (!name) return HttpServerResponse.empty({ status: 404 }); return yield* counters.fetch(name, request); }),};A client connecting to wss://example.com/counters/alice targets the
"alice" instance. Every RPC on that socket stays on that object; RPC
payloads do not need an object ID. Another name selects another object.
The Worker defines this URL mapping, not Alchemy.
Authenticate and authorize access to the selected name before forwarding.
See the Effect RPC guide
for a browser client whose Layer owns the connection lifetime. Clients use
Effect’s RpcClient.layerProtocolSocket with JSON serialization. Idle
connections survive hibernation. Restored sockets with unfinished requests
close with code 1012; platform resets can also cause transport errors.
Requests and streams are never replayed. No application acknowledgment
methods or checkpoints are required.
RpcDurableObject: Modular form: separate the class from its runtime
Section titled “RpcDurableObject: Modular form: separate the class from its runtime”The inline class form above bundles the runtime into the class
declaration. The two-arg form (name, { schema }) declares the
class as a pure tagged identifier; provide the runtime separately
via Class.make(impl). Consumer Workers can import the class for
binding (Counter.from(HostWorker)) without pulling the runtime
into their bundle.
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import { CounterRpcs } from "./rpcs.ts";
export class Counter extends Cloudflare.RpcDurableObject<Counter>()( "Counter", { schema: CounterRpcs },) {}
// Only the host script imports this default export.export default Counter.make( Effect.gen(function* () { const state = yield* Cloudflare.DurableObjectState; return Effect.gen(function* () { return CounterRpcs.toLayer({ setTitle: ({ title }) => state.storage.put("title", title), getTitle: () => Effect.map(state.storage.get<string>("title"), (t) => t ?? ""), }); }); }),);RpcDurableObject: Cross-script binding via Counter.from(Worker)
Section titled “RpcDurableObject: Cross-script binding via Counter.from(Worker)”Hosting on WorkerA, binding from WorkerB
The host Worker declares Counter in its Deps (third type
arg of Worker<Self, Bindings, Deps> or second of
RpcWorker<Self, Deps>) and provides CounterLive. Any other
Worker uses Counter.from(HostWorker) to bind to the same DO
instances — writes through HostWorker.getByName(name) are
visible from Counter.from(HostWorker).getByName(name).
// host worker (declares + provides Counter)import CounterLive, { Counter } from "./counter.ts";
export class WorkerA extends Cloudflare.Worker<WorkerA, {}, Counter>()( "WorkerA", { main: import.meta.url },) {}
export default WorkerA.make( Effect.gen(function* () { const counters = yield* Counter; // local host binding // ... fetch handler ... }).pipe(Effect.provide(CounterLive)),);
// consumer worker (binds via .from)export default class WorkerB extends Cloudflare.Worker<WorkerB>()( "WorkerB", { main: import.meta.url }, Effect.gen(function* () { const counters = yield* Counter.from(WorkerA); return { fetch: Effect.gen(function* () { const client = yield* counters.getByName("shared"); yield* client.setTitle({ title: "via WorkerB" }); return HttpServerResponse.text("ok"); }).pipe(Effect.scoped), }; }),) {}Self-hosted isolated namespace
A Worker that declares Counter in its own Deps and provides
CounterLive hosts its own isolated namespace — instances under
it are separate from any other host’s. Use Counter.from(Self)
inside the host to be explicit about which script’s namespace
you’re binding to.
export class WorkerC extends Cloudflare.Worker<WorkerC, {}, Counter>()( "WorkerC", { main: import.meta.url },) {}
export default WorkerC.make( Effect.gen(function* () { const counters = yield* Counter.from(WorkerC); // explicit self // ... fetch handler ... }).pipe(Effect.provide(CounterLive)),);RpcDurableObject: Yielding the surrounding namespace from inside a DO
Section titled “RpcDurableObject: Yielding the surrounding namespace from inside a DO”Lets a DO instance refer to its own namespace — e.g. to fan a call
out to sibling instances. Mirrors yield* DurableObject
on the regular DurableObject.
Effect.gen(function* () { const self = yield* Cloudflare.RpcDurableObject; const peer = yield* self.getByName("peer-1"); yield* peer.setTitle({ title: "Sibling call" });}).pipe(Effect.scoped);Source:
src/Cloudflare/Workers/RpcWebSocketClient.tsKind: Layer · Provides:service
Provide a typed RPC client and its WebSocket connection for the Layer’s lifetime.
Import from alchemy/Cloudflare/RpcWebSocketClient in browsers to avoid
Cloudflare server modules. Provide Socket.layerWebSocketConstructorGlobal
in a browser, or the corresponding constructor Layer for another platform.
RPC client middleware remains an explicit dependency.
Provide this Layer around the application or session that shares the client.
Releasing the Layer closes the connection on success, failure, or interruption.
Ordinary calls need no caller Scope; streaming calls using { asQueue: true }
still require a Scope for the queue consumer. Inside a Cloudflare Worker,
provide this Layer per request, not in the isolate-scoped initializer.
layer: Browser Client
Section titled “layer: Browser Client”import * as RpcWebSocketClient from "alchemy/Cloudflare/RpcWebSocketClient";import { Context, Effect, Layer } from "effect";import type * as RpcClient from "effect/unstable/rpc/RpcClient";import type { RpcClientError } from "effect/unstable/rpc/RpcClientError";import * as Socket from "effect/unstable/socket/Socket";import { CounterRpcs } from "./rpcs.ts";
class CounterClient extends Context.Service< CounterClient, RpcClient.FromGroup<typeof CounterRpcs, RpcClientError>>()("CounterClient") {}
const ClientLive = RpcWebSocketClient.layer( CounterClient, CounterRpcs, "wss://example.com/counters/alice",).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal));
const program = Effect.gen(function* () { const counter = yield* CounterClient; return yield* counter.increment();}).pipe(Effect.provide(ClientLive));RpcWorker
Section titled “RpcWorker”Source:
src/Cloudflare/Workers/RpcWorker.ts
RpcWorker is a thin sugar over Worker for the common case
where a worker’s entire fetch surface is a typed Effect RpcGroup.
It takes the rpc schema directly in props alongside main, and
accepts an init Effect that returns the already-piped
RpcServer.toHttpEffect(...)-producing Effect (no { fetch }
wrapper) — the wrapper plugs it into the worker’s fetch for you.
Functionally identical to writing Cloudflare.Worker(...) with
return { fetch: RpcServer.toHttpEffect(schema).pipe(...) }; use
whichever style you prefer.
The class form (class X extends Cloudflare.RpcWorker<X>()(...))
carries Self through the result type as Rpc<Self>, so other
workers binding to this one see the rpc shape pinned to Self.
RpcWorker: Defining the rpc group
Section titled “RpcWorker: Defining the rpc group”The rpc group and its schemas live outside any worker so both the
server (RpcWorker) and any consumers (RpcClient.make /
RpcDurableObject) import the same value.
import * as Schema from "effect/Schema";import { Rpc, RpcGroup } from "effect/unstable/rpc";
export class TaskNotFound extends Schema.TaggedClass<TaskNotFound>()( "TaskNotFound", { id: Schema.String },) {}
const getTask = Rpc.make("getTask", { payload: { id: Schema.String }, success: Schema.String, error: TaskNotFound,});
export class TaskRpcs extends RpcGroup.make(getTask) {}RpcWorker: Implementing the worker
Section titled “RpcWorker: Implementing the worker”Class form (recommended)
Mirrors Cloudflare.Worker<Self>()(...) — class X extends ...
works the same. The init Effect builds a handlers Layer from the
group and returns the RpcServer.toHttpEffect(schema)-piped Effect
directly.
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import { RpcSerialization, RpcServer } from "effect/unstable/rpc";import { TaskRpcs } from "./rpcs.ts";
export default class Worker extends Cloudflare.RpcWorker<Worker>()( "Worker", { main: import.meta.url, schema: TaskRpcs }, Effect.gen(function* () { const handlers = TaskRpcs.toLayer({ getTask: ({ id }) => Effect.succeed(`task-${id}`), }); return RpcServer.toHttpEffect(TaskRpcs).pipe( Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerJson)), ); }),) {}NDJSON for streaming rpcs
If any rpc in the group is a streaming rpc, the wire serialization
must be RpcSerialization.layerNdjson — streaming rpcs need
newline framing on the wire.
return RpcServer.toHttpEffect(ChatRpcs).pipe( Effect.provide(handlers), Effect.provide(RpcSerialization.layerNdjson),);RpcWorker: Modular form: separate the class from its runtime
Section titled “RpcWorker: Modular form: separate the class from its runtime”The inline class form above bundles the runtime into the class
declaration. The two-arg form (id, props) declares the class
as a pure tagged identifier; provide the runtime separately via
WorkerClass.make(impl) so consumers can import the class for
binding without pulling the host’s runtime into their bundle.
export class TaskWorker extends Cloudflare.RpcWorker<TaskWorker>()( "TaskWorker", { main: import.meta.url, schema: TaskRpcs },) {}
// Only the host script imports this default export; consumers// import the class above for `RpcWorker.bind(TaskWorker)`.export default TaskWorker.make( Effect.gen(function* () { const handlers = TaskRpcs.toLayer({ getTask: ({ id }) => Effect.succeed(`task-${id}`), }); return RpcServer.toHttpEffect(TaskRpcs).pipe( Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerJson)), ); }),);RpcWorker: Hosting a Durable Object for cross-script binding
Section titled “RpcWorker: Hosting a Durable Object for cross-script binding”The optional second type argument Deps mirrors
Cloudflare.Worker<Self, Bindings, Deps> — it declares the DOs
this Worker publishes for cross-script binding. With Counter
named in Deps, any other Worker can write
Counter.from(TaskWorker) and have it type-check.
import { Counter } from "./counter.ts";
export class TaskWorker extends Cloudflare.RpcWorker<TaskWorker, Counter>()( "TaskWorker", { main: import.meta.url, schema: TaskRpcs },) {}See RpcDurableObject for the consumer side
(Counter.from(TaskWorker)).
RpcWorker: Binding it from another worker
Section titled “RpcWorker: Binding it from another worker”Inside another worker’s init, RpcWorker.bind(WorkerClass)
registers the service binding on the surrounding worker and returns
a typed RpcClient you can call directly from any per-request
handler. Internally each method invocation builds a fresh underlying
client (because Cloudflare rejects cross-request reuse of the
stub I/O), but that’s hidden behind a Proxy so the consumer sees a
normal RpcClient.
import TaskWorker from "./task-worker.ts";
export default class Caller extends Cloudflare.RpcWorker<Caller>()( "Caller", { main: import.meta.url, schema: CallerRpcs }, Effect.gen(function* () { // INIT: register binding, get the typed client const tasks = yield* Cloudflare.RpcWorker.bind(TaskWorker);
const handlers = CallerRpcs.toLayer({ // PER-REQUEST: just call methods directly proxyGetTask: ({ id }) => tasks.getTask({ id }), }); return RpcServer.toHttpEffect(CallerRpcs).pipe( Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerJson)), ); }),) {}RpcWorker: Driving it from a test
Section titled “RpcWorker: Driving it from a test”The same RpcGroup drives a typed client. Test.make deploys the
stack once for the file; each test yields the deploy handle for its
URL and calls procedures directly.
import { expect } from "alchemy-test";import * as Cloudflare from "alchemy/Cloudflare";import * as Test from "alchemy/Test/Alchemy";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Schedule from "effect/Schedule";import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";import * as RpcClient from "effect/unstable/rpc/RpcClient";import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";import Stack from "../alchemy.run.ts";import { TaskRpcs } from "../src/rpcs.ts";
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ providers: Cloudflare.providers(),});const stack = beforeAll(deploy(Stack));afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
const layer = (url: string) => RpcClient.layerProtocolHttp({ url }).pipe( Layer.provide(FetchHttpClient.layer), Layer.provide( Layer.succeed(RpcSerialization.RpcSerialization, RpcSerialization.json), ), );
test( "getTask", Effect.gen(function* () { const { url } = yield* stack; yield* Effect.gen(function* () { const client = yield* RpcClient.make(TaskRpcs); const result = yield* client .getTask({ id: "abc" }) .pipe(Effect.retry({ schedule: Schedule.exponential("500 millis"), times: 5 })); expect(result).toBe("task-abc"); }).pipe(Effect.scoped, Effect.provide(layer(url))); }),);RpcWorker: Yielding the surrounding worker from inside the impl
Section titled “RpcWorker: Yielding the surrounding worker from inside the impl”Mirrors yield* DurableObject — yield the tag to access
the surrounding worker.
Effect.gen(function* () { const self = yield* Cloudflare.RpcWorker;});SecretKey
Section titled “SecretKey”Source:
src/Cloudflare/Workers/SecretKey.ts
A Cloudflare Workers Secret Key binding — key material uploaded once and
exposed to the Worker as a native, non-extractable
CryptoKey.
The Worker can sign, verify, encrypt, or decrypt with the key via
crypto.subtle, but can never read the raw key material back out.
SecretKey is a single value that is at once the Binding.Service tag, the
callable that produces a SecretKeyBinding, and the type. Declare it
on a Worker’s env (it flows through InferEnv → the native CryptoKey)
or yield* it inside an Effect-native Worker to attach the binding and
obtain a deferred SecretKeyAccessor.
SecretKey: Binding inside an Effect-native Worker
Section titled “SecretKey: Binding inside an Effect-native Worker”HMAC sign and verify
Cloudflare.Worker( "SignerWorker", { main: import.meta.url }, Effect.gen(function* () { // Attaches the binding to this Worker AND returns a deferred accessor. const hmacKey = yield* Cloudflare.Workers.SecretKey("HMAC_KEY", { format: "raw", algorithm: { name: "HMAC", hash: "SHA-256" }, usages: ["sign", "verify"], keyBase64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", });
return { fetch: Effect.gen(function* () { const key = yield* hmacKey; const data = new TextEncoder().encode("hello"); const signature = yield* Effect.promise(() => crypto.subtle.sign("HMAC", key, data), ); const valid = yield* Effect.promise(() => crypto.subtle.verify("HMAC", key, signature, data), ); return HttpServerResponse.json({ valid }); }), }; }).pipe(Effect.provide(Cloudflare.Workers.SecretKeyBinding)),);JSON Web Key format
const jwkKey = yield* Cloudflare.Workers.SecretKey("HMAC_KEY_JWK", { format: "jwk", algorithm: { name: "HMAC", hash: "SHA-256" }, usages: ["sign", "verify"], keyJwk: { kty: "oct", k: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8", alg: "HS256", },});SecretKey: Declaring on a Worker’s env
Section titled “SecretKey: Declaring on a Worker’s env”export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { HMAC_KEY: Cloudflare.Workers.SecretKey("HMAC_KEY", { format: "raw", algorithm: { name: "HMAC", hash: "SHA-256" }, usages: ["sign", "verify"], keyBase64: Redacted.make("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="), }), },});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;// { HMAC_KEY: CryptoKey } — the native runtime binding
// worker.tsexport default { fetch: async (req: Request, env: WorkerEnv) => { const signature = await crypto.subtle.sign( "HMAC", env.HMAC_KEY, new TextEncoder().encode("hello"), ); return new Response(btoa(String.fromCharCode(...new Uint8Array(signature)))); },};SqlMigrations
Section titled “SqlMigrations”Source:
src/Cloudflare/Workers/SqlMigrations.ts
Read SQL migrations during construction and carry them into a Durable
Object without importing .sql files or generated migrations.js.
Files are ordered and hashed using Alchemy’s shared SQL migration format. Flat SQL files and modern drizzle-kit migration directories are supported. Paths are relative to the directory where Alchemy runs, not this module. The records are embedded in the Worker JavaScript bundle, so they do not consume environment-variable bindings. The runtime never reads the directory. Missing directories and unsupported layouts fail construction.
SqlMigrations: Drizzle Durable Objects
Section titled “SqlMigrations: Drizzle Durable Objects”import * as Cloudflare from "alchemy/Cloudflare";import * as Drizzle from "alchemy/Drizzle/Cloudflare";import * as Effect from "effect/Effect";import { relations, users } from "./schema.ts";
export class Users extends Cloudflare.DurableObject<Users>()( "Users", Effect.gen(function* () { const migrations = yield* Cloudflare.SqlMigrations("./drizzle"); return Effect.gen(function* () { const db = yield* Drizzle.DurableObject({ migrations, relations }); return { list: () => db.select().from(users) }; }); }),) {}SqlMigrations: Apply Migrations Without Drizzle
Section titled “SqlMigrations: Apply Migrations Without Drizzle”Effect.gen(function* () { const migrations = yield* Cloudflare.SqlMigrations("./drizzle"); return Effect.gen(function* () { yield* migrations.apply().pipe(Effect.orDie); return {}; });});apply() requires RuntimeContext and the current Durable Object state.
Each pending file and its __alchemy_migrations row commit atomically.
A failed file rolls back; successfully applied earlier files stay committed.
Existing modern Drizzle history is adopted without replaying applied SQL.
SqlMigrations: Custom Bookkeeping Table
Section titled “SqlMigrations: Custom Bookkeeping Table”const migrations = yield* Cloudflare.SqlMigrations({ dir: "./drizzle", table: "app_migrations",});Subdomain
Section titled “Subdomain”Source:
src/Cloudflare/Workers/Subdomain.ts
The account-wide workers.dev subdomain singleton
(/accounts/{account_id}/workers/subdomain). Every Worker with
workers.dev enabled is served at
https://<script>.<subdomain>.workers.dev.
Each account has at most one subdomain — “creating” this resource claims
(or renames to) the requested name. Subdomain names are globally unique
across all Cloudflare accounts; claiming a taken name fails with the
typed SubdomainAlreadyExists error.
Destroy is capture-and-restore: the subdomain is renamed back to the value it had before Alchemy first managed it. If the account had no subdomain at first touch, destroy removes it entirely.
Warning: renaming or removing the subdomain immediately changes the
URL of every deployed Worker on the account that relies on
workers.dev. Only manage this resource on accounts where that is
acceptable.
Subdomain: Managing the subdomain
Section titled “Subdomain: Managing the subdomain”const sub = yield* Cloudflare.Workers.Subdomain("Subdomain", { subdomain: "my-team",});// Workers are now served from https://<script>.my-team.workers.devTelemetry
Section titled “Telemetry”Source:
src/Cloudflare/Workers/Telemetry.tsKind: Layer · Provides:Tracer.Tracer
Mirror Effect spans into Cloudflare Workers Observability.
A binding layer: at deploy time it turns on observability.traces on the
host Worker (the same path as Cloudflare.cache()) and registers a
per-event Effect Tracer. Cloudflare auto-instruments fetch/KV/R2/D1;
Effect.withSpan / Effect.fn frames nest in that waterfall. Cloudflare
owns sampling and export — no OTLP URL, flush, or Wrangler block.
Until the global default compatibility date is raised past 2026-07-28,
pin compatibility: { date: "2026-08-25" } (or later) or deploy fails.
alchemy dev does not gate the date: what a local Worker records is the
local runtime’s concern.
Compose it into the Function/Worker’s single Effect.provide:
Telemetry: Enabling native tracing
Section titled “Telemetry: Enabling native tracing”export default Cloudflare.Worker( "Worker", { main: import.meta.url, compatibility: { date: "2026-08-25" }, }, Effect.gen(function* () { return { fetch: Effect.gen(function* () { yield* doWork().pipe(Effect.withSpan("operation")); return HttpServerResponse.text("ok"); }), }; }).pipe( Effect.provide( Layer.mergeAll( Cloudflare.R2.ReadWriteBucketBinding, Cloudflare.Telemetry(), ), ), ),);Telemetry: Combining with other exporters
Section titled “Telemetry: Combining with other exporters”Effect.provide( Layer.mergeAll( Cloudflare.Telemetry(), // omit `traces` — Cloudflare.Telemetry provides the Tracer Axiom.Telemetry({ token: Ingest, logs: Logs, metrics: Metrics }), ),)To ship the same waterfall (Effect + platform spans) elsewhere, export
it from Cloudflare with Cloudflare.Workers.ObservabilityDestination
rather than sending a second Worker-side OTLP trace stream.
VersionMetadata
Section titled “VersionMetadata”Source:
src/Cloudflare/Workers/VersionMetadata.ts
A Cloudflare Workers Version Metadata binding — a Worker-only binding with no
backing cloud resource. Cloudflare provides the deployed Worker version at
runtime (id, tag, timestamp).
VersionMetadata is a single value that is at once the Binding.Service tag,
the callable that produces a VersionMetadataBinding, and the type.
Declare it on a Worker’s env (it flows through InferEnv →
WorkerVersionMetadata) or yield* it inside an Effect-native Worker
to attach the binding and obtain a deferred VersionMetadataAccessor.
VersionMetadata: Effect-style Worker (recommended)
Section titled “VersionMetadata: Effect-style Worker (recommended)”import * as Effect from "effect/Effect";
Cloudflare.Worker( "VersionWorker", { main: import.meta.url }, Effect.gen(function* () { // Attaches the binding to this Worker AND returns a deferred accessor. const versionMetadata = yield* Cloudflare.Workers.VersionMetadata();
return { fetch: Effect.gen(function* () { const { id, tag, timestamp } = yield* versionMetadata; return Response.json({ id, tag, timestamp }); }), }; }).pipe(Effect.provide(Cloudflare.Workers.VersionMetadataBinding)),);VersionMetadata: Worker binding metadata
Section titled “VersionMetadata: Worker binding metadata”export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { CF_VERSION_METADATA: Cloudflare.Workers.VersionMetadata() },});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;// { CF_VERSION_METADATA: WorkerVersionMetadata }Worker
Section titled “Worker”Source:
src/Cloudflare/Workers/Worker.ts
A Cloudflare Worker host with deploy-time binding support and runtime export collection.
A Worker follows a two-phase pattern. The outer Effect.gen runs at
deploy time to bind resources (KV, R2, Durable Objects, etc.). It returns
an object whose properties are the Worker’s runtime handlers — fetch for
HTTP requests and any additional RPC methods.
Effect.gen(function* () { // Construction: bind resources (deploy time and cold start) const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return { // Runtime: handlers, once per request fetch: Effect.gen(function* () { const value = yield* kv.get("key"); return HttpServerResponse.text(value ?? "not found"); }), };})There are three ways to define a Worker, from simplest to most flexible. See the Runtime page for the full explanation.
- Async — plain
async fetchhandler, no Effect runtime in the bundle. - Effect — Effect implementation passed directly, single file.
- Layer — class and
.make()in a single file; Rolldown tree-shakes.make()from consumers.
Worker: Protect with Access
Section titled “Worker: Protect with Access”Put Cloudflare Access in front of the Worker with the access prop —
unauthenticated requests are redirected to your team’s login page, and
handlers read the authenticated identity from ctx.access via
Cloudflare.Access.Context. See the
Protect a Worker with Access guide.
Dedicated application — per-Worker policies
The { policies } form declares an Access application owned by this
Worker (namespaced under it as <Worker>/Access), created, updated,
and deleted with it:
export default Cloudflare.Worker( "Api", { main: import.meta.url, access: { policies: [ { decision: "allow", include: [{ emailDomain: "example.com" }] }, ], }, // simulate the authenticated state under `alchemy dev` dev: { access: { aud: "dev", identity: { email: "dev@example.com" } } }, }, Effect.gen(function* () { return { fetch: Effect.gen(function* () { const access = yield* Cloudflare.Access.Context; const identity = yield* access!.getIdentity(); return yield* HttpServerResponse.json({ email: identity?.email }); }), }; }),);Shared application — one policy set, many Workers
Pass a Cloudflare.Access.Application directly to enroll into it.
Access policies are application-wide: every enrolled Worker is gated
by the same policy set.
const TeamOnly = Cloudflare.Access.Application("TeamOnly", { type: "self_hosted", policies: [ { decision: "allow", include: [{ emailDomain: "example.com" }] }, ],});
export default Cloudflare.Worker( "Api", { main: import.meta.url, access: TeamOnly }, /* ... */);Worker: Async Workers
Section titled “Worker: Async Workers”You don’t have to use Effect for your runtime code. If you create
a Worker resource with main pointing at a file but provide no
Effect.gen implementation, Alchemy bundles and deploys that file
as-is. Your handler is a plain async fetch — no Effect runtime
is included in the bundle.
Use the env prop to declare which resources, Config values,
and literal env vars are available at runtime, and
Cloudflare.InferEnv to extract a fully typed env object from
them.
See the Workers guide for a comprehensive walkthrough of all binding types (R2, D1, Durable Objects, Assets, and more).
Defining an async Worker in your stack
const db = yield* Cloudflare.D1.Database("DB");const bucket = yield* Cloudflare.R2.Bucket("Bucket");
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { db, bucket },});Writing the async handler
import type { WorkerEnv } from "../alchemy.run.ts";
export default { async fetch(request: Request, env: WorkerEnv) { if (request.method === "GET") { const object = await env.bucket.get("key"); return new Response(object?.body ?? null); } return new Response("Not Found", { status: 404 }); },};Binding a named entrypoint
env: { TARGET: worker } targets the Worker’s default entrypoint.
Cloudflare.WorkerEntrypoint binds one of its named
WorkerEntrypoint class exports instead; InferEnv types the entry
as a Fetcher stub whose RPC methods are called directly.
const caller = yield* Cloudflare.Worker("Caller", { main: "./src/caller.ts", env: { API: Cloudflare.WorkerEntrypoint(target, "Api"), // env.API.greet("alice") },});Entrypoint props
The options form attaches properties the target entrypoint reads from
this.ctx.props. Output values resolve at deploy time. alchemy dev
delivers props to the local workerd; the deploy API’s binding schema
does not carry the field yet, so props are dropped at upload while the
binding itself deploys correctly.
env: { VENDOR: Cloudflare.WorkerEntrypoint(vendorWorker, { entrypoint: "Vendor", props: { baseUrl: site.url }, }),}Worker: Python Workers
Section titled “Worker: Python Workers”Point main at a .py file to deploy a
Python Worker
(open beta). There is no bundling step — the entry and every sibling
.py module upload as-is and are interpreted by Pyodide, and the
python_workers compatibility flag is added automatically. Like async
Workers, Python Workers take no inline Effect implementation; declare
bindings with the env prop and read them from self.env in Python.
Dependencies come from pyproject.toml next to the entry: Alchemy
vendors [project.dependencies] with uv
against the Pyodide wheel index and uploads them under
python_modules/. If a python_modules/ directory already exists
(e.g. produced by pywrangler sync), it is uploaded as-is and uv is
not invoked.
See the Python Workers guide for the full walkthrough.
Defining a Python Worker in your stack
const kv = yield* Cloudflare.KV.Namespace("Cache");
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.py", env: { CACHE: kv },});Writing the Python handler
from workers import Response, WorkerEntrypoint
class Default(WorkerEntrypoint): async def fetch(self, request): cached = await self.env.CACHE.get("greeting") return Response(cached or "Hello from Python!")Vendoring dependencies with pyproject.toml
# src/pyproject.toml — vendored with uv on deploy[project]name = "my-worker"version = "0.1.0"requires-python = ">=3.13"dependencies = ["humanize"]Worker: Effect Workers
Section titled “Worker: Effect Workers”Pass the Effect implementation as the third argument. This is the simplest Effect-based approach — everything lives in one file. Convenient for standalone Workers that don’t need to be referenced by other Workers.
export default class MyWorker extends Cloudflare.Worker<MyWorker>()( "MyWorker", { main: import.meta.url }, Effect.gen(function* () { // Construction: bind resources const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return { // Runtime: use them fetch: Effect.gen(function* () { const value = yield* kv.get("key"); return HttpServerResponse.text(value ?? "not found"); }), }; }),) {}Worker: Worker Layer
Section titled “Worker: Worker Layer”When two Workers need to reference each other (e.g. WorkerA calls
WorkerB and vice versa), or you simply want optimal tree-shaking,
define the Worker class separately from its .make() call. The
class is a lightweight identifier; .make() provides the runtime
implementation as an export default. Rolldown treats .make()
as pure, so any Worker that imports the class to bind it will not
pull in the .make() dependencies — the bundler tree-shakes
them away entirely.
The class and .make() can live in the same file. This is the
same pattern used by Container and DurableObject,
and is recommended for any cross-Worker or cross-DO bindings.
Worker Layer (class + .make() in one file)
// src/WorkerB.ts — the tag carries the name + RPC shape; props live// on `.make()`.export class WorkerB extends Cloudflare.Worker< WorkerB, { greet: (name: string) => Effect.Effect<string> }>()("WorkerB") {}
export default WorkerB.make( { main: import.meta.url }, Effect.gen(function* () { // Construction: bind resources const kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return { // Runtime: use them greet: (name: string) => Effect.gen(function* () { yield* kv.put("last-greeted", name); return `Hello ${name}`; }), }; }),);Binding a Worker Layer from another Worker
// src/WorkerA.ts — imports WorkerB; bundler tree-shakes .make()import WorkerB from "./WorkerB.ts";
export default class WorkerA extends Cloudflare.Worker<WorkerA>()( "WorkerA", { main: import.meta.url }, Effect.gen(function* () { const b = yield* Cloudflare.Workers.bindWorker(WorkerB); return { fetch: Effect.gen(function* () { return yield* b.greet("world"); }), }; }),) {}Worker: Configuration
Section titled “Worker: Configuration”The props object controls compatibility flags, static assets, and build options. These are evaluated at deploy time.
Enabling Node.js compatibility
{ main: import.meta.url, compatibility: { flags: ["nodejs_compat"], date: "2026-08-31", },}Serving static assets
{ main: import.meta.url, assets: "./public",}Run the Worker before the asset layer
A path that matches no asset already falls through to the Worker.
runWorkerFirst routes the listed paths (or, with true, every
request) through the Worker ahead of asset matching, for an asset
that would otherwise shadow a route or an SPA fallback that would
answer an API call. A _headers or _redirects file in the
directory is applied automatically and .assetsignore excludes
files from the upload.
{ main: import.meta.url, assets: { directory: "./public", runWorkerFirst: ["/api/*", "/admin/*"], },}Assets-only Worker (static site)
Omit main and script entirely to deploy a static site: no Worker
code is uploaded — Cloudflare’s asset layer serves every request and
applies htmlHandling / notFoundHandling (including SPA fallback)
itself, exactly like an assets-only wrangler deploy.
const site = yield* Cloudflare.Worker("Site", { assets: { directory: "./public", htmlHandling: "drop-trailing-slash", notFoundHandling: "404-page", }, domain: "static.example.com",});Zone routes
{ main: import.meta.filename, routes: [ { pattern: "api.example.com/*", zoneName: "example.com" }, { pattern: "example.com/api/*", zoneId: "<YOUR_ZONE_ID>" }, ],}Deploying a prebuilt Worker without bundling
When main already points at a complete, runtime-ready ESM bundle
produced by an external tool (e.g. OpenNext), set bundle: false to
upload it byte-for-byte. The entry’s directory is walked recursively
and every file matching the module rules (by default .js, .mjs,
.wasm, .txt, .html, .sql, and .bin) is uploaded as an
additional module named by its path relative to that directory.
{ main: "./.open-next/worker.js", bundle: false, assets: "./.open-next/assets",}Worker: Bundling & Tree-shaking
Section titled “Worker: Bundling & Tree-shaking”main is bundled with rolldown at deploy time. Unused code is
tree-shaken. effect, alchemy, and @distilled.cloud are marked
pure so unused parts prune more aggressively. Your app is not
marked pure.
Mark additional packages as pure
Only list packages with no top-level side effects.
{ main: "./src/worker.ts", build: { pure: { packages: ["my-lib", "@my-scope/*"] }, },}Replace Node modules with Worker-compatible stubs
Use Rolldown’s build.input.resolve.alias for module replacements.
Aliases apply to imports and static require() calls before Node
compatibility shims. Use absolute paths for file replacements.
Keep bundling enabled: bundle: false uploads files unchanged and
does not apply aliases. Alternatively, apply aliases in your external
build before uploading its output with bundle: false.
import * as Path from "effect/Path";
const path = yield* Path.Path;const stub = yield* path.fromFileUrl( new URL("./.mastra/output/module-stub.mjs", import.meta.url),);const worker = yield* Cloudflare.Worker("Worker", { main: "./.mastra/output/index.mjs", compatibility: { date: "2025-04-01", flags: ["nodejs_compat", "nodejs_compat_populate_process_env"], }, build: { input: { resolve: { alias: { module: stub, "node:module": stub } } }, },});Turn it off
{ main: "./src/worker.ts", build: { pure: false },}Worker: URLs & Domains
Section titled “Worker: URLs & Domains”Every URL that serves the Worker is collected in worker.urls, most
significant first, and worker.url is always urls[0]. The ranking:
the canonical custom domain (domain.name), then aliases in declared
order, then the stable workers.dev URL, then version preview URLs.
Under alchemy dev, urls is the local dev server’s
[localhost, ...LAN] addresses instead. Redirect hostnames never
appear in urls — they serve no content.
The workersDev prop controls the workers.dev surface (true by
default = stable URL + version previews; false = neither; object form
toggles independently), and the domain prop attaches custom domains —
DNS records and edge certificates are managed automatically.
Custom domain with aliases and redirects
const worker = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", domain: { name: "example.com", zoneId: "<YOUR_ZONE_ID>", aliases: ["www.example.com"], redirects: ["old.example.com"], // 301 → https://example.com },});// worker.url === "https://example.com"// worker.urls === ["https://example.com", "https://www.example.com",// "https://<name>.<account>.workers.dev"]workers.dev toggles
// No workers.dev URLs at all:{ main: "./src/api.ts", workersDev: false, domain: "api.example.com" }
// Previews only — each deploy's version preview URL becomes worker.url:{ main: "./src/api.ts", workersDev: { enabled: false, previewsEnabled: true } }All URLs as a CORS allow-list
const site = yield* Cloudflare.Worker("Site", { main: "./src/site.ts", domain: { name: "example.com", aliases: ["www.example.com"] },});const api = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", env: { ALLOWED_ORIGINS: site.urls },});Worker: Worker Previews
Section titled “Worker: Worker Previews”The preview prop maps Cloudflare’s
Worker Previews —
a named copy of a Worker with its own URL, variables, secrets, bindings,
and isolated same-Worker Durable Object state. Use it for branch and
pull-request testing. Distinct from version: a version is an
immutable upload onto the parent script (gradual rollouts, canaries);
a Preview does not take production traffic.
A Preview Worker’s url is its stable Preview URL
(<name>-<parent>.<subdomain>.workers.dev, or
<name>.<domain> when the parent has domain.previews). The name
defaults to the stack stage (override with preview.name). Destroying
the Preview Worker deletes the Preview; the parent is untouched.
PR preview of another stage’s Worker
const parent = yield* Cloudflare.Worker.ref("Api", { stage: "prod" });const preview = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", preview: { of: parent, message: `PR #${process.env.PR_NUMBER}` },});// preview.url -> https://<stage>-<name>.<subdomain>.workers.devCustom-domain Preview URLs
// On the production Worker:yield* Cloudflare.Worker("Api", { main: "./src/api.ts", domain: { name: "app.example.com", previews: true },});// A Preview of that Worker is then at https://<preview-name>.app.example.comWorker: Versions & Gradual Deployments
Section titled “Worker: Versions & Gradual Deployments”The version prop maps Cloudflare’s
versions and gradual deployments
onto Alchemy stages. A Worker with version.parent set uploads an
immutable version to the parent Worker’s script instead of creating its
own — give it traffic to run it as a canary, or use version.traffic
on a normal Worker to roll out its own deploys gradually. For branch
and pull-request testing, use preview instead.
A version worker’s url is its aliased preview URL
(<alias>-<name>.<subdomain>.workers.dev) — the alias is derived from
the stack, stage, and logical id (override with version.alias), so the
URL is stable across deploys and always points at the latest uploaded
version. The per-version URL (<version-prefix>-...) is also returned
in domains. Because the aliased URL is known before the version
exists, Worker.URL works on version workers and resolves to it.
A version carries code, static assets, bindings, and compatibility settings. Script-level settings (routes, domains, crons, tags, observability, …) belong to the parent and are rejected on version workers, as are locally-hosted Durable Object or Workflow classes. Preview URLs require the parent’s workers.dev subdomain to be enabled (the default).
Upload a version without routing traffic
// Inspect this upload at its Version URL before a gradual rollout.// For branch/PR testing, use `preview.of` instead.yield* Cloudflare.Worker("MyWorker", { main: "./src/worker.ts", version: { traffic: 0, tag: process.env.GITHUB_SHA },});Canary: send 10% of the parent’s traffic to a version
const parent = yield* Cloudflare.Worker.ref("MyWorker", { stage: "prod" });yield* Cloudflare.Worker("MyWorker", { main: "./src/worker.ts", version: { parent, traffic: 10 },});Gradual rollout of a Worker’s own deploy
// The new version takes 25% of traffic; the previously-live version// keeps 75%. Bump traffic (or remove the prop) and re-deploy to promote.yield* Cloudflare.Worker("MyWorker", { main: "./src/worker.ts", version: { traffic: 25 },});Keep users on one version during the rollout
// Percentages route each request independently; affinity pins users by// filling the Cloudflare-Workers-Version-Key header on zone traffic —// here from the session cookie, falling back to the client IP. Requires// a `domain` or `routes` (with `parent`, the parent's).yield* Cloudflare.Worker("MyWorker", { main: "./src/worker.ts", domain: "api.example.com", version: { traffic: 25, affinity: { cookie: "session_id", ip: true }, },});Worker: The Worker’s own URL
Section titled “Worker: The Worker’s own URL”Worker.URL injects the URL a Worker is served at as a binding on that
same Worker — the first custom domain if one is configured, otherwise
its workers.dev URL, always equal to the resource’s url attribute.
Under alchemy dev it resolves to the local dev server’s URL.
Read the Worker’s own URL inside a handler
Cloudflare.Worker( "Api", { main: import.meta.url }, Effect.gen(function* () { // Attaches the binding and returns a deferred accessor. const url = yield* Cloudflare.Worker.URL;
return { fetch: Effect.gen(function* () { const publicUrl = yield* url; return yield* HttpServerResponse.json({ url: publicUrl }); }), }; }),);Inject the URL into an async Worker’s env
InferEnv types the entry as string. A VITE_-prefixed key on a
vite-built Worker is additionally inlined into the client bundle as
import.meta.env.VITE_PUBLIC_URL at build time.
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { PUBLIC_URL: Cloudflare.Worker.URL },});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;// { PUBLIC_URL: string }Worker: Observability
Section titled “Worker: Observability”Cloudflare Workers Observability is on by default — logs.enabled and
logs.invocationLogs are turned on if you don’t pass an observability
prop. Pass the prop yourself to tune sampling, enable persistence, or
turn on the new traces channel (the same toggle the dashboard’s
Observability tab writes).
Effect-native Workers should prefer Cloudflare.Telemetry() over
setting observability.traces by hand: providing the Layer enables
traces on this Worker and mirrors Effect.withSpan into the Cloudflare
waterfall. Pin compatibility: { date: "2026-08-25" } (or later) until
the global default date is raised past 2026-07-28.
Field names match the Cloudflare API (camelCased): headSamplingRate,
invocationLogs, etc.
{ main: import.meta.url, observability: { enabled: true, headSamplingRate: 1, logs: { enabled: true, invocationLogs: true, headSamplingRate: 1, persist: true, }, traces: { enabled: true, headSamplingRate: 1, persist: true, }, },}Worker: Tail Workers
Section titled “Worker: Tail Workers”A Tail Worker
receives execution traces (console logs, exceptions, event metadata) from
other Workers. List it in a producer’s tailConsumers and export a
tail() handler from the consumer; Cloudflare delivers each invocation’s
trace events to every listed consumer after the invocation completes.
Sending a Worker’s traces to a Tail Worker
const tailWorker = yield* Cloudflare.Worker("TailWorker", { // exports: export default { async tail(events, env, ctx) { ... } } main: "./src/tail.ts",});
const api = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", tailConsumers: [tailWorker],});A streaming Tail Worker receives the same invocation’s events live,
while the producer is still executing: list it in
streamingTailConsumers and export a tailStream() handler that is
invoked with the invocation’s onset event and returns a handler for
every subsequent event of the session, ending with the terminal
outcome.
Streaming a Worker’s events to a streaming Tail Worker
const streamTailWorker = yield* Cloudflare.Worker("StreamTailWorker", { // exports: export default { // tailStream(onset, env, ctx) { // return (event) => { ... }; // log, spanOpen, ..., outcome // }, // } main: "./src/stream-tail.ts",});
const api = yield* Cloudflare.Worker("Api", { main: "./src/api.ts", streamingTailConsumers: [streamTailWorker],});Worker: Workers Cache
Section titled “Worker: Workers Cache”Workers Cache puts a regionally tiered cache in front of the Worker —
cache hits are served from the edge without invoking the Worker (and
without billing CPU time). In an Effect-native Worker, enable it by
yielding Cloudflare.cache() in the Construction phase, which also returns the
runtime purge client; async Workers use the cache prop instead. Control
what gets cached from your handlers via standard response headers:
Cache-Control (including stale-while-revalidate), Cache-Tag for
tag-based purging, and Vary for content negotiation.
The cache is scoped to a single Worker version by default, so every
deploy starts cold. Set crossVersionCache: true to share cached
responses across versions.
Enabling and purging the cache in an Effect Worker
Effect.gen(function* () { // Construction: enable Workers Cache on this Worker const { purge } = yield* Cloudflare.cache({ crossVersionCache: true });
return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; if (request.url.startsWith("/invalidate")) { yield* purge({ tags: ["products"] }); return HttpServerResponse.text("purged"); } return HttpServerResponse.text("hello", { headers: { "Cache-Control": "public, max-age=300, stale-while-revalidate=3600", "Cache-Tag": "products,product:123", }, }); }), };})Enabling Workers Cache on an async Worker
{ main: "./src/worker.ts", cache: { enabled: true, crossVersionCache: true, },}Worker: Background Work & Scopes
Section titled “Worker: Background Work & Scopes”Each incoming event (fetch, RPC call, scheduled run) gets its own Effect
Scope. When the handler finishes, the bridge closes that scope and
registers the close promise with workerd’s ctx.waitUntil — so
finalizers added with Effect.addFinalizer inside a handler run after
the response is sent, without blocking it, and the Worker stays alive
until they settle. Streaming responses transfer the scope to the stream,
so those finalizers run when the stream completes instead.
For ad-hoc background work, WorkerExecutionContext.waitUntil(effect)
forks an Effect with the caller’s full context and keeps the invocation
alive until it settles. The context can be yielded once in the
constructor and used from any handler; its methods are RuntimeContext-
colored, so they can only run inside a handler.
The constructor is evaluated once per isolate: the bridge builds the
Worker’s layer stack on the first event and every later event reuses the
built services. Resolve services, bind resources, build handlers there —
one-shot I/O that caches a plain value (e.g. fetching a secret for a
client) is fine, but nothing disposable: the build scope is never closed
(workerd has no isolate-teardown hook), so a finalizer added in the
constructor never runs, and I/O-backed objects (sockets, response bodies) are
pinned to the request that created them. Anything that needs cleanup
belongs in a handler, where Effect.addFinalizer attaches to the
per-event scope.
Post-response cleanup with a scope finalizer
return { fetch: Effect.gen(function* () { // runs after this response is sent, kept alive by waitUntil yield* Effect.addFinalizer(() => flushMetrics().pipe(Effect.ignore)); return HttpServerResponse.text("ok"); }),};Background work with waitUntil
// Constructionconst exec = yield* Cloudflare.WorkerExecutionContext;
return { fetch: Effect.gen(function* () { // respond now; the audit write completes in the background yield* exec.waitUntil(writeAuditLog(event)); return HttpServerResponse.text("accepted", { status: 202 }); }),};Worker: R2 Bucket
Section titled “Worker: R2 Bucket”Bind an R2 bucket in the Construction phase with Cloudflare.R2.ReadWriteBucket.
The returned handle exposes get, put, delete, and list
methods you can call in your runtime handlers.
// Constructionconst bucket = yield* Cloudflare.R2.ReadWriteBucket(MyBucket);
return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const key = request.url.split("/").pop()!;
if (request.method === "GET") { const object = yield* bucket.get(key); return object ? HttpServerResponse.text(yield* object.text()) : HttpServerResponse.empty({ status: 404 }); }
yield* bucket.put(key, request.stream); return HttpServerResponse.empty({ status: 201 }); }),};Worker: KV Namespace
Section titled “Worker: KV Namespace”Bind a KV namespace with Cloudflare.KV.ReadWriteNamespace. KV provides
eventually-consistent, low-latency key-value reads replicated
globally across Cloudflare’s edge.
// Constructionconst kv = yield* Cloudflare.KV.ReadWriteNamespace(MyKV);
return { fetch: Effect.gen(function* () { const value = yield* kv.get("my-key"); return HttpServerResponse.text(value ?? "not found"); }),};Worker: D1 Database
Section titled “Worker: D1 Database”Bind a D1 database with Cloudflare.D1.QueryDatabase. D1 is a
serverless SQLite database — use prepare to build parameterized
queries and all, first, or run to execute them.
// Constructionconst db = yield* Cloudflare.D1.QueryDatabase(MyDatabase);
return { fetch: Effect.gen(function* () { const results = yield* db .prepare("SELECT * FROM users WHERE id = ?") .bind(userId) .all(); return yield* HttpServerResponse.json(results); }),};Worker: Durable Objects
Section titled “Worker: Durable Objects”Yield a DurableObject class in the Construction phase to get a
namespace handle. Call getByName or getById to get a typed RPC
stub, then call its methods from your runtime handlers.
// Constructionconst counters = yield* Counter;
return { fetch: Effect.gen(function* () { const counter = counters.getByName("user-123"); const value = yield* counter.increment(); return HttpServerResponse.text(String(value)); }),};Worker: Containers
Section titled “Worker: Containers”Containers run long-lived processes alongside Durable Objects.
Provide Cloudflare.Containers.layer(Sandbox, …) on a DO’s constructor to
bind, start, and monitor the container; then yield* Sandbox
resolves the running instance. Call its typed methods or use
getTcpPort to make HTTP requests to its exposed ports.
export default class Agent extends Cloudflare.DurableObject<Agent>()( "Agents", Effect.gen(function* () { const sandbox = yield* Sandbox;
return Effect.gen(function* () { return { exec: (cmd: string) => sandbox.exec(cmd), health: () => Effect.gen(function* () { const { fetch } = yield* sandbox.getTcpPort(3000); const res = yield* fetch( HttpClientRequest.get("http://container/health"), ); return yield* res.text; }), }; }); }).pipe( Effect.provide( Cloudflare.Containers.layer(Sandbox, { enableInternet: true }), ), ),) {}Worker: Dynamic Workers
Section titled “Worker: Dynamic Workers”WorkerLoader lets you spin up ephemeral Workers at runtime
from inline JavaScript modules. This is useful for sandboxing
user-provided code or running untrusted scripts in isolation.
// Constructionconst loader = yield* Cloudflare.WorkerLoader("Loader");
return { fetch: Effect.gen(function* () { const worker = yield* loader.load({ compatibilityDate: "2026-08-31", mainModule: "worker.js", modules: { "worker.js": `export default { async fetch(req) { return new Response("sandboxed"); } }`, }, });
const res = yield* worker.fetch( HttpClientRequest.get("https://worker/"), ); return HttpServerResponse.fromClientResponse(res); }),};WorkerEntrypoint
Section titled “WorkerEntrypoint”Source:
src/Cloudflare/Workers/WorkerEntrypoint.ts
Bind a specific WorkerEntrypoint class exported by another Worker.
Binding a Worker directly in env (env: { TARGET: worker }) targets
its default entrypoint. A Worker that exposes additional
WorkerEntrypoint classes — workerd treats every named class export of
an entry module as an entrypoint — is bound with WorkerEntrypoint,
which selects the class by name and can deliver ctx.props to it.
WorkerEntrypoint: Defining the Target Entrypoint
Section titled “WorkerEntrypoint: Defining the Target Entrypoint”Export a class extending Cloudflare’s native WorkerEntrypoint from
the target Worker’s module. This Api class defines the RPC methods
that callers can invoke through a named service binding.
import { WorkerEntrypoint } from "cloudflare:workers";
export class Api extends WorkerEntrypoint { async greet(name: string): Promise<string> { return `hello ${name}`; }}
export default { async fetch() { return new Response("ok"); },};WorkerEntrypoint: Binding a Named Entrypoint
Section titled “WorkerEntrypoint: Binding a Named Entrypoint”Import the exported Api class as a type and select its named export
with "Api". Pass its instance type (Api, not typeof Api) to get
Cloudflare’s native Service<Api> RPC client. Without a type argument,
the binding is a bare Fetcher; the entrypoint name alone cannot
identify the class’s type.
import * as Cloudflare from "alchemy/Cloudflare";import type { Api } from "./target/src/worker.ts";
const target = yield* Cloudflare.Worker("Target", { main: "./target/src/worker.ts",});
const caller = yield* Cloudflare.Worker("Caller", { main: "./caller/src/worker.ts", env: { API: Cloudflare.WorkerEntrypoint<Api>(target, "Api"), },});import type { CallerEnv } from "../../alchemy.run.ts";
export default { async fetch(request: Request, env: CallerEnv) { return new Response(await env.API.greet("alice")); },};WorkerEntrypoint: Delivering ctx.props
Section titled “WorkerEntrypoint: Delivering ctx.props”The options form attaches properties the target reads from
this.ctx.props — workerd’s per-binding configuration channel. Output
values resolve at deploy time.
env: { VENDOR: Cloudflare.WorkerEntrypoint(vendorWorker, { entrypoint: "Vendor", props: { baseUrl: site.url }, }),}WorkerLoader
Section titled “WorkerLoader”Source:
src/Cloudflare/Workers/WorkerLoader.ts
Load and run ephemeral Workers at runtime from inline JavaScript modules.
WorkerLoader registers a worker_loader binding on the
parent Worker at deploy time. At runtime you call .load() with
inline module source code and get back a fully typed Worker
instance you can fetch or call RPC methods on. Each loaded
Worker runs in its own isolate with full sandboxing.
This is useful for evaluating user-provided code, running untrusted plugins, or dynamically generating Workers from templates.
WorkerLoader: Creating a Loader
Section titled “WorkerLoader: Creating a Loader”Yield Cloudflare.WorkerLoader(name) in your Worker’s init
phase to register the binding and get back a runtime handle. The
string argument becomes the binding name on the deployed Worker.
Registering a loader (effect-native Worker)
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
export default class EvalWorker extends Cloudflare.Worker<EvalWorker>()( "EvalWorker", { main: import.meta.url }, Effect.gen(function* () { // Registers the `worker_loader` binding on this Worker const loader = yield* Cloudflare.WorkerLoader("LOADER");
return { fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const code = yield* request.text;
// Spin up an isolated, sandboxed Worker from inline source. const worker = yield* loader.load({ compatibilityDate: "2026-08-31", mainModule: "worker.js", modules: { "worker.js": `export default { async fetch(req) { const result = (0, eval)(await req.text()); return new Response(String(result)); } }`, }, globalOutbound: null, // block outbound network access });
// Call the loaded Worker over Effect-native HTTP. const response = yield* worker.fetch( HttpClientRequest.post("https://worker/").pipe( HttpClientRequest.bodyText(code), ), ); return HttpServerResponse.fromClientResponse(response); }), }; }),) {}Declaring on env (async Worker)
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", env: { LOADER: Cloudflare.WorkerLoader() },});
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
// worker.tsexport default { async fetch(req: Request, env: WorkerEnv) { const worker = env.LOADER.load({ compatibilityDate: "2026-08-31", mainModule: "worker.js", modules: { "worker.js": "export default { fetch: () => new Response('ok') }" }, }); return worker.getEntrypoint().fetch(req); },};WorkerLoader: Loading a Worker
Section titled “WorkerLoader: Loading a Worker”Call loader.load() with a compatibility date, a main module
name, and a map of module names to source code strings. The
returned instance exposes .fetch() for HTTP and RPC methods
for named entrypoints.
const worker = loader.load({ compatibilityDate: "2026-08-31", mainModule: "worker.js", modules: { "worker.js": `export default { async fetch(request) { return new Response("Hello from dynamic worker!"); } }`, },});
const response = yield* worker.fetch( HttpClientRequest.get("https://worker/"),);WorkerLoader: Caching a Worker
Section titled “WorkerLoader: Caching a Worker”Call loader.get(id, getCode) to address a dynamic Worker by
name. If an isolate with that id is already warm it is reused;
getCode runs only on a cold start. The returned stub is a
WorkerStub: call .fetch() on it directly, or
.getEntrypoint() for a named export.
const worker = yield* loader.get("eval", () => ({ compatibilityDate: "2026-08-31", mainModule: "worker.js", modules: { "worker.js": `export default { async fetch() { return new Response("cached"); } }`, },}));
const response = yield* worker.fetch( HttpClientRequest.get("https://worker/"),);WorkerLoader: Sandboxing
Section titled “WorkerLoader: Sandboxing”Set globalOutbound to null to block all outbound network
access from the dynamic Worker, or pass an RPC stub to intercept
and proxy outbound requests.
const worker = loader.load({ compatibilityDate: "2026-08-31", mainModule: "worker.js", modules: { "worker.js": `export default { async fetch(req) { // fetch() calls from here will fail return new Response("sandboxed"); } }`, }, globalOutbound: null,});WorkerLoader: Named Entrypoints
Section titled “WorkerLoader: Named Entrypoints”If the dynamic Worker exports named entrypoints, use
.getEntrypoint(name) to get a typed stub for calling its
methods.
const worker = loader.load({ ... });const api = worker.getEntrypoint<{ greet: (name: string) => Effect.Effect<string> }>("api");const greeting = yield* api.greet("world");