| 1 | --- |
| 2 | title: 2.0.0-beta.62 - Build-Once Workers & GitHub Action |
| 3 | date: 2026-07-14T06:00:00Z |
| 4 | category: release |
| 5 | excerpt: v2.0.0-beta.62 makes Workers build their layer stack once per isolate instead of on every request. It also adds an official GitHub Action, PlanetScale OAuth login, a custom Worker entry for Vite websites, and a batch of `alchemy dev` fixes. |
| 6 | --- |
| 7 | |
| 8 | beta.62 makes Workers build their layer stack **once per isolate** |
| 9 | instead of on every request. It also adds an official **GitHub |
| 10 | Action**, **PlanetScale OAuth login**, a **custom Worker entry** |
| 11 | for Vite websites, and a batch of `alchemy dev` fixes. |
| 12 | |
| 13 | :::caution |
| 14 | **Breaking changes** — migration for each is below. |
| 15 | |
| 16 | - **Worker layers build once per isolate, not once per request** |
| 17 | ([#774](https://github.com/alchemy-run/alchemy/pull/774)). |
| 18 | Connections and pools move from init to the handler. |
| 19 | - **effect `>=4.0.0-beta.97` is required** |
| 20 | ([#801](https://github.com/alchemy-run/alchemy/pull/801), |
| 21 | [#813](https://github.com/alchemy-run/alchemy/pull/813)). |
| 22 | effect removed several `Schedule` APIs. Alchemy is migrated and |
| 23 | the peer floor is raised to match. |
| 24 | ::: |
| 25 | |
| 26 | ## Layers build once per isolate |
| 27 | |
| 28 | The Worker bridge used to rebuild your entire layer stack on |
| 29 | **every request**. Every `Layer.effect`, config decode, and client |
| 30 | constructor re-ran per event. Now layers build once, on the first |
| 31 | event. Every later event reuses them |
| 32 | ([#774](https://github.com/alchemy-run/alchemy/pull/774)): |
| 33 | |
| 34 | ```typescript |
| 35 | Effect.gen(function* () { |
| 36 | // init: runs once per isolate. beta.61 ran this on every request. |
| 37 | const client = yield* HttpClient.HttpClient; |
| 38 | const response = yield* client.get("https://example.com/config.json"); |
| 39 | const config = yield* response.json; |
| 40 | |
| 41 | return { |
| 42 | fetch: Effect.gen(function* () { |
| 43 | // handler: runs per event, reusing the cached config |
| 44 | return yield* HttpServerResponse.json(config); |
| 45 | }), |
| 46 | }; |
| 47 | }) |
| 48 | ``` |
| 49 | |
| 50 | Init cost is paid once at cold start. After that, every request |
| 51 | goes straight to the handler. Durable Objects get the same win: |
| 52 | one layer build shared across activations. |
| 53 | |
| 54 | Sharing one build across events needs cross-request async. One |
| 55 | event awaits a layer build that another event started. workerd |
| 56 | normally forbids this: a promise created in one request context |
| 57 | can't be resolved from another. So alchemy enables the |
| 58 | [`handle_cross_request_promise_resolution`](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#handle-cross-request-promise-resolution-correctly) |
| 59 | compatibility flag automatically for Workers with a compatibility |
| 60 | date before 2024-10-14. Later dates enable it by default. Each |
| 61 | awaiting event also pins the in-flight build with `ctx.waitUntil`, |
| 62 | so workerd doesn't drop the continuation when the original request |
| 63 | ends. Opting out with `no_handle_cross_request_promise_resolution` |
| 64 | is a deploy-time error. |
| 65 | |
| 66 | There is one migration. Anything **disposable** moves out of init |
| 67 | and into the handler. The handler's per-event `Scope` runs its |
| 68 | finalizers after the response: |
| 69 | |
| 70 | ```typescript |
| 71 | // ❌ init: workerd has no isolate teardown, so this finalizer never runs |
| 72 | const pool = yield* acquirePool(); |
| 73 | |
| 74 | // ✅ handler: acquired per event, released when the event settles |
| 75 | fetch: Effect.gen(function* () { |
| 76 | const conn = yield* acquireConn(); |
| 77 | // ... |
| 78 | }), |
| 79 | ``` |
| 80 | |
| 81 | You can still declare the resource in init and use it at runtime. |
| 82 | An Effect is only a description, so declaring one in init acquires |
| 83 | nothing: |
| 84 | |
| 85 | ```typescript |
| 86 | import * as PgClient from "@effect/sql-pg/PgClient"; |
| 87 | |
| 88 | Effect.gen(function* () { |
| 89 | // init: declare the client. Nothing connects yet. |
| 90 | const client = PgClient.make({ url }); |
| 91 | |
| 92 | return { |
| 93 | fetch: Effect.gen(function* () { |
| 94 | // handler: connects on this event's Scope |
| 95 | const sql = yield* client; |
| 96 | const rows = yield* sql`select * from users`; |
| 97 | return yield* HttpServerResponse.json(rows); |
| 98 | }), |
| 99 | }; |
| 100 | }) |
| 101 | ``` |
| 102 | |
| 103 | The pool is acquired on the event's `Scope` and released when the |
| 104 | event settles. |
| 105 | |
| 106 | `proxyChain` removes the extra `yield*`. It wraps the deferred |
| 107 | client in a chainable handle. Yielding a query resolves the client |
| 108 | on the current event's `Scope`, then replays the chain against it: |
| 109 | |
| 110 | ```typescript |
| 111 | import { proxyChain } from "alchemy/Util/proxy-chain"; |
| 112 | |
| 113 | Effect.gen(function* () { |
| 114 | // init: a chainable handle over the deferred client |
| 115 | const sql = proxyChain(PgClient.make({ url })); |
| 116 | |
| 117 | return { |
| 118 | fetch: Effect.gen(function* () { |
| 119 | const rows = yield* sql`select * from users`; |
| 120 | return yield* HttpServerResponse.json(rows); |
| 121 | }), |
| 122 | }; |
| 123 | }) |
| 124 | ``` |
| 125 | |
| 126 | `Drizzle.postgres` is this pattern plus `Effect.cached` to memoize |
| 127 | the acquisition per event. The first query opens the pool, every |
| 128 | later query in the same event reuses it, and the pool closes when |
| 129 | the event settles: |
| 130 | |
| 131 | ```typescript |
| 132 | Effect.gen(function* () { |
| 133 | // init: declares the handle. No connection is made. |
| 134 | const db = yield* Drizzle.postgres(hyperdrive.connectionString); |
| 135 | |
| 136 | return { |
| 137 | fetch: Effect.gen(function* () { |
| 138 | // first query builds the pool, memoized on this event's Scope |
| 139 | const rows = yield* db.select().from(users); |
| 140 | return yield* HttpServerResponse.json(rows); |
| 141 | }), |
| 142 | }; |
| 143 | }) |
| 144 | ``` |
| 145 | |
| 146 | See |
| 147 | [Postgres.ts](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Drizzle/Postgres.ts) |
| 148 | for the full implementation. It is also why `Drizzle.postgres` now |
| 149 | works inside Durable Object methods. |
| 150 | |
| 151 | Lambda gets a real shutdown from the same change. The generated |
| 152 | entry registers an internal extension, so the instance receives |
| 153 | SIGTERM and a ~500 ms window to run init-level finalizers. |
| 154 | |
| 155 | Docs: [Instance scope vs request |
| 156 | scope](/infrastructure-as-effects/runtime#instance-scope-vs-request-scope). |
| 157 | |
| 158 | ## Deploy from GitHub Actions |
| 159 | |
| 160 | One step resolves the Alchemy lifecycle from the GitHub event. |
| 161 | Opening a PR deploys a `staging-{pr}` stage, closing it destroys |
| 162 | the stage, and pushing to main deploys `prod` |
| 163 | ([#699](https://github.com/alchemy-run/alchemy/pull/699)). |
| 164 | Thanks **Harry Solovay**! |
| 165 | |
| 166 | ```yml |
| 167 | - uses: alchemy-run/alchemy@v1 |
| 168 | ``` |
| 169 | |
| 170 | A new `GitHub.GitHubEnv` config exposes the Actions metadata to |
| 171 | your stack. It resolves to `undefined` outside CI, so the same |
| 172 | stack runs unchanged locally: |
| 173 | |
| 174 | ```typescript |
| 175 | import * as GitHub from "alchemy/GitHub"; |
| 176 | |
| 177 | const github = yield* GitHub.GitHubEnv; |
| 178 | |
| 179 | if (github?.pr) { |
| 180 | yield* GitHub.Comment("preview-comment", { |
| 181 | owner: github.owner, |
| 182 | repository: github.repository, |
| 183 | issueNumber: github.pr, |
| 184 | body: `Preview for ${github.sha}`, |
| 185 | }); |
| 186 | } |
| 187 | ``` |
| 188 | |
| 189 | Docs: [CI](/environments/ci). |
| 190 | |
| 191 | ## PlanetScale OAuth login |
| 192 | |
| 193 | `alchemy login` now offers browser-based OAuth for PlanetScale, |
| 194 | the same flow as Cloudflare |
| 195 | ([#467](https://github.com/alchemy-run/alchemy/pull/467)): |
| 196 | |
| 197 | ```sh |
| 198 | alchemy login |
| 199 | # authorize in the browser → token stored, refreshed proactively |
| 200 | ``` |
| 201 | |
| 202 | Env vars and service tokens keep working. Both providers now land |
| 203 | on shared `/auth/success` and `/auth/error` pages |
| 204 | ([#780](https://github.com/alchemy-run/alchemy/pull/780)). |
| 205 | |
| 206 | ## Custom Worker entry for Vite websites |
| 207 | |
| 208 | `Cloudflare.Website.Vite` accepts a `main` prop that forwards a |
| 209 | custom Worker entry to the Vite plugin, in dev and deploy |
| 210 | ([#779](https://github.com/alchemy-run/alchemy/pull/779)). |
| 211 | Thanks **Daniel Gangl**! |
| 212 | |
| 213 | ```typescript |
| 214 | // worker/index.ts: export more than the framework's fetch handler |
| 215 | import handler from "virtual:react-router/unstable_rsc/server"; |
| 216 | export { MyDurableObject } from "./my-durable-object"; |
| 217 | export default handler; |
| 218 | ``` |
| 219 | |
| 220 | ```typescript |
| 221 | yield* Cloudflare.Website.Vite("Site", { |
| 222 | rootDir: "./site", |
| 223 | main: "./site/worker/index.ts", |
| 224 | }); |
| 225 | ``` |
| 226 | |
| 227 | React Router / RSC setups need this. The framework plugin owns the |
| 228 | entry, so the old `rollupOptions.input` workaround doesn't apply. |
| 229 | |
| 230 | ## `alchemy dev` reliability |
| 231 | |
| 232 | - **Hot reload no longer kills service bindings** |
| 233 | ([#812](https://github.com/alchemy-run/alchemy/pull/812)) — |
| 234 | reloading a worker used to delete its replacement's registration, |
| 235 | so RPC calls failed with `Worker not found` after every backend |
| 236 | change. Thanks **utopy**! |
| 237 | - **Container environment variables work in dev** |
| 238 | ([#785](https://github.com/alchemy-run/alchemy/pull/785)). |
| 239 | - **Dev container images persist across restarts** |
| 240 | ([#727](https://github.com/alchemy-run/alchemy/pull/727)). |
| 241 | - **Remote images and external Dockerfiles work in |
| 242 | `LocalContainerProvider`** |
| 243 | ([#790](https://github.com/alchemy-run/alchemy/pull/790)). |
| 244 | - **The provider RPC session is cached and reused** |
| 245 | ([#789](https://github.com/alchemy-run/alchemy/pull/789)). |
| 246 | - **Dev-URL detection favors localhost/IP URLs** |
| 247 | ([#787](https://github.com/alchemy-run/alchemy/pull/787)). |
| 248 | |
| 249 | ## Also in this release |
| 250 | |
| 251 | - **`plan` no longer claims ownership of adopted resources** |
| 252 | ([#794](https://github.com/alchemy-run/alchemy/pull/794)) — |
| 253 | a read-only dry run could silently adopt an unowned cloud |
| 254 | resource, and a later unrelated deploy would orphan-delete it. |
| 255 | Thanks **Daniel Gangl**! |
| 256 | - **State-store credentials are bound to the account** |
| 257 | ([#760](https://github.com/alchemy-run/alchemy/pull/760)) — |
| 258 | after `alchemy login --configure` switched accounts, state |
| 259 | reads/writes silently kept going to the old account's store. |
| 260 | - **Provider handlers receive the resource `fqn`** |
| 261 | ([#783](https://github.com/alchemy-run/alchemy/pull/783)) — |
| 262 | the fully-qualified name joins `id` in every lifecycle handler, |
| 263 | so custom providers can stamp collision-free ownership metadata. |
| 264 | Thanks **Daniel Gangl**! |
| 265 | - **Cron handlers keep their requirements** |
| 266 | ([#791](https://github.com/alchemy-run/alchemy/pull/791)) — |
| 267 | services required by a `Workers.cron` handler surface on the |
| 268 | Worker's init effect again. Thanks **Jack Bisceglia**! |
| 269 | - **Vite build output is read after the build resolves** |
| 270 | ([#795](https://github.com/alchemy-run/alchemy/pull/795)) — |
| 271 | fixes a bundling race in `Website` deploys. |
| 272 | - **Container healthcheck durations are unit-suffixed** |
| 273 | ([#778](https://github.com/alchemy-run/alchemy/pull/778)) — |
| 274 | Docker rejects bare numbers. Thanks **Matthew Aylward**! |
| 275 | - **`alchemy state clear` respects `ALCHEMY_PROFILE`** |
| 276 | ([ba705](https://github.com/alchemy-run/alchemy/commit/ba70585f)). |
| 277 | - **TypeScript 7 (stable) toolchain** |
| 278 | ([#804](https://github.com/alchemy-run/alchemy/pull/804)). |
| 279 | - **New guides**: [Worker Loader](/cloudflare/compute/worker-loader), |
| 280 | [Rate limiting](/cloudflare/compute/rate-limiting), and |
| 281 | [Workers Cache](/cloudflare/compute/cache) |
| 282 | ([#805](https://github.com/alchemy-run/alchemy/pull/805)), |
| 283 | plus a TanStack + Atom RPC + Drizzle example |
| 284 | ([#634](https://github.com/alchemy-run/alchemy/pull/634)). |
| 285 | |
| 286 | ## Where to go next |
| 287 | |
| 288 | - [Instance scope vs request scope](/infrastructure-as-effects/runtime#instance-scope-vs-request-scope) |
| 289 | - [CI](/environments/ci) |
| 290 | - [Workers](/cloudflare/compute/workers) |
| 291 | - [Workers Cache](/cloudflare/compute/cache) |
| 292 | - [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta62) |
| 293 | - [Compare v2.0.0-beta.61 → v2.0.0-beta.62](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.61...v2.0.0-beta.62) |