| 1 | --- |
| 2 | title: 2.0.0-beta.45 - Config & RPC Workers |
| 3 | date: 2026-05-29T22:00:00Z |
| 4 | category: release |
| 5 | excerpt: 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.** |
| 10 | Anything reading config off the environment now goes through |
| 11 | [`effect/Config`](https://effect.website/docs/configuration) |
| 12 | directly — `Config.string`, `Config.redacted`, and friends. |
| 13 | Replace `yield* Alchemy.Secret("API_KEY")` with |
| 14 | `yield* Config.redacted("API_KEY")`. See |
| 15 | [Concepts › Secrets and Config](/environments/secrets) for the new |
| 16 | model. |
| 17 | ::: |
| 18 | |
| 19 | `v2.0.0-beta.45` has three things worth calling out: config |
| 20 | binding now rides on `effect/Config`, and two new Worker |
| 21 | abstractions — **`Cloudflare.RpcWorker`** and |
| 22 | **`Cloudflare.RpcDurableObject`** — collapse the |
| 23 | [Effect RPC](/cloudflare/apis/effect-rpc) boilerplate down to "hand me an |
| 24 | Effect RPC schema." |
| 25 | |
| 26 | ## `effect/Config` replaces `Alchemy.Secret` / `Alchemy.Variable` |
| 27 | |
| 28 | `Alchemy.Secret` and `Alchemy.Variable` forced an accessor |
| 29 | pattern: you yielded the secret in Init to get a handle, then |
| 30 | yielded the handle *again* at runtime to read it. You couldn't |
| 31 | use the value during Init, so initializing a client meant |
| 32 | threading Outputs and accessors around for no reason. |
| 33 | |
| 34 | beta.45 drops both helpers and leans on `effect/Config`. A |
| 35 | `Config` value `yield*`ed in a Worker's Init phase is captured |
| 36 | and bound to the deploy target automatically — and the same |
| 37 | value is usable immediately, right there in Init. |
| 38 | |
| 39 | |
| 40 | ```diff lang="typescript" |
| 41 | export 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 | |
| 58 | Under the hood a custom `ConfigProvider` intercepts `Config` |
| 59 | evaluation inside a Platform context and records the binding. |
| 60 | Combinators (`withDefault`, `orElse`, `mapAttempt`, …) work as |
| 61 | usual — what gets bound is the *source* env var, not the |
| 62 | transformed result, so the combinators re-run at runtime against |
| 63 | the bound source. |
| 64 | |
| 65 | One footgun to internalize: a `Config` only `yield*`ed inside |
| 66 | `fetch` is **never bound**, because `fetch` doesn't run during |
| 67 | deploy. Always resolve it in the outer Init `Effect.gen` and |
| 68 | capture it in a `const`. |
| 69 | ([#445](https://github.com/alchemy-run/alchemy/pull/445)) |
| 70 | |
| 71 | Relatedly, **`WorkerProps.bindings` collapses into |
| 72 | `WorkerProps.env`** — there's now one place to declare a Worker's |
| 73 | environment, 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 | |
| 84 | For async (non-Effect) Workers that can't `yield*` `Config`, this |
| 85 | is also where you bind secrets: |
| 86 | |
| 87 | ```typescript |
| 88 | export 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 |
| 103 | is 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 }` |
| 106 | shape and transport plumbing for you. |
| 107 | |
| 108 | Until 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 |
| 112 | every RPC Worker. `RpcWorker` collapses it. |
| 113 | |
| 114 | ```typescript |
| 115 | export 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 | |
| 134 | Here's the payoff. Any other Worker in the same account binds it |
| 135 | with `Cloudflare.RpcWorker.bind(TaskWorker)` and gets back a |
| 136 | **fully typed RPC client** — every procedure in the schema is a |
| 137 | method you can just call, request and response typed end to end: |
| 138 | |
| 139 | ```typescript |
| 140 | export 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 | |
| 158 | The schema is recovered from the `TaskWorker` class itself (via |
| 159 | an internal stash), so binding is a single line — it mirrors |
| 160 | `Bucket.bind`, and the call goes over the in-account service |
| 161 | binding rather than the public network. |
| 162 | |
| 163 | The full walkthrough — schema, handlers, deploy, integration |
| 164 | test, cross-Worker binding, and the modular `Class.make(impl)` |
| 165 | form — 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 | |
| 171 | Alchemy's built-in DO method bridge `JSON.stringify`s every |
| 172 | return value, which strips class identity — fine for primitives, |
| 173 | a problem when a value contains `Schema.Class` instances. |
| 174 | `Cloudflare.RpcDurableObject` runs an Effect RPC server |
| 175 | on the DO's `fetch` so both ends share one `RpcSerialization` |
| 176 | codec, preserving class identity (and supporting streaming). |
| 177 | |
| 178 | ```typescript |
| 179 | export 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 | |
| 198 | A Worker reaches the DO by yielding the class to register the |
| 199 | binding, then `getByName(id)` hands back a typed client for that |
| 200 | instance — same `RpcClient` shape as `RpcWorker.bind`, every |
| 201 | procedure a callable method: |
| 202 | |
| 203 | ```typescript |
| 204 | export 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 |
| 224 | a per-request `Effect.scoped` handler and the client is freed |
| 225 | with the request. |
| 226 | |
| 227 | Both `RpcWorker` and `RpcDurableObject` ship a matching |
| 228 | **modular form** — `(name, { schema })` with no impl declares a |
| 229 | pure tagged identifier, `Class.make(impl)` returns the runtime |
| 230 | `Layer`, and `Class.from(Worker)` binds cross-script. That makes |
| 231 | a fully typed `RpcWorker → RpcDurableObject` binding |
| 232 | work across scripts without dragging the DO runtime into the |
| 233 | caller's bundle. |
| 234 | |
| 235 | The [RpcDurableObject tutorial](/cloudflare/compute/durable-objects#schemaless-rpc) |
| 236 | covers the single-DO flow, the modular split, and cross-script |
| 237 | binding. |
| 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 | |
| 268 | A batch of dev-server and bundler fixes also landed — reconciling |
| 269 | the Worker preview subdomain, websocket upgrades in vite dev, |
| 270 | external `require` calls, and tighter `Cloudflare.providers()` |
| 271 | types. 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) |