| 1 | --- |
| 2 | title: 2.0.0-beta.52 - Dev Servers & Browser Actions |
| 3 | date: 2026-06-05T20:00:00Z |
| 4 | category: release |
| 5 | excerpt: 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 | |
| 8 | beta.52 is a dev-loop release: `alchemy dev` can now run your static |
| 9 | site's own framework dev server instead of rebuilding on every |
| 10 | change, backed by a new `Build.DevServer` resource that survives HMR |
| 11 | restarts. Browser Rendering quick actions become first-class typed |
| 12 | Effects, 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 |
| 33 | changes — the build command runs and the output directory ships as |
| 34 | Worker assets. Under `alchemy dev`, the build is skipped and |
| 35 | `dev.command` is spawned as your local server instead: |
| 36 | |
| 37 | ```typescript |
| 38 | const 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 |
| 51 | URL in dev. The repo's `cloudflare-static-site` example was rebuilt |
| 52 | around [Zola](https://www.getzola.org/) to show the flow. |
| 53 | |
| 54 | Under the hood this is a new `Build.DevServer` resource: a long-lived |
| 55 | shell process scoped to the stack instance, started during |
| 56 | `alchemy dev`, restarted when its inputs change, and a no-op on |
| 57 | deploy. It runs inside the dev sidecar so it survives user-code HMR |
| 58 | restarts, mirrors the child's stdout/stderr to your terminal, and |
| 59 | scans output for the first `http(s)://` URL, exposed as its `url` |
| 60 | output: |
| 61 | |
| 62 | ```typescript |
| 63 | const dev = yield* DevServer("Frontend", { |
| 64 | command: "npm run dev", |
| 65 | }); |
| 66 | console.log(dev.url); // e.g. "http://localhost:5173" |
| 67 | ``` |
| 68 | |
| 69 | For servers that don't print a URL, StaticSite's `dev.url` prop sets |
| 70 | it 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 |
| 76 | assets shapes (`assets.config`, `assetsConfig`, and |
| 77 | `{ path, hash, config }`). Now `AssetsProps extends AssetsConfig`, so |
| 78 | routing 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 | |
| 95 | For `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 | |
| 100 | The `BrowserClient` returned by `yield* Cloudflare.Browser(...)` now |
| 101 | mirrors Cloudflare's Browser Rendering quick actions as typed Effect |
| 102 | operations. JSON actions (`content`, `markdown`, `links`, `scrape`, |
| 103 | `snapshot`, `json`) resolve to their parsed success payloads and fail |
| 104 | with a typed `BrowserError`; binary actions (`screenshot`, `pdf`) |
| 105 | return a `Stream` of the response bytes. No more hand-rolling |
| 106 | `Response.json()` against the raw binding: |
| 107 | |
| 108 | ```typescript |
| 109 | const browser = yield* Cloudflare.Browser({ name: "BROWSER" }); |
| 110 | const url = "https://example.com"; |
| 111 | |
| 112 | // JSON quick actions resolve to their parsed payload. |
| 113 | const markdown = yield* browser.markdown({ url }); |
| 114 | const { result: links } = yield* browser.links({ url }); |
| 115 | |
| 116 | // Binary actions stream bytes — collect or pipe them. |
| 117 | const png = yield* browser.screenshot({ url }).pipe(Stream.runCollect); |
| 118 | ``` |
| 119 | |
| 120 | Provide `Cloudflare.BrowserBindingLive` on the Worker's init Effect. |
| 121 | `raw` and `fetch` remain as escape hatches for promise-based |
| 122 | libraries 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 |
| 128 | zero-arg class-constructor form as `Cloudflare.Worker`: calling with |
| 129 | no arguments returns a constructor you `extend` to declare the site |
| 130 | as a named class — both a yieldable Effect and a type other resources |
| 131 | can reference: |
| 132 | |
| 133 | ```typescript |
| 134 | export class Website extends Cloudflare.Vite<Website>()("Website", { |
| 135 | compatibility: { |
| 136 | flags: ["nodejs_compat"], |
| 137 | }, |
| 138 | assets: { |
| 139 | runWorkerFirst: true, |
| 140 | }, |
| 141 | }) {} |
| 142 | |
| 143 | export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>; |
| 144 | |
| 145 | // in the stack: |
| 146 | const site = yield* Website; |
| 147 | ``` |
| 148 | |
| 149 | The `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) |