1---
2title: 2.0.0-beta.45 - Config & RPC Workers
3date: 2026-05-29T22:00:00Z
4category: release
5excerpt: v2.0.0-beta.45 replaces `Alchemy.Secret` and `Alchemy.Variable` with plain `effect/Config`, and ships two new Worker abstractions — `Cloudflare.RpcWorker` and `Cloudflare.RpcDurableObject` — that take a typed Effect RPC schema directly and hand back typed clients.
6---
7
8:::caution
9**Breaking: `Alchemy.Secret` and `Alchemy.Variable` are gone.**
10Anything reading config off the environment now goes through
11[`effect/Config`](https://effect.website/docs/configuration)
12directly — `Config.string`, `Config.redacted`, and friends.
13Replace `yield* Alchemy.Secret("API_KEY")` with
14`yield* Config.redacted("API_KEY")`. See
15[Concepts › Secrets and Config](/environments/secrets) for the new
16model.
17:::
18
19`v2.0.0-beta.45` has three things worth calling out: config
20binding now rides on `effect/Config`, and two new Worker
21abstractions — **`Cloudflare.RpcWorker`** and
22**`Cloudflare.RpcDurableObject`** — collapse the
23[Effect RPC](/cloudflare/apis/effect-rpc) boilerplate down to "hand me an
24Effect RPC schema."
25
26## `effect/Config` replaces `Alchemy.Secret` / `Alchemy.Variable`
27
28`Alchemy.Secret` and `Alchemy.Variable` forced an accessor
29pattern: you yielded the secret in Init to get a handle, then
30yielded the handle *again* at runtime to read it. You couldn't
31use the value during Init, so initializing a client meant
32threading Outputs and accessors around for no reason.
33
34beta.45 drops both helpers and leans on `effect/Config`. A
35`Config` value `yield*`ed in a Worker's Init phase is captured
36and bound to the deploy target automatically — and the same
37value is usable immediately, right there in Init.
38
39
40```diff lang="typescript"
41export default Cloudflare.Worker(
42 "Worker",
43 { main: import.meta.filename },
44 Effect.gen(function* () {
45- const API_KEY = yield* Alchemy.Secret("API_KEY");
46+ const apiKey = yield* Config.redacted("API_KEY");
47
48 return {
49 fetch: Effect.gen(function* () {
50- const apiKey = yield* API_KEY;
51 // ...
52 }),
53 };
54 }),
55);
56```
57
58Under the hood a custom `ConfigProvider` intercepts `Config`
59evaluation inside a Platform context and records the binding.
60Combinators (`withDefault`, `orElse`, `mapAttempt`, …) work as
61usual — what gets bound is the *source* env var, not the
62transformed result, so the combinators re-run at runtime against
63the bound source.
64
65One footgun to internalize: a `Config` only `yield*`ed inside
66`fetch` is **never bound**, because `fetch` doesn't run during
67deploy. Always resolve it in the outer Init `Effect.gen` and
68capture it in a `const`.
69([#445](https://github.com/alchemy-run/alchemy/pull/445))
70
71Relatedly, **`WorkerProps.bindings` collapses into
72`WorkerProps.env`** — there's now one place to declare a Worker's
73environment, whether the entries are resources, plain values, or
74`Config` references.
75
76```diff lang="typescript"
77 export const Worker = Cloudflare.Worker("Worker", {
78 main: "./src/worker.ts",
79- bindings: { Bucket, KV },
80+ env: { Bucket, KV },
81 });
82```
83
84For async (non-Effect) Workers that can't `yield*` `Config`, this
85is also where you bind secrets:
86
87```typescript
88export const Worker = Cloudflare.Worker("Worker", {
89 main: "./src/worker.ts",
90 env: {
91 API_KEY: Config.redacted("API_KEY"),
92 HOST: Config.string("HOST"),
93 Bucket,
94 },
95});
96```
97
98([#446](https://github.com/alchemy-run/alchemy/pull/446))
99
100## `Cloudflare.RpcWorker`
101
102`Cloudflare.RpcWorker` is a Worker whose entire `fetch` surface
103is a typed Effect RPC schema. You pass the schema directly in
104`props.schema`, and the init returns the piped
105`RpcServer.toHttpEffect(schema)` Effect — it wraps the `{ fetch }`
106shape and transport plumbing for you.
107
108Until now you wrote that plumbing by hand: build the handler
109`Layer`, pipe it into `RpcServer.toHttpEffect`, return
110`{ fetch }` — the long-form recipe in the
111[Effect RPC guide](/cloudflare/apis/effect-rpc), nearly identical for
112every RPC Worker. `RpcWorker` collapses it.
113
114```typescript
115export default class Worker extends Cloudflare.RpcWorker<Worker>()(
116 "Worker",
117 {
118 main: import.meta.filename,
119 // the served Effect RPC schema — this is what callers bind
120 // against and get a typed client for
121 schema: TaskRpcs,
122 },
123 Effect.gen(function* () {
124 const handlers = TaskRpcs.toLayer({
125 getTask: ({ id }) => Effect.succeed(`task-${id}`),
126 });
127 return RpcServer.toHttpEffect(TaskRpcs).pipe(
128 Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerJson)),
129 );
130 }),
131) {}
132```
133
134Here's the payoff. Any other Worker in the same account binds it
135with `Cloudflare.RpcWorker.bind(TaskWorker)` and gets back a
136**fully typed RPC client** — every procedure in the schema is a
137method you can just call, request and response typed end to end:
138
139```typescript
140export default class Caller extends Cloudflare.Worker<Caller>()(
141 "Caller",
142 { main: import.meta.filename },
143 Effect.gen(function* () {
144 // tasks is an RpcClient<TaskRpcs> — every Rpc is a method
145 const tasks = yield* Cloudflare.RpcWorker.bind(TaskWorker);
146
147 return {
148 fetch: Effect.gen(function* () {
149 // just call it — typed payload, typed result, typed errors
150 const task = yield* tasks.getTask({ id: "abc" });
151 return HttpServerResponse.text(task);
152 }),
153 };
154 }),
155) {}
156```
157
158The schema is recovered from the `TaskWorker` class itself (via
159an internal stash), so binding is a single line — it mirrors
160`Bucket.bind`, and the call goes over the in-account service
161binding rather than the public network.
162
163The full walkthrough — schema, handlers, deploy, integration
164test, cross-Worker binding, and the modular `Class.make(impl)`
165form — lives in the new
166[RpcWorker tutorial](/cloudflare/compute/workers#call-another-worker).
167([#387](https://github.com/alchemy-run/alchemy/pull/387))
168
169## `Cloudflare.RpcDurableObject`
170
171Alchemy's built-in DO method bridge `JSON.stringify`s every
172return value, which strips class identity — fine for primitives,
173a problem when a value contains `Schema.Class` instances.
174`Cloudflare.RpcDurableObject` runs an Effect RPC server
175on the DO's `fetch` so both ends share one `RpcSerialization`
176codec, preserving class identity (and supporting streaming).
177
178```typescript
179export default class Counter extends Cloudflare.RpcDurableObject<Counter>()(
180 "Counter",
181 { schema: CounterRpcs },
182 Effect.gen(function* () {
183 return Effect.gen(function* () {
184 const state = yield* Cloudflare.DurableObjectState;
185 const handlers = CounterRpcs.toLayer({
186 setTitle: ({ title }) => state.storage.put("title", title),
187 getTitle: () =>
188 Effect.map(state.storage.get<string>("title"), (t) => t ?? ""),
189 });
190 return RpcServer.toHttpEffect(CounterRpcs).pipe(
191 Effect.provide(Layer.mergeAll(handlers, RpcSerialization.layerNdjson)),
192 );
193 });
194 }),
195) {}
196```
197
198A Worker reaches the DO by yielding the class to register the
199binding, then `getByName(id)` hands back a typed client for that
200instance — same `RpcClient` shape as `RpcWorker.bind`, every
201procedure a callable method:
202
203```typescript
204export default class Worker extends Cloudflare.Worker<Worker>()(
205 "Worker",
206 { main: import.meta.filename },
207 Effect.gen(function* () {
208 const counters = yield* Counter;
209
210 return {
211 fetch: Effect.gen(function* () {
212 // typed client for this DO instance
213 const counter = yield* counters.getByName("default");
214 yield* counter.setTitle({ title: "Hello" });
215 const title = yield* counter.getTitle({});
216 return HttpServerResponse.text(title);
217 }).pipe(Effect.scoped),
218 };
219 }),
220) {}
221```
222
223`getByName(id)` returns an `Effect<RpcClient>` — yield it inside
224a per-request `Effect.scoped` handler and the client is freed
225with the request.
226
227Both `RpcWorker` and `RpcDurableObject` ship a matching
228**modular form** — `(name, { schema })` with no impl declares a
229pure tagged identifier, `Class.make(impl)` returns the runtime
230`Layer`, and `Class.from(Worker)` binds cross-script. That makes
231a fully typed `RpcWorker → RpcDurableObject` binding
232work across scripts without dragging the DO runtime into the
233caller's bundle.
234
235The [RpcDurableObject tutorial](/cloudflare/compute/durable-objects#schemaless-rpc)
236covers the single-DO flow, the modular split, and cross-script
237binding.
238([#388](https://github.com/alchemy-run/alchemy/pull/388))
239
240## Also in this release
241
242- **`Cloudflare` Browser Rendering binding** — bind headless
243 Chrome to a Worker. Thanks
244 **[Alex](https://github.com/agcty)**
245 ([#372](https://github.com/alchemy-run/alchemy/pull/372)).
246- **Cross-script Durable Object binding** — the primitive under
247 `RpcDurableObject`'s `Class.from(Worker)`
248 ([#435](https://github.com/alchemy-run/alchemy/pull/435)).
249- **`Random.KeyPair` resource** — a managed public/private key
250 pair
251 ([#441](https://github.com/alchemy-run/alchemy/pull/441)).
252- **`timeout` on `FunctionProps`** — set AWS Lambda timeout
253 declaratively
254 ([#440](https://github.com/alchemy-run/alchemy/pull/440)).
255- **`Cloudflare.Queues.Queue` accepts `Duration.Input`** for time props
256 on `messages()` — `"30 seconds"` instead of raw seconds
257 ([#443](https://github.com/alchemy-run/alchemy/pull/443)).
258- **Workflows, AnalyticsEngine + SendEmail, and custom ports in
259 `alchemy dev`** — more bindings light up locally. Thanks
260 **[John Royal](https://github.com/jdrydn)**
261 ([#449](https://github.com/alchemy-run/alchemy/pull/449),
262 [#460](https://github.com/alchemy-run/alchemy/pull/460),
263 [#469](https://github.com/alchemy-run/alchemy/pull/469)).
264- **`Worker.url` infers the canonical URL** and preserves domain
265 order. Thanks **[Michael K](https://github.com/michaelkleene)**
266 ([#432](https://github.com/alchemy-run/alchemy/pull/432)).
267
268A batch of dev-server and bundler fixes also landed — reconciling
269the Worker preview subdomain, websocket upgrades in vite dev,
270external `require` calls, and tighter `Cloudflare.providers()`
271types. See the changelog for the full list.
272
273## Where to go next
274
275- [Concepts › Secrets and Config](/environments/secrets)
276- [RpcWorker tutorial](/cloudflare/compute/workers#call-another-worker)
277- [RpcDurableObject tutorial](/cloudflare/compute/durable-objects#schemaless-rpc)
278- [Effect RPC guide](/cloudflare/apis/effect-rpc)
279- [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta45)
280- [Compare v2.0.0-beta.44 → v2.0.0-beta.45](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.44...v2.0.0-beta.45)