1---
2title: 2.0.0-beta.62 - Build-Once Workers & GitHub Action
3date: 2026-07-14T06:00:00Z
4category: release
5excerpt: 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
8beta.62 makes Workers build their layer stack **once per isolate**
9instead of on every request. It also adds an official **GitHub
10Action**, **PlanetScale OAuth login**, a **custom Worker entry**
11for 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
28The Worker bridge used to rebuild your entire layer stack on
29**every request**. Every `Layer.effect`, config decode, and client
30constructor re-ran per event. Now layers build once, on the first
31event. Every later event reuses them
32([#774](https://github.com/alchemy-run/alchemy/pull/774)):
33
34```typescript
35Effect.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
50Init cost is paid once at cold start. After that, every request
51goes straight to the handler. Durable Objects get the same win:
52one layer build shared across activations.
53
54Sharing one build across events needs cross-request async. One
55event awaits a layer build that another event started. workerd
56normally forbids this: a promise created in one request context
57can'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)
59compatibility flag automatically for Workers with a compatibility
60date before 2024-10-14. Later dates enable it by default. Each
61awaiting event also pins the in-flight build with `ctx.waitUntil`,
62so workerd doesn't drop the continuation when the original request
63ends. Opting out with `no_handle_cross_request_promise_resolution`
64is a deploy-time error.
65
66There is one migration. Anything **disposable** moves out of init
67and into the handler. The handler's per-event `Scope` runs its
68finalizers after the response:
69
70```typescript
71// ❌ init: workerd has no isolate teardown, so this finalizer never runs
72const pool = yield* acquirePool();
73
74// ✅ handler: acquired per event, released when the event settles
75fetch: Effect.gen(function* () {
76 const conn = yield* acquireConn();
77 // ...
78}),
79```
80
81You can still declare the resource in init and use it at runtime.
82An Effect is only a description, so declaring one in init acquires
83nothing:
84
85```typescript
86import * as PgClient from "@effect/sql-pg/PgClient";
87
88Effect.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
103The pool is acquired on the event's `Scope` and released when the
104event settles.
105
106`proxyChain` removes the extra `yield*`. It wraps the deferred
107client in a chainable handle. Yielding a query resolves the client
108on the current event's `Scope`, then replays the chain against it:
109
110```typescript
111import { proxyChain } from "alchemy/Util/proxy-chain";
112
113Effect.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
127the acquisition per event. The first query opens the pool, every
128later query in the same event reuses it, and the pool closes when
129the event settles:
130
131```typescript
132Effect.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
146See
147[Postgres.ts](https://github.com/alchemy-run/alchemy/blob/main/packages/alchemy/src/Drizzle/Postgres.ts)
148for the full implementation. It is also why `Drizzle.postgres` now
149works inside Durable Object methods.
150
151Lambda gets a real shutdown from the same change. The generated
152entry registers an internal extension, so the instance receives
153SIGTERM and a ~500 ms window to run init-level finalizers.
154
155Docs: [Instance scope vs request
156scope](/infrastructure-as-effects/runtime#instance-scope-vs-request-scope).
157
158## Deploy from GitHub Actions
159
160One step resolves the Alchemy lifecycle from the GitHub event.
161Opening a PR deploys a `staging-{pr}` stage, closing it destroys
162the stage, and pushing to main deploys `prod`
163([#699](https://github.com/alchemy-run/alchemy/pull/699)).
164Thanks **Harry Solovay**!
165
166```yml
167- uses: alchemy-run/alchemy@v1
168```
169
170A new `GitHub.GitHubEnv` config exposes the Actions metadata to
171your stack. It resolves to `undefined` outside CI, so the same
172stack runs unchanged locally:
173
174```typescript
175import * as GitHub from "alchemy/GitHub";
176
177const github = yield* GitHub.GitHubEnv;
178
179if (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
189Docs: [CI](/environments/ci).
190
191## PlanetScale OAuth login
192
193`alchemy login` now offers browser-based OAuth for PlanetScale,
194the same flow as Cloudflare
195([#467](https://github.com/alchemy-run/alchemy/pull/467)):
196
197```sh
198alchemy login
199# authorize in the browser → token stored, refreshed proactively
200```
201
202Env vars and service tokens keep working. Both providers now land
203on 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
209custom Worker entry to the Vite plugin, in dev and deploy
210([#779](https://github.com/alchemy-run/alchemy/pull/779)).
211Thanks **Daniel Gangl**!
212
213```typescript
214// worker/index.ts: export more than the framework's fetch handler
215import handler from "virtual:react-router/unstable_rsc/server";
216export { MyDurableObject } from "./my-durable-object";
217export default handler;
218```
219
220```typescript
221yield* Cloudflare.Website.Vite("Site", {
222 rootDir: "./site",
223 main: "./site/worker/index.ts",
224});
225```
226
227React Router / RSC setups need this. The framework plugin owns the
228entry, 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)