1---
2title: 2.0.0-beta.52 - Dev Servers & Browser Actions
3date: 2026-06-05T20:00:00Z
4category: release
5excerpt: v2.0.0-beta.52 brings local dev servers to `Cloudflare.StaticSite` via the new `Build.DevServer` resource, flattens the `assets` shape across Worker, StaticSite, and Vite, and wraps Browser Rendering quick actions in typed Effects.
6---
7
8beta.52 is a dev-loop release: `alchemy dev` can now run your static
9site's own framework dev server instead of rebuilding on every
10change, backed by a new `Build.DevServer` resource that survives HMR
11restarts. Browser Rendering quick actions become first-class typed
12Effects, and `Vite` / `StaticSite` gain the same class form as
13`Worker`.
14
15:::caution
16**Breaking changes:**
17
18- **`props.assets` is one flat shape** across `Worker`, `StaticSite`,
19 and `Vite` ([#534](https://github.com/alchemy-run/alchemy/pull/534)).
20 Asset-routing options move up a level: `assets.config.runWorkerFirst`
21 becomes `assets.runWorkerFirst`, StaticSite's `assetsConfig` prop is
22 renamed to `assets`, and the prebuilt form `{ path, hash, config }`
23 becomes `{ directory, hash, ...config }`. Migration is a one-level
24 un-nesting — see the diff below.
25- **effect peer dependency bumped to `4.0.0-beta.78`**
26 ([#542](https://github.com/alchemy-run/alchemy/pull/542)) —
27 update your project's `effect` version to match.
28:::
29
30## Local dev servers for StaticSite
31
32`Cloudflare.StaticSite` grows a `dev` prop. On deploy nothing
33changes — the build command runs and the output directory ships as
34Worker assets. Under `alchemy dev`, the build is skipped and
35`dev.command` is spawned as your local server instead:
36
37```typescript
38const site = yield* Cloudflare.StaticSite("Website", {
39 command: "zola build",
40 outdir: "public",
41 dev: {
42 command: "zola serve",
43 },
44 assets: {
45 notFoundHandling: "404-page",
46 },
47});
48```
49
50`site.url` is the deployed URL on deploy and the local dev server's
51URL in dev. The repo's `cloudflare-static-site` example was rebuilt
52around [Zola](https://www.getzola.org/) to show the flow.
53
54Under the hood this is a new `Build.DevServer` resource: a long-lived
55shell process scoped to the stack instance, started during
56`alchemy dev`, restarted when its inputs change, and a no-op on
57deploy. It runs inside the dev sidecar so it survives user-code HMR
58restarts, mirrors the child's stdout/stderr to your terminal, and
59scans output for the first `http(s)://` URL, exposed as its `url`
60output:
61
62```typescript
63const dev = yield* DevServer("Frontend", {
64 command: "npm run dev",
65});
66console.log(dev.url); // e.g. "http://localhost:5173"
67```
68
69For servers that don't print a URL, StaticSite's `dev.url` prop sets
70it explicitly.
71([#537](https://github.com/alchemy-run/alchemy/pull/537))
72
73## One `assets` shape everywhere
74
75`Worker`, `StaticSite`, and `Vite` previously took three different
76assets shapes (`assets.config`, `assetsConfig`, and
77`{ path, hash, config }`). Now `AssetsProps extends AssetsConfig`, so
78routing options like `runWorkerFirst`, `htmlHandling`, and
79`notFoundHandling` sit directly on `assets` for all three:
80
81```diff lang="typescript"
82 const app = yield* Cloudflare.Vite("SolidStart", {
83 compatibility: {
84 flags: ["nodejs_compat"],
85 },
86 assets: {
87- config: {
88- runWorkerFirst: true,
89- },
90+ runWorkerFirst: true,
91 },
92 });
93```
94
95For `StaticSite`, rename the `assetsConfig:` key to `assets:`.
96([#534](https://github.com/alchemy-run/alchemy/pull/534))
97
98## Effect-native Browser quick actions
99
100The `BrowserClient` returned by `yield* Cloudflare.Browser(...)` now
101mirrors Cloudflare's Browser Rendering quick actions as typed Effect
102operations. JSON actions (`content`, `markdown`, `links`, `scrape`,
103`snapshot`, `json`) resolve to their parsed success payloads and fail
104with a typed `BrowserError`; binary actions (`screenshot`, `pdf`)
105return a `Stream` of the response bytes. No more hand-rolling
106`Response.json()` against the raw binding:
107
108```typescript
109const browser = yield* Cloudflare.Browser({ name: "BROWSER" });
110const url = "https://example.com";
111
112// JSON quick actions resolve to their parsed payload.
113const markdown = yield* browser.markdown({ url });
114const { result: links } = yield* browser.links({ url });
115
116// Binary actions stream bytes — collect or pipe them.
117const png = yield* browser.screenshot({ url }).pipe(Stream.runCollect);
118```
119
120Provide `Cloudflare.BrowserBindingLive` on the Worker's init Effect.
121`raw` and `fetch` remain as escape hatches for promise-based
122libraries like `@cloudflare/puppeteer`.
123([#525](https://github.com/alchemy-run/alchemy/pull/525))
124
125## Class form for Vite and StaticSite
126
127`Cloudflare.Vite` and `Cloudflare.StaticSite` support the same
128zero-arg class-constructor form as `Cloudflare.Worker`: calling with
129no arguments returns a constructor you `extend` to declare the site
130as a named class — both a yieldable Effect and a type other resources
131can reference:
132
133```typescript
134export class Website extends Cloudflare.Vite<Website>()("Website", {
135 compatibility: {
136 flags: ["nodejs_compat"],
137 },
138 assets: {
139 runWorkerFirst: true,
140 },
141}) {}
142
143export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;
144
145// in the stack:
146const site = yield* Website;
147```
148
149The `cloudflare-website-tanstack-start` example uses it.
150([#527](https://github.com/alchemy-run/alchemy/pull/527))
151
152## Also in this release
153
154- **Version Metadata binding** — `yield* Cloudflare.VersionMetadata()`
155 attaches Cloudflare's `version_metadata` binding and returns an
156 accessor for the deployed version's `id`, `tag`, and `timestamp`;
157 also declarable on `env`. Thanks
158 **[Alex](https://github.com/agcty)**
159 ([#524](https://github.com/alchemy-run/alchemy/pull/524)).
160- **Workflow runs get a per-invocation `ExecutionContext`** (scope +
161 cache), so binding helpers like `Drizzle.postgres` resolve inside
162 workflow steps just like in a fetch or queue handler. Thanks
163 **[Saatvik Arya](https://github.com/aryasaatvik)**
164 ([#515](https://github.com/alchemy-run/alchemy/pull/515)).
165- **`retain` removal policy is honored on replace** — the old
166 generation of a replaced resource is retained, not just orphan
167 deletes ([#548](https://github.com/alchemy-run/alchemy/pull/548)).
168- **Changing a `Redacted` config/secret value now triggers an
169 update** — redacted wrappers are unwrapped during diffing
170 ([#549](https://github.com/alchemy-run/alchemy/pull/549)).
171- **Local dev fixes** — the assets binding is populated for Vite
172 websites ([#547](https://github.com/alchemy-run/alchemy/pull/547)),
173 cross-script Durable Object bindings work
174 ([#540](https://github.com/alchemy-run/alchemy/pull/540)),
175 and multi-worker dev is more reliable in bun
176 ([#535](https://github.com/alchemy-run/alchemy/pull/535)).
177- **Drizzle `Schema` exposes `out` as a cwd-relative path** so
178 drizzle-kit resolves migrations regardless of where alchemy runs
179 ([#551](https://github.com/alchemy-run/alchemy/pull/551)).
180- **The Vite SPA tutorial documents the local dev workflow**
181 ([#526](https://github.com/alchemy-run/alchemy/pull/526)).
182- **`@distilled.cloud/*` bumped to 0.23.1**
183 ([#539](https://github.com/alchemy-run/alchemy/pull/539)).
184
185## Where to go next
186
187- [Vite SPA tutorial](/cloudflare/frontend/vite-spa)
188- [Workflows tutorial](/cloudflare/compute/add-a-workflow)
189- [Concepts › Local Development](/environments/local-development)
190- [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta52)
191- [Compare v2.0.0-beta.51 → v2.0.0-beta.52](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.51...v2.0.0-beta.52)