| 1 | --- |
| 2 | title: 2.0.0-beta.37 - Cross-Stack References & Bindings |
| 3 | date: 2026-05-12 |
| 4 | category: release |
| 5 | excerpt: Cross-stack and cross-stage references, fully-typed Worker-to-Worker bindings, Workflows with typed I/O, `Alchemy.Secret` / `Alchemy.Variable`, cron triggers, an Analytics Engine binding, and R2 buckets that finally empty themselves on destroy. |
| 6 | --- |
| 7 | |
| 8 | `v2.0.0-beta.37` is the biggest beta in a while. The |
| 9 | headline addition is **cross-stack and cross-stage |
| 10 | references** — the missing piece that lets PR-preview |
| 11 | stages share a database with `staging` instead of |
| 12 | re-provisioning a whole Postgres cluster every time someone |
| 13 | opens a draft PR. On top of that: fully-typed |
| 14 | Worker-to-Worker bindings, typed Workflow I/O, the new |
| 15 | `Alchemy.Secret` / `Alchemy.Variable` one-liners, cron |
| 16 | triggers, and an Analytics Engine binding. |
| 17 | |
| 18 | A bunch of this came from outside the core team — props |
| 19 | to the contributors throughout, and full credits at the |
| 20 | [bottom of the post](#contributors). |
| 21 | |
| 22 | ## Cross-stack and cross-stage references |
| 23 | |
| 24 | Lazy, typed references to resources deployed by a |
| 25 | *different* stack or stage. The use case it was built for: |
| 26 | ephemeral PR-preview stages that need a Neon project, |
| 27 | shouldn't pay for their own, and should instead share one |
| 28 | that's owned by `staging`. |
| 29 | |
| 30 | ### Pointing a PR stage at `staging`'s database |
| 31 | |
| 32 | Same `alchemy.run.ts`, conditional on the stage. If the |
| 33 | stage looks like a PR preview (`pr-147`, `pr-148`, …), |
| 34 | `Neon.Project.ref` reaches into the `staging` stage's state |
| 35 | file and pulls out the already-deployed project. Otherwise |
| 36 | the stage creates its own. |
| 37 | |
| 38 | ```typescript |
| 39 | // src/Db.ts |
| 40 | import * as Alchemy from "alchemy"; |
| 41 | import * as Drizzle from "alchemy/Drizzle"; |
| 42 | import * as Neon from "alchemy/Neon"; |
| 43 | import * as Effect from "effect/Effect"; |
| 44 | |
| 45 | export const NeonDb = Effect.gen(function* () { |
| 46 | const { stage } = yield* Alchemy.Stack; |
| 47 | |
| 48 | const schema = yield* Drizzle.Schema("app-schema", { |
| 49 | schema: "./src/schema.ts", |
| 50 | out: "./migrations", |
| 51 | }); |
| 52 | |
| 53 | // PR previews share the long-lived staging project. |
| 54 | // Every other stage gets its own. |
| 55 | const project = stage.startsWith("pr-") |
| 56 | ? yield* Neon.Project.ref("app-db", { stage: "staging" }) |
| 57 | : yield* Neon.Project("app-db", { region: "aws-us-east-1" }); |
| 58 | |
| 59 | // Branches are cheap and per-stage either way. |
| 60 | const branch = yield* Neon.Branch("app-branch", { |
| 61 | project, |
| 62 | migrationsDir: schema.out, |
| 63 | }); |
| 64 | |
| 65 | return { project, branch, schema }; |
| 66 | }); |
| 67 | ``` |
| 68 | |
| 69 | Three things to note: |
| 70 | |
| 71 | - **Same logical id, same type.** `"app-db"` matches the |
| 72 | id `staging` uses to create the project; `project` is |
| 73 | `Neon.Project` either way, so downstream |
| 74 | (`Neon.Branch({ project })`) doesn't know or care |
| 75 | whether it's real or referenced. |
| 76 | - **Resolved at plan time.** Alchemy reads the project's |
| 77 | attributes (id, host, etc.) out of `staging`'s persisted |
| 78 | [state store](/state-store). If `staging` |
| 79 | hasn't been deployed yet, plan fails loudly with |
| 80 | `InvalidReferenceError`. |
| 81 | - **PR teardown stays scoped.** `alchemy destroy --stage |
| 82 | pr-147` deletes the per-PR `Neon.Branch` but doesn't |
| 83 | touch the shared project — this stage doesn't own it. |
| 84 | |
| 85 | Deploy `staging` once, then PR stages can point at it: |
| 86 | |
| 87 | ```sh |
| 88 | alchemy deploy --stage staging # creates the project once |
| 89 | alchemy deploy --stage pr-147 # references it, creates only the branch |
| 90 | ``` |
| 91 | |
| 92 | The full file lives in |
| 93 | [`examples/cloudflare-neon-drizzle/src/Db.ts`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-neon-drizzle/src/Db.ts); |
| 94 | the guide is at |
| 95 | [Guides › Shared database across stages](/cloudflare/data/shared-database). |
| 96 | |
| 97 | ### Referencing an entire stack's outputs |
| 98 | |
| 99 | The example above pulls one **resource** across stages. |
| 100 | The other shape — pulling a **whole stack's outputs** — |
| 101 | is what you reach for in a monorepo where the frontend |
| 102 | package wants to read the backend stack's deployed URL. |
| 103 | |
| 104 | Declare a typed stack handle once: |
| 105 | |
| 106 | ```typescript |
| 107 | // backend/src/Stack.ts |
| 108 | import * as Alchemy from "alchemy"; |
| 109 | |
| 110 | export class Backend extends Alchemy.Stack< |
| 111 | Backend, |
| 112 | { url: string } |
| 113 | >()("Backend") {} |
| 114 | ``` |
| 115 | |
| 116 | Deploy the backend with `Backend.make(...)` (the typed |
| 117 | shorthand for `Alchemy.Stack`), then `yield* Backend` from |
| 118 | the frontend's stack to get its outputs back, type-checked: |
| 119 | |
| 120 | ```typescript |
| 121 | // frontend/alchemy.run.ts |
| 122 | import * as Alchemy from "alchemy"; |
| 123 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 124 | import { Backend } from "backend"; |
| 125 | import * as Effect from "effect/Effect"; |
| 126 | |
| 127 | export default Alchemy.Stack( |
| 128 | "Frontend", |
| 129 | { providers: Cloudflare.providers(), state: Cloudflare.state() }, |
| 130 | Effect.gen(function* () { |
| 131 | // Resolves Backend's outputs from the same stage of the same |
| 132 | // stack name. `pr-42` frontend reads `pr-42` backend. |
| 133 | const backend = yield* Backend; |
| 134 | // ^? { url: string } |
| 135 | |
| 136 | return yield* Cloudflare.Website.Vite("Website", { |
| 137 | env: { VITE_API_URL: backend.url }, |
| 138 | }); |
| 139 | }), |
| 140 | ); |
| 141 | ``` |
| 142 | |
| 143 | `yield* Backend` defaults to "same stage as the consumer". |
| 144 | When you need to pin — say, the prod frontend always reads |
| 145 | the prod backend regardless of which branch deploys it — |
| 146 | use `Backend.stage.<name>`: |
| 147 | |
| 148 | ```typescript |
| 149 | const backend = yield* Backend.stage.prod; // always pin to prod |
| 150 | const backend = yield* Backend.stage["pr-42"]; // arbitrary stage name |
| 151 | ``` |
| 152 | |
| 153 | Under the hood, both shapes are |
| 154 | [`Output.stackRef`](/infrastructure-as-code/outputs#ref) / |
| 155 | [`Resource.ref`](/infrastructure-as-code/resource#ref) reading the |
| 156 | state store. The new APIs just make them ergonomic. |
| 157 | |
| 158 | [Concepts › References](/infrastructure-as-code/references) · |
| 159 | [Guides › Shared database](/cloudflare/data/shared-database) · |
| 160 | [Guides › Monorepos](/project-structure/monorepo) · |
| 161 | [Tutorial › Branch from a shared database](/cloudflare/data/branch-from-shared-database) |
| 162 | |
| 163 | ## `Alchemy.Secret` and `Alchemy.Variable` |
| 164 | |
| 165 | The boring part of a stack — wiring an env var into a |
| 166 | deploy target — used to leak across three files. One yield |
| 167 | now collapses it into a line that's also a typed runtime |
| 168 | accessor. |
| 169 | |
| 170 | ```typescript |
| 171 | // alchemy.run.ts — declare once on the Worker |
| 172 | export default Cloudflare.Worker("Api", |
| 173 | { main: import.meta.filename }, |
| 174 | Effect.gen(function* () { |
| 175 | const apiKey = yield* Alchemy.Secret("OPENAI_API_KEY"); |
| 176 | // ^? Output<Redacted<string>> |
| 177 | |
| 178 | return { |
| 179 | fetch: Effect.gen(function* () { |
| 180 | // …and read the bound value inside the handler. |
| 181 | const key = yield* apiKey; // Redacted<string> |
| 182 | return HttpServerResponse.text( |
| 183 | `key has ${Redacted.value(key).length} chars`, |
| 184 | ); |
| 185 | }), |
| 186 | }; |
| 187 | }), |
| 188 | ); |
| 189 | ``` |
| 190 | |
| 191 | `Alchemy.Variable` is the same shape without `Redacted`: |
| 192 | |
| 193 | ```typescript |
| 194 | const port = yield* Alchemy.Variable("PORT", 3000); |
| 195 | const flags = yield* Alchemy.Variable("FLAGS", { beta: true }); |
| 196 | |
| 197 | // inside fetch |
| 198 | const p = yield* port; // number — 3000 |
| 199 | const f = yield* flags; // { beta: true } |
| 200 | ``` |
| 201 | |
| 202 | Both accept a literal, an `Effect`, a `Config`, or default |
| 203 | to reading the value from the active `ConfigProvider` under |
| 204 | the same name. The same call routes to the platform's |
| 205 | native secret/variable binding — Cloudflare `secret_text` |
| 206 | for `Secret`, Lambda encrypted env vars on AWS — and the |
| 207 | runtime accessor decodes back to the original type. |
| 208 | |
| 209 | The docs are at [Concepts › Secrets](/environments/secrets) and [Secrets & env](/cloudflare/security/secrets-env). |
| 210 | |
| 211 | ## Worker-to-Worker bindings, fully typed |
| 212 | |
| 213 | A Worker is now bindable as a binding on another Worker, |
| 214 | and the caller gets full RPC types on the other side — no |
| 215 | codegen, no manual interfaces, no `as` casts on `env`. |
| 216 | Three call shapes are supported, all on the same |
| 217 | deployment. |
| 218 | |
| 219 | The example: a `Backend` Worker exposing both an RPC method |
| 220 | and an HTTP route, plus a TanStack Start frontend calling |
| 221 | it three different ways. (This is the |
| 222 | [`examples/cloudflare-tanstack`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-website-tanstack-start) |
| 223 | project — cut down to the relevant pieces.) |
| 224 | |
| 225 | The backend Worker: |
| 226 | |
| 227 | ```typescript |
| 228 | // src/backend.ts |
| 229 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 230 | import * as Effect from "effect/Effect"; |
| 231 | import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; |
| 232 | import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; |
| 233 | |
| 234 | export const Bucket = Cloudflare.R2.Bucket("Bucket"); |
| 235 | |
| 236 | export default class Backend extends Cloudflare.Worker<Backend>()( |
| 237 | "Backend", |
| 238 | { main: import.meta.filename }, |
| 239 | Effect.gen(function* () { |
| 240 | const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket); |
| 241 | |
| 242 | return { |
| 243 | // RPC method — callable via `backend.hello(key)` on the other side. |
| 244 | hello: Effect.fn("Backend.hello")(function* (key: string) { |
| 245 | const object = yield* bucket.get(key); |
| 246 | return object === null ? null : yield* object.text(); |
| 247 | }), |
| 248 | |
| 249 | // HTTP handler — callable via `env.BACKEND.fetch(...)`. |
| 250 | fetch: Effect.gen(function* () { |
| 251 | const request = yield* HttpServerRequest; |
| 252 | const key = new URL(request.url, "http://backend").searchParams.get("key"); |
| 253 | if (!key) return HttpServerResponse.text("missing key", { status: 400 }); |
| 254 | |
| 255 | if (request.method === "GET") { |
| 256 | const object = yield* bucket.get(key); |
| 257 | return object === null |
| 258 | ? HttpServerResponse.text("not found", { status: 404 }) |
| 259 | : HttpServerResponse.stream(object.body); |
| 260 | } |
| 261 | return HttpServerResponse.text("method not allowed", { status: 405 }); |
| 262 | }), |
| 263 | }; |
| 264 | }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)), |
| 265 | ) {} |
| 266 | ``` |
| 267 | |
| 268 | Wire it into another Worker as a binding: |
| 269 | |
| 270 | ```typescript |
| 271 | // alchemy.run.ts |
| 272 | import Backend, { Bucket } from "./src/backend.ts"; |
| 273 | |
| 274 | export const Website = Cloudflare.Website.Vite("Website", { |
| 275 | bindings: { |
| 276 | BUCKET: Bucket, // R2 binding |
| 277 | BACKEND: Backend, // Worker-to-Worker binding |
| 278 | }, |
| 279 | }); |
| 280 | ``` |
| 281 | |
| 282 | Now the caller has three ways to talk to the backend. All |
| 283 | three are real, all three are typed, all three work in the |
| 284 | same handler — pick whichever fits the call site. |
| 285 | |
| 286 | ```typescript |
| 287 | // frontend route handler |
| 288 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 289 | import type Backend from "../backend.ts"; |
| 290 | import { env } from "../env.ts"; |
| 291 | |
| 292 | // Option 1 — async binding (just call the platform API directly). |
| 293 | const object = await env.BUCKET.get(key); |
| 294 | |
| 295 | // Option 2 — Worker-to-Worker fetch over the service binding. |
| 296 | const res = await env.BACKEND.fetch(`https://backend/?key=${encodeURIComponent(key)}`); |
| 297 | |
| 298 | // Option 3 — typed RPC. `toPromiseApi<Backend>` wraps the wire-shape |
| 299 | // binding into a Promise<T> view that throws on `Effect.fail` and |
| 300 | // unwraps stream envelopes — full method signatures from `Backend`. |
| 301 | const backend = Cloudflare.toPromiseApi<Backend>(env.BACKEND); |
| 302 | const value = await backend.hello(key); |
| 303 | // ^? string | null (typed end-to-end) |
| 304 | ``` |
| 305 | |
| 306 | Effect-native callers also get a fourth path — `yield* |
| 307 | Backend.bind(env.BACKEND)` returns the same RPC surface |
| 308 | without the Promise envelope. The HTTP option (#2) is the |
| 309 | right one when you need request/response semantics with |
| 310 | streaming bodies; the RPC option (#3) is the right one |
| 311 | when you want typed method calls. |
| 312 | |
| 313 | [Tutorial › Vite SPA + Worker bridge](/cloudflare/frontend/vite-spa) · |
| 314 | [Example › `cloudflare-tanstack`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-website-tanstack-start) |
| 315 | |
| 316 | ## Workflows with typed input and output |
| 317 | |
| 318 | `Cloudflare.Workflow` is now generic over input *and* |
| 319 | output types. The body is an `Effect.fn` that takes the |
| 320 | typed input directly; `workflow.create(input)` is |
| 321 | type-checked end to end; the returned value flows through |
| 322 | to `instance.status().output`. |
| 323 | |
| 324 | A realistic example — a notifier workflow that touches KV, |
| 325 | reads an `Alchemy.Secret`, broadcasts through a Durable |
| 326 | Object, sleeps, and finalizes — with each side effect |
| 327 | wrapped in a `task` so a crash + replay returns the |
| 328 | persisted result instead of re-running: |
| 329 | |
| 330 | ```typescript |
| 331 | // src/NotifyWorkflow.ts |
| 332 | import * as Alchemy from "alchemy"; |
| 333 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 334 | import * as Effect from "effect/Effect"; |
| 335 | import * as Redacted from "effect/Redacted"; |
| 336 | import { KV } from "./KV.ts"; |
| 337 | import Room from "./Room.ts"; |
| 338 | |
| 339 | export default class NotifyWorkflow extends Cloudflare.Workflow<NotifyWorkflow>()( |
| 340 | "Notifier", |
| 341 | Effect.gen(function* () { |
| 342 | // Outer init phase: resolve shared dependencies once. |
| 343 | const rooms = yield* Room; |
| 344 | const kv = yield* Cloudflare.KV.ReadWriteNamespace(KV); |
| 345 | const secret = yield* Alchemy.Secret("WORKFLOW_SECRET"); |
| 346 | |
| 347 | return Effect.fn(function* (input: { roomId: string; message: string }) { |
| 348 | const { roomId, message } = input; |
| 349 | |
| 350 | // Each `task` is a checkpoint — replay-safe. |
| 351 | const stored = yield* Cloudflare.Workflows.task("kv-roundtrip", |
| 352 | Effect.gen(function* () { |
| 353 | const key = `notify:${roomId}`; |
| 354 | yield* kv.put(key, message); |
| 355 | return (yield* kv.get(key)) ?? message; |
| 356 | }).pipe(Effect.orDie), |
| 357 | ); |
| 358 | |
| 359 | const value = Redacted.value(yield* secret); |
| 360 | const processed = yield* Cloudflare.Workflows.task("process", |
| 361 | Effect.succeed({ text: `Processed: ${stored}`, secret: value }), |
| 362 | ); |
| 363 | |
| 364 | yield* Cloudflare.Workflows.task("broadcast", |
| 365 | rooms.getByName(roomId).broadcast(`[workflow] ${processed.text}`), |
| 366 | ); |
| 367 | |
| 368 | yield* Cloudflare.Workflows.sleep("cooldown", "2 seconds"); |
| 369 | yield* Cloudflare.Workflows.task("finalize", |
| 370 | rooms.getByName(roomId).broadcast(`[workflow] complete for ${roomId}`), |
| 371 | ); |
| 372 | |
| 373 | return processed; |
| 374 | }); |
| 375 | }), |
| 376 | ) {} |
| 377 | ``` |
| 378 | |
| 379 | Start it from a Worker — `create` is typed against the |
| 380 | input shape, `instance.status()` reports the typed output: |
| 381 | |
| 382 | ```typescript |
| 383 | // inside a Worker's fetch handler |
| 384 | const notifier = yield* NotifyWorkflow; |
| 385 | |
| 386 | const instance = yield* notifier.create({ |
| 387 | roomId: "room-42", |
| 388 | message: "hello", |
| 389 | }); |
| 390 | // instance.id: string |
| 391 | |
| 392 | const status = yield* (yield* notifier.get(instance.id)).status(); |
| 393 | // status.output: { text: string; secret: string } | undefined |
| 394 | ``` |
| 395 | |
| 396 | [Tutorial › Workflows](/cloudflare/compute/add-a-workflow) |
| 397 | |
| 398 | ## Cron triggers via `Cloudflare.Workers.cron(...)` |
| 399 | |
| 400 | Subscribe to a Cloudflare Cron Trigger with an Effect |
| 401 | handler. The deploy-time half attaches the cron expression |
| 402 | to the host Worker; the runtime half registers a |
| 403 | `scheduled` listener. Use it alongside any other binding |
| 404 | you've already wired up — the handler runs inside the same |
| 405 | Worker context. |
| 406 | |
| 407 | ```typescript |
| 408 | export default Cloudflare.Worker("Reporter", |
| 409 | { main: import.meta.filename }, |
| 410 | Effect.gen(function* () { |
| 411 | const kv = yield* Cloudflare.KV.ReadWriteNamespace(Counters); |
| 412 | |
| 413 | // Fires once at the top of every hour. |
| 414 | yield* Cloudflare.Workers.cron("0 * * * *").subscribe((controller) => |
| 415 | Effect.gen(function* () { |
| 416 | yield* kv.put(`tick:${controller.scheduledTime}`, "ok"); |
| 417 | yield* Effect.log(`tick at ${new Date(controller.scheduledTime).toISOString()}`); |
| 418 | }), |
| 419 | ); |
| 420 | |
| 421 | return { fetch: Effect.succeed(HttpServerResponse.text("ok")) }; |
| 422 | }), |
| 423 | ); |
| 424 | ``` |
| 425 | |
| 426 | Multiple `cron("…")` calls register multiple schedules on |
| 427 | the same Worker; the cheapest cron granularity Cloudflare |
| 428 | offers is one minute (`* * * * *`). |
| 429 | |
| 430 | — *Thanks to **Dawson** ([#288](https://github.com/alchemy-run/alchemy/pull/288)) |
| 431 | for the contribution.* |
| 432 | |
| 433 | ## Analytics Engine binding |
| 434 | |
| 435 | Cloudflare Workers Analytics Engine, exposed as a |
| 436 | zero-provisioning Worker binding. Declare a dataset |
| 437 | resource, bind it on a Worker, and call `writeDataPoint` |
| 438 | from the handler — same Effect-error channel as every |
| 439 | other Alchemy binding. |
| 440 | |
| 441 | ```typescript |
| 442 | // alchemy.run.ts |
| 443 | export const Events = Cloudflare.AnalyticsEngine.Dataset("Events", { |
| 444 | dataset: "app-events", |
| 445 | }); |
| 446 | |
| 447 | // inside the Worker |
| 448 | export default Cloudflare.Worker("Api", |
| 449 | { main: import.meta.filename }, |
| 450 | Effect.gen(function* () { |
| 451 | const analytics = yield* Cloudflare.AnalyticsEngineDataset.bind(Events); |
| 452 | |
| 453 | return { |
| 454 | fetch: Effect.gen(function* () { |
| 455 | yield* analytics.writeDataPoint({ |
| 456 | indexes: ["account-1"], // queryable, low-cardinality |
| 457 | blobs: ["signup"], // arbitrary string columns |
| 458 | doubles: [1], // numeric metrics |
| 459 | }); |
| 460 | return HttpServerResponse.text("recorded"); |
| 461 | }), |
| 462 | }; |
| 463 | }).pipe(Effect.provide(Cloudflare.AnalyticsEngineDatasetBindingLive)), |
| 464 | ); |
| 465 | ``` |
| 466 | |
| 467 | — *Thanks to **Dawson** ([#286](https://github.com/alchemy-run/alchemy/pull/286)) |
| 468 | for the contribution.* |
| 469 | |
| 470 | ## R2 buckets empty themselves on destroy |
| 471 | |
| 472 | `destroy` on an `Bucket` now drains the contents before |
| 473 | deleting the bucket. No more `BucketNotEmpty` failures |
| 474 | during teardown — ephemeral PR previews and integration |
| 475 | tests tear down cleanly with a single `alchemy destroy`. |
| 476 | |
| 477 | ```typescript |
| 478 | const Photos = Cloudflare.R2.Bucket("Photos"); |
| 479 | // `alchemy destroy` empties Photos and deletes it in one go |
| 480 | ``` |
| 481 | |
| 482 | If you want the old behavior (fail if non-empty) for |
| 483 | production guards, opt out per-bucket: |
| 484 | |
| 485 | ```typescript |
| 486 | const Photos = Cloudflare.R2.Bucket("Photos", { emptyOnDestroy: false }); |
| 487 | ``` |
| 488 | |
| 489 | — *Thanks to **Michael K** ([#276](https://github.com/alchemy-run/alchemy/pull/276)) |
| 490 | for the contribution.* |
| 491 | |
| 492 | ## Fixes worth knowing about |
| 493 | |
| 494 | - **D1 `prepare()` / `bind()` are synchronous now.** Matches |
| 495 | the upstream Cloudflare Workers API — no more `yield*` |
| 496 | on trivial statement construction. |
| 497 | - **WASM modules in the local sidecar bundle.** `bun alchemy |
| 498 | dev` now correctly bundles `.wasm` modules into the local |
| 499 | sidecar, fixing a class of "module not found" errors for |
| 500 | Workers that depend on WASM. *Thanks to **Baptiste |
| 501 | Arnaud** |
| 502 | ([#305](https://github.com/alchemy-run/alchemy/pull/305)).* |
| 503 | - **Unresolved `Output` JS-coercion throws.** Accidentally |
| 504 | using an unresolved `Output<string>` in a template |
| 505 | literal (e.g. `` `${bucket.bucketName}` `` outside an |
| 506 | Effect) previously coerced to `"[object Output]"` and |
| 507 | shipped garbage to the cloud. It now throws. *Thanks to |
| 508 | **Zé Yuri** |
| 509 | ([#306](https://github.com/alchemy-run/alchemy/pull/306)).* |
| 510 | - **Deprecated libsodium wrapper types removed.** No public |
| 511 | API impact; if you were importing internal types they're |
| 512 | gone. *Thanks to **齐天大圣** |
| 513 | ([#311](https://github.com/alchemy-run/alchemy/pull/311)).* |
| 514 | |
| 515 | ## Contributors |
| 516 | |
| 517 | Big thank-you to everyone who shipped code in this beta: |
| 518 | |
| 519 | - **Michael K** — R2 empty-on-destroy ([#276](https://github.com/alchemy-run/alchemy/pull/276)) |
| 520 | - **Dawson** — Worker cron triggers ([#288](https://github.com/alchemy-run/alchemy/pull/288)) |
| 521 | - **Dawson** — Analytics Engine binding ([#286](https://github.com/alchemy-run/alchemy/pull/286)) |
| 522 | - **Baptiste Arnaud** — WASM in local sidecar bundle ([#305](https://github.com/alchemy-run/alchemy/pull/305)) |
| 523 | - **Zé Yuri** — throw on unresolved `Output` JS-coercion ([#306](https://github.com/alchemy-run/alchemy/pull/306)) |
| 524 | - **齐天大圣** — remove deprecated libsodium wrapper types ([#311](https://github.com/alchemy-run/alchemy/pull/311)) |
| 525 | |
| 526 | ## Where to go next |
| 527 | |
| 528 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta37) |
| 529 | - [Compare v2.0.0-beta.36 → v2.0.0-beta.37](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.36...v2.0.0-beta.37) |