1---
2title: 2.0.0-beta.43 - PlanetScale Provider
3date: 2026-05-21T22:00:00Z
4category: release
5excerpt: v2.0.0-beta.43 includes a new `alchemy/Planetscale` provider, improvements to how Resources and Bindings are encapsulated in Layers, and a handful of other fixes.
6---
7
8`v2.0.0-beta.43` includes a new `alchemy/Planetscale` provider,
9improvements to how Resources and Bindings are encapsulated in
10Layers, and a handful of other fixes.
11
12The headline is **PlanetScale** — typed `MySQLDatabase` and
13`PostgresDatabase` resources, branches, credentials, and a
14Drizzle integration that plugs straight into Cloudflare
15Hyperdrive, the same way every other Alchemy provider plugs
16into the rest of the graph.
17([#113](https://github.com/alchemy-run/alchemy/pull/113))
18
19We'll walk it end-to-end against Postgres. The MySQL deltas are
20noted along the way — the shape is one-for-one.
21
22| MySQL | Postgres |
23| --------------------------- | ------------------------------ |
24| `Planetscale.MySQLDatabase` | `Planetscale.PostgresDatabase` |
25| `Planetscale.MySQLBranch` | `Planetscale.PostgresBranch` |
26| `Planetscale.MySQLPassword` | `Planetscale.PostgresRole` |
27
28## Register the provider
29
30Add `Planetscale.providers()` to your stack. This registers the
31deploy-time policy bindings and an auth step that, on next
32`bun alchemy login`, either reads `PLANETSCALE_API_TOKEN_ID`,
33`PLANETSCALE_API_TOKEN`, and `PLANETSCALE_ORGANIZATION` from the
34environment or stores them under
35`~/.alchemy/credentials/<profile>/planetscale-stored.json`.
36
37```diff lang="typescript"
38 // alchemy.run.ts
39 import * as Alchemy from "alchemy";
40 import * as Cloudflare from "alchemy/Cloudflare";
41+import * as Planetscale from "alchemy/Planetscale";
42+import * as Layer from "effect/Layer";
43
44 export default Alchemy.Stack("App", {
45- providers: Cloudflare.providers(),
46+ providers: Layer.mergeAll(
47+ Cloudflare.providers(),
48+ Planetscale.providers(),
49+ ),
50 state: Alchemy.localState(),
51 }, /* ... */);
52```
53
54## Cluster → branch → role
55
56The three database resources mirror PlanetScale's own model: the
57**cluster** owns storage and region, **branches** are cheap
58forks per environment, and **roles** mint credentials against a
59branch.
60
61```typescript
62// src/Db.ts
63import * as Planetscale from "alchemy/Planetscale";
64import * as Effect from "effect/Effect";
65
66export const Db = Effect.gen(function* () {
67 const database = yield* Planetscale.PostgresDatabase("app-db", {
68 region: { slug: "us-east" },
69 clusterSize: "PS_10",
70 });
71
72 const branch = yield* Planetscale.PostgresBranch("app-branch", {
73 database,
74 });
75
76 const role = yield* Planetscale.PostgresRole("app-role", {
77 database,
78 branch,
79 inheritedRoles: ["postgres"],
80 });
81
82 return { database, branch, role };
83});
84```
85
86## Snap Hyperdrive on top
87
88Because `role.origin` is an `Output`, the deploy graph orders
89itself: `PostgresDatabase` → `PostgresBranch` → `PostgresRole` →
90`Hyperdrive` → `Worker`. No `dependsOn`, no `await` — the graph
91follows the dataflow.
92
93```typescript
94import * as Cloudflare from "alchemy/Cloudflare";
95
96export const Hyperdrive = Effect.gen(function* () {
97 const { role } = yield* Db;
98 return yield* Cloudflare.Hyperdrive("app-hyperdrive", {
99 origin: role.origin,
100 });
101});
102```
103
104For MySQL, pass `password.origin`; Hyperdrive sees
105`scheme: "mysql"` and provisions a MySQL pooler.
106
107## Drizzle on Hyperdrive
108
109`Cloudflare.Hyperdrive.bind(Hyperdrive)` returns a binding whose
110`connectionString` resolves at runtime to a `Redacted` pooled
111endpoint. The new `Drizzle.postgres` helper takes that
112connection string and gives you an Effect-native Drizzle client
113— queries are `yield*`-able and errors land on the Effect error
114channel.
115
116```diff lang="typescript"
117 // src/Api.ts
118 import * as Cloudflare from "alchemy/Cloudflare";
119+import * as Drizzle from "alchemy/Drizzle";
120 import * as Effect from "effect/Effect";
121+import { relations, Users } from "./schema.ts";
122 import { Hyperdrive } from "./Db.ts";
123
124 export default class Api extends Cloudflare.Worker<Api>()(
125 "Api",
126 { main: import.meta.filename },
127 Effect.gen(function* () {
128 const conn = yield* Cloudflare.Hyperdrive.bind(Hyperdrive);
129+ const db = yield* Drizzle.postgres(conn.connectionString, { relations });
130
131 return {
132 fetch: Effect.gen(function* () {
133+ const users = yield* db.select().from(Users);
134 return yield* HttpServerResponse.json({ users });
135 }),
136 };
137 }).pipe(Effect.provide(Cloudflare.HyperdriveBindingLive)),
138 ) {}
139```
140
141(For MySQL there's no `Drizzle.mysql` helper yet — pair with
142`mysql2` directly inside `fetch`.)
143
144## Migrations as a resource
145
146`Drizzle.Schema` wraps drizzle-kit's programmatic API as an
147Alchemy resource. On each deploy it diffs `./src/schema.ts`
148against the latest snapshot and, if it drifted, writes a new
149migration directory. Wire its `out` into the branch's
150`migrationsDir` and the branch scans the directory and applies
151new SQL files transactionally — schema → migration files →
152applied migrations, in one deploy.
153
154```diff lang="typescript"
155 export const Db = Effect.gen(function* () {
156+ const schema = yield* Drizzle.Schema("app-schema", {
157+ schema: "./src/schema.ts",
158+ out: "./migrations",
159+ });
160
161 const database = yield* Planetscale.PostgresDatabase("app-db", { /* ... */ });
162
163 const branch = yield* Planetscale.PostgresBranch("app-branch", {
164 database,
165+ migrationsDir: schema.out,
166 });
167 // role / hyperdrive unchanged
168 });
169```
170
171Don't forget to add `Drizzle.providers()` alongside
172`Planetscale.providers()` in the stack.
173
174## `.ref()` — fork from a centralised staging database
175
176The walkthrough so far creates a fresh `PostgresDatabase` for
177every stage. That's right for `dev_<user>` and `prod`, but wrong
178for PR previews — spinning up a new Postgres cluster per PR is
179slow, expensive, and out of step with how PlanetScale itself
180thinks about branching.
181
182beta.43 ships `Resource.ref()` on every resource type. It reads
183output attributes from another stage's state instead of
184provisioning, returns a handle of the same type, and is
185indistinguishable from the owned resource to anything
186downstream. Combined with a two-tier stage layout —
187`staging-*` owns long-lived databases, `pr-*` owns ephemeral
188compute — the fork point collapses into one ternary:
189
190```diff lang="typescript"
191 export const Db = Effect.gen(function* () {
192+ const { stage } = yield* Alchemy.Stack;
193
194- const database = yield* Planetscale.PostgresDatabase("app-db", {
195- region: { slug: "us-east" },
196- clusterSize: "PS_10",
197- });
198+ const database = stage.startsWith("pr-")
199+ ? yield* Planetscale.PostgresDatabase.ref("app-db", {
200+ stage: `staging-${stage}`,
201+ })
202+ : yield* Planetscale.PostgresDatabase("app-db", {
203+ region: { slug: "us-east" },
204+ clusterSize: "PS_10",
205+ });
206 // branch / role / hyperdrive stay the same
207 });
208```
209
210When `pr-123` deploys, `staging-pr-123` has already run and
211owns the cluster; `pr-123` references it and provisions only
212its own branch, role, Hyperdrive, and Worker. Tear-down on PR
213close drops the per-PR resources without touching the cluster.
214Same code, three deployment topologies.
215
216## Also in this release
217
218A smaller but load-bearing change: **Infrastructure Layers no
219longer leak `WorkerEnvironment`**. Every binding API method
220(`kv.get`, `db.prepare(...).first()`, `Hyperdrive.bind`) used
221to return an `Effect` whose `R` channel carried
222`WorkerEnvironment`, which propagated up through every Layer
223that wrapped it. `WorkerEnvironment` is now resolved once
224inside each `*BindingLive` Layer and closed over, leaving only
225`Alchemy.RuntimeContext` — the cloud-agnostic color for "runs
226inside a deployed Function or Worker" — in the requirement
227channel.
228
229```diff lang="typescript"
230-kv.get(key, "json"): Effect.Effect<Job | null, NamespaceError, WorkerEnvironment>
231+kv.get(key, "json"): Effect.Effect<Job | null, NamespaceError, RuntimeContext>
232```
233
234A `JobService` wrapping KV can now expose
235`Effect<Job, _, RuntimeContext>` to consumers without
236mentioning Cloudflare at all. See
237[Concepts › Layers](/infrastructure-as-effects/layers) for
238the underlying model.
239([#383](https://github.com/alchemy-run/alchemy/pull/383),
240[#386](https://github.com/alchemy-run/alchemy/pull/386))
241
242## Where to go next
243
244- [Hyperdrive tutorial](/cloudflare/data/hyperdrive) — PlanetScale Postgres and MySQL tabs alongside Neon
245- [Drizzle tutorial](/cloudflare/data/drizzle) — `Drizzle.Schema` and deploy-driven migrations
246- [Branch from a shared database](/cloudflare/data/branch-from-shared-database) — `.ref()` as a guided tutorial
247- [PlanetScale Postgres + Drizzle example](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-planetscale-postgres-drizzle)
248- [PlanetScale MySQL + Drizzle example](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-planetscale-mysql-drizzle)
249- [CHANGELOG](https://github.com/alchemy-run/alchemy/blob/main/CHANGELOG.md) · [v2.0.0-beta.42 → v2.0.0-beta.43](https://github.com/alchemy-run/alchemy/compare/v2.0.0-beta.42...v2.0.0-beta.43)