1---
2title: 2.0.0-beta.71 - Effect beta.105 & Better Auth
3date: 2026-08-12T09:00:00Z
4category: release
5excerpt: v2.0.0-beta.71 moves to effect 4.0.0-beta.105 and drizzle 1.0.0-rc.5-ab785fc in lockstep (effect's TaggedErrorClass → TaggedError rename broke drizzle rc.4), gives @alchemy.run/better-auth an effectful API and a database Layer for every platform, lets Worker props be an Effect, and fixes the ways alchemy dev could serve the wrong app on a configured port.
6---
7
8Alchemy now runs on **effect 4.0.0-beta.105**, with
9**drizzle 1.0.0-rc.5-ab785fc** bumped in lockstep to survive
10effect's `TaggedErrorClass` → `TaggedError` rename. On top of that, `@alchemy.run/better-auth` gains an
11effectful API and a database Layer for every platform, Worker
12props can now be an Effect, and a batch of `alchemy dev` fixes
13makes configured ports, queue consumers, and pre-beta.66 state
14behave.
15
16:::caution
17**Breaking changes**
18
19- **effect `>=4.0.0-beta.105` is the new minimum peer**
20 ([#1132](https://github.com/alchemy-run/alchemy/pull/1132)).
21 effect renamed `Schema.TaggedErrorClass` to
22 `Schema.TaggedError`, so earlier betas can no longer resolve.
23 If you use Drizzle, upgrade `drizzle-orm`/`drizzle-kit` to
24 `1.0.0-rc.5-ab785fc` at the same time — see
25 [below](#effect-beta105-and-the-drizzle-rc5-lockstep).
26- **Drizzle backends move out of the `alchemy/Drizzle` barrel**
27 ([#1119](https://github.com/alchemy-run/alchemy/pull/1119)).
28 Import `alchemy/Drizzle/Postgres`, `alchemy/Drizzle/MySQL`, and
29 `alchemy/Drizzle/Cloudflare` (Durable Object) directly; the
30 barrel keeps `Schema`, `providers()`, `D1`, and the errors.
31- **`alchemy dev` now serves assets first** when
32 `assets.runWorkerFirst` is omitted
33 ([#1121](https://github.com/alchemy-run/alchemy/pull/1121)),
34 matching the deployed default. A worker that intercepts
35 asset-path requests in dev must opt in explicitly with
36 `assets: { runWorkerFirst: true }` — such apps already behaved
37 assets-first in production, so the old dev default was masking
38 a dev/prod divergence.
39:::
40
41## effect beta.105 and the drizzle rc.5 lockstep
42
43effect `4.0.0-beta.105` renamed `Schema.TaggedErrorClass` to
44`Schema.TaggedError`:
45
46```diff lang="typescript"
47-export class AuthError extends Schema.TaggedErrorClass<AuthError>()(
48+export class AuthError extends Schema.TaggedError<AuthError>()(
49 "AuthError",
50 { message: Schema.String },
51 ) {}
52```
53
54drizzle-orm `1.0.0-rc.4` — which ships effect-native clients built
55on that API — broke against it, which pinned the entire dependency
56tree: you couldn't take the effect upgrade without breaking
57drizzle, and vice versa. Drizzle's `1.0.0-rc.5-ab785fc` release
58ships `TaggedError` natively, so beta.71 upgrades both in lockstep
59([#1132](https://github.com/alchemy-run/alchemy/pull/1132),
60[#1184](https://github.com/alchemy-run/alchemy/pull/1184)):
61
62```diff lang="json"
63-"effect": ">=4.0.0-beta.102",
64-"drizzle-orm": "1.0.0-rc.4",
65-"drizzle-kit": "1.0.0-rc.4",
66+"effect": ">=4.0.0-beta.105",
67+"drizzle-orm": "1.0.0-rc.5-ab785fc",
68+"drizzle-kit": "1.0.0-rc.5-ab785fc",
69```
70
71When you upgrade, pin drizzle exactly — a `^1.0.0-rc.4` range can
72still hand you the rc.4 that breaks against effect beta.105.
73
74Alongside the version bump, the optional Drizzle drivers now load
75lazily ([#1119](https://github.com/alchemy-run/alchemy/pull/1119))
76— importing the Postgres or MySQL client no longer requires
77`@effect/sql-mysql2`/`@effect/sql-pg` to be installed unless you
78actually connect:
79
80```diff lang="typescript"
81-import * as Drizzle from "alchemy/Drizzle";
82+import * as Drizzle from "alchemy/Drizzle/Postgres";
83 const db = yield* Drizzle.Postgres(conn.connectionString, { relations });
84```
85
86Docs: [SQL & Drizzle](/sql).
87
88## Better Auth: every database, effectful API
89
90`@alchemy.run/better-auth` now supports every storage backend and
91wraps Better Auth's async API in an effectful interface
92([#1157](https://github.com/alchemy-run/alchemy/pull/1157)):
93`yield* BetterAuth(options)` gives you a fully-typed auth
94instance, and the database is a Layer you pick per platform.
95Plugin typing flows end-to-end — every `auth.api.*` endpoint is an
96Effect with a tagged `BetterAuthApiError` — and schema migrations
97run automatically at deploy as an alchemy Action, input-hash
98diffed and tree-shaken out of runtime bundles.
99
100```typescript
101const auth = yield* BetterAuth({
102 basePath: "/auth",
103 emailAndPassword: { enabled: true },
104 plugins: [anonymous()], // auth.api.signInAnonymous is now typed
105});
106
107// serve the Better Auth routes on Workers and Lambda unchanged
108if (request.url.startsWith("/auth")) return yield* auth.fetch;
109
110// or call endpoints directly — typed errors, set-cookie preserved
111yield* auth.api.signInEmail({ body: { email, password } }).pipe(
112 Effect.catchTag("BetterAuthApiError", (e) => ...), // e.statusCode, e.body.code
113);
114```
115
116The database Layers cover every environment → target pair:
117`CloudflareD1` (native binding + migrations over the D1 HTTP API,
118local simulator included), `Neon` (serverless driver over
119WebSocket — no Hyperdrive, no `pg`), `AuroraDataApi` (SQL over
120HTTPS with IAM — no VPC attachment), `CloudflareHyperdrive`,
121generic `Postgres` and `MySQL` TCP fallbacks (PlanetScale, RDS,
122any connection string), `SQLite` for local dev, `Memory` for
123tests, and `Drizzle` to bring your own db via better-auth's
124official adapter.
125
126Because the database is a Layer, your auth code never changes —
127you swap the provided Layer. On D1:
128
129```typescript
130import { CloudflareD1 } from "@alchemy.run/better-auth/CloudflareD1";
131
132export const AuthDb = Cloudflare.D1.Database("AuthDb");
133
134export default class AuthWorker extends Cloudflare.Worker<AuthWorker>()(
135 "AuthWorker",
136 { main: import.meta.url },
137 Effect.gen(function* () {
138 const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
139 return {
140 fetch: Effect.gen(function* () {
141 const request = yield* HttpServerRequest;
142 if (request.url.startsWith("/auth")) return yield* auth.fetch;
143 return HttpServerResponse.text("ok");
144 }),
145 };
146 }).pipe(Effect.provide(CloudflareD1(AuthDb))),
147) {}
148```
149
150Moving to PlanetScale Postgres is only a Layer swap — the
151`BetterAuth` program is untouched:
152
153```diff lang="typescript"
154-import { CloudflareD1 } from "@alchemy.run/better-auth/CloudflareD1";
155+import { Postgres } from "@alchemy.run/better-auth/Postgres";
156
157-export const AuthDb = Cloudflare.D1.Database("AuthDb");
158+export const AuthDb = Planetscale.PostgresDatabase("AuthDb");
159+export const AuthRole = Planetscale.PostgresRole("AuthRole", { database: AuthDb });
160+
161+const AuthDatabase = Layer.unwrap(
162+ Effect.map(AuthRole, (role) => Postgres(role.connectionUrl)),
163+);
164
165 export default class AuthWorker extends Cloudflare.Worker<AuthWorker>()(
166 "AuthWorker",
167 { main: import.meta.url },
168 Effect.gen(function* () {
169 const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
170 return {
171 fetch: ...,
172 };
173- }).pipe(Effect.provide(CloudflareD1(AuthDb))),
174+ }).pipe(Effect.provide(AuthDatabase)),
175 ) {}
176```
177
178The API reference also learned to document Layers as first-class
179pages ([#1159](https://github.com/alchemy-run/alchemy/pull/1159))
180— each database Layer gets its own generated page declaring what
181it provides and its peer dependencies.
182
183Docs: [Better Auth](/better-auth).
184
185## `alchemy dev` fixes
186
187A configured dev port could serve the wrong app
188([#1129](https://github.com/alchemy-run/alchemy/pull/1129)): the
189proxy bound only `127.0.0.1`, browsers prefer `::1`, and any
190framework dev server hunting from its default port could squat
191the IPv6 half of `localhost`:
192
193```sh
194proxy listening on 127.0.0.1:3000
195vite dev server binds [::1]:3000
196http://localhost:3000 → the wrong app
197```
198
199The proxy now owns its port on both address families, and a dev
200restart no longer cascades every configured port in the stack up
201by one.
202
203`Alchemy.remote()` bindings no longer go stale
204([#1139](https://github.com/alchemy-run/alchemy/pull/1139)) —
205Cloudflare's edge preview was pinning the remote-binding proxy to
206sessions pointing at destroyed resources, 400ing every proxied
207KV/R2/D1 call for up to 50 minutes.
208
209Framework production builds run in disposable child processes
210with `NODE_ENV=production` pinned
211([#1139](https://github.com/alchemy-run/alchemy/pull/1139),
212[#1178](https://github.com/alchemy-run/alchemy/pull/1178)), so
213user config and plugins can `chdir`, mutate `process.env`, or
214crash without taking the engine with them.
215
216And every local worker announces where it's serving as it comes
217up:
218
219```text
220[api-worker] ready at http://localhost:1337
221```
222
223## State from older versions recovers automatically
224
225Stacks that mixed `alchemy dev` and `alchemy deploy` on versions
226before beta.66 could end up with state the engine no longer knew
227how to handle — deploys and destroys failing forever with errors
228like:
229
230```text
231BadRequest: There is a malformed parameter in the request
232
233Cloudflare queue "jobs" already has a worker consumer for script
234"worker-t2po...", but this resource is configured for "worker-qwh3..."
235```
236
237beta.71 recognizes these situations and repairs them on your next
238run: old dev-mode state is routed to the local simulator instead
239of the real Cloudflare API
240([#1130](https://github.com/alchemy-run/alchemy/pull/1130)),
241resources deployed before beta.66 are properly replaced — not
242silently abandoned — when you run dev
243([#1110](https://github.com/alchemy-run/alchemy/pull/1110)), and
244queue/consumer wiring left behind by old runs is reattached or
245cleaned up
246([#1133](https://github.com/alchemy-run/alchemy/pull/1133)).
247
248## Also in this release
249
250- **Cloudflare tooling moves into the monorepo**
251 ([#1122](https://github.com/alchemy-run/alchemy/pull/1122)) —
252 the runtime and framework adapters are now
253 `@alchemy.run/cloudflare-runtime` and
254 `@alchemy.run/cloudflare-frameworks`, developed, tested, and
255 released with alchemy itself. Thanks
256 [Rahul Mishra](https://github.com/BlankParticle)!
257- **Queue consumers work on `Website.Vite` workers in dev**
258 ([#1113](https://github.com/alchemy-run/alchemy/pull/1113)) —
259 consumer wiring survives the Vite child's boot order, and
260 `send()` to a queue with no running consumer resolves instead
261 of hanging forever.
262- **Local queue consumers honor batch settings**
263 ([#1126](https://github.com/alchemy-run/alchemy/pull/1126)) —
264 `batchSize`/`maxWaitTimeMs` now map onto the local broker's
265 field names instead of being silently ignored.
266- **Asset serving fixes**
267 ([#1121](https://github.com/alchemy-run/alchemy/pull/1121)) —
268 the asset MIME table covers the full extension set (avif, webp,
269 woff2, … previously `application/octet-stream`), and asset
270 hashes include the extension so identical bodies under
271 different extensions keep distinct content types. One-time
272 effect: all assets re-upload on your next deploy.
273- **Docker containers receive their environment values**
274 ([#1118](https://github.com/alchemy-run/alchemy/pull/1118)) —
275 `environment` entries resolved empty inside the container,
276 breaking e.g. PostgreSQL bootstrap.
277- **InternetGateway detach rides out lingering ENIs**
278 ([#1125](https://github.com/alchemy-run/alchemy/pull/1125)) —
279 Fargate and Lambda ENIs can hold public IPs for 5–20 minutes
280 after their owner is deleted; the detach now waits them out
281 instead of leaving an IGW+VPC residue on destroy.
282- **Auto-generated names avoid reserved prefixes**
283 ([#1186](https://github.com/alchemy-run/alchemy/pull/1186)) —
284 stacks named `aws-*` produced physical names that Resource
285 Groups, S3 Tables, and S3 Vectors reject; colliding prefixes
286 are now escaped, and non-colliding names are byte-for-byte
287 unchanged.
288- **Dates persist correctly in state**
289 ([#1164](https://github.com/alchemy-run/alchemy/pull/1164)) —
290 a `Date`-typed prop used to serialize as `{}` in durable state
291 stores, churning a phantom update on every subsequent plan.
292
293## Where to go next
294
295- [Better Auth](/better-auth)
296- [SQL & Drizzle](/sql)
297- [Local development](/environments/local-development)
298- [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md#v200-beta71)
299- [Compare v2.0.0-beta.70 → v2.0.0-beta.71](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.70...v2.0.0-beta.71)