| 1 | --- |
| 2 | title: Shared database across stages |
| 3 | description: Have ephemeral PR-preview stages reference a long-lived Neon or PlanetScale database from a staging stage instead of provisioning their own — fast previews, copy-on-write branches, no extra clusters. |
| 4 | --- |
| 5 | |
| 6 | [Stages](/environments/stages) make it cheap to spin up isolated copies |
| 7 | of a stack — per-developer, `pr-42`, `prod`. Most resources should |
| 8 | be isolated. But some — a Neon Postgres project, a shared S3 |
| 9 | bucket, a global rate limiter — are too expensive or too stateful |
| 10 | to re-provision per stage. PR-preview stages should *point at* the |
| 11 | shared instance instead. |
| 12 | |
| 13 | `Resource.ref(id, { stage })` reads a deployed resource's |
| 14 | attributes from another stage of the same stack — typed, lazy, |
| 15 | resolved at plan time against the persisted state store. See |
| 16 | [References](/infrastructure-as-code/references) for the full reference surface |
| 17 | (`Output.ref`, `Output.stackRef`, `Stack.stage`). |
| 18 | |
| 19 | The example we'll build: a Neon Postgres project that's owned by |
| 20 | `staging`, with PR-preview stages (`pr-*`) referencing it instead |
| 21 | of creating their own. The full file lives in |
| 22 | [`examples/cloudflare-neon-drizzle/src/Db.ts`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-neon-drizzle/src/Db.ts). |
| 23 | |
| 24 | ## Start with the unconditional version |
| 25 | |
| 26 | A Neon project plus a per-stage branch. Every stage gets its own |
| 27 | project — wasteful for short-lived PR previews: |
| 28 | |
| 29 | ```typescript |
| 30 | // src/Db.ts |
| 31 | import * as Alchemy from "alchemy"; |
| 32 | import * as Drizzle from "alchemy/Drizzle"; |
| 33 | import * as Neon from "alchemy/Neon"; |
| 34 | import * as Effect from "effect/Effect"; |
| 35 | |
| 36 | export const NeonDb = Effect.gen(function* () { |
| 37 | const schema = yield* Drizzle.Schema("app-schema", { |
| 38 | schema: "./src/schema.ts", |
| 39 | out: "./migrations", |
| 40 | }); |
| 41 | |
| 42 | const project = yield* Neon.Project("app-db", { |
| 43 | region: "aws-us-east-1", |
| 44 | }); |
| 45 | |
| 46 | const branch = yield* Neon.Branch("app-branch", { |
| 47 | project, |
| 48 | migrations: schema, |
| 49 | }); |
| 50 | |
| 51 | return { project, branch, schema }; |
| 52 | }); |
| 53 | ``` |
| 54 | |
| 55 | Each stage that runs this gets: |
| 56 | |
| 57 | - its own `Neon.Project` (a whole Postgres cluster), |
| 58 | - its own `Neon.Branch` against that project, |
| 59 | - migrations re-applied from scratch. |
| 60 | |
| 61 | Fine for `staging` and `prod`. Overkill for `pr-147`. |
| 62 | |
| 63 | ## Read the current stage |
| 64 | |
| 65 | Pull the active stage from the `Stack` service so we can branch on |
| 66 | it. `Alchemy.Stack` is the same handle used everywhere — it gives |
| 67 | you `name`, `stage`, and friends: |
| 68 | |
| 69 | ```diff lang="typescript" |
| 70 | export const NeonDb = Effect.gen(function* () { |
| 71 | + const { stage } = yield* Alchemy.Stack; |
| 72 | + |
| 73 | const schema = yield* Drizzle.Schema("app-schema", { |
| 74 | schema: "./src/schema.ts", |
| 75 | out: "./migrations", |
| 76 | }); |
| 77 | ``` |
| 78 | |
| 79 | `stage` is whatever was passed to `alchemy deploy --stage <name>`. |
| 80 | We'll use it to route PR previews to the shared project. |
| 81 | |
| 82 | ## Swap in `Neon.Project.ref` for PR stages |
| 83 | |
| 84 | Here's the key step. Replace the unconditional `Neon.Project(...)` |
| 85 | with a conditional that uses `Neon.Project.ref(...)` when the stage |
| 86 | is a PR preview, and creates a real project otherwise: |
| 87 | |
| 88 | ```diff lang="typescript" |
| 89 | - const project = yield* Neon.Project("app-db", { |
| 90 | - region: "aws-us-east-1", |
| 91 | - }); |
| 92 | + const project = stage.startsWith("pr-") |
| 93 | + ? yield* Neon.Project.ref("app-db", { stage: "staging" }) |
| 94 | + : yield* Neon.Project("app-db", { |
| 95 | + region: "aws-us-east-1", |
| 96 | + }); |
| 97 | ``` |
| 98 | |
| 99 | Three things to know about `Resource.ref`: |
| 100 | |
| 101 | 1. **Same logical ID.** `"app-db"` — the same logical ID the |
| 102 | `staging` stage uses to create the project. The ref lookup is |
| 103 | keyed by `{ stack, stage, id }`. |
| 104 | 2. **Typed.** `project` is `Neon.Project` either way — same |
| 105 | attributes, same downstream API. The branch resource doesn't |
| 106 | know or care that one stage's `project` is real and another's |
| 107 | is a reference. |
| 108 | 3. **Resolved at plan time.** Alchemy reads the attributes (project |
| 109 | id, host, etc.) from the staging stage's state file. If |
| 110 | `staging` hasn't been deployed yet, plan fails with |
| 111 | `InvalidReferenceError` — deploy `staging` first, then PR |
| 112 | stages can reference it. |
| 113 | |
| 114 | The signature is |
| 115 | `Resource.ref(id, { stage?: string, stack?: string })`. Both |
| 116 | options default to the current stack and stage; here we override |
| 117 | `stage` to point at the long-lived shared environment. |
| 118 | |
| 119 | ## Pass the reference downstream |
| 120 | |
| 121 | Once `project` exists (real or referenced), the rest of the file |
| 122 | is identical. The branch creates per-stage: |
| 123 | |
| 124 | ```typescript |
| 125 | const branch = yield* Neon.Branch("app-branch", { |
| 126 | project, |
| 127 | migrations: schema, |
| 128 | }); |
| 129 | ``` |
| 130 | |
| 131 | Each PR stage gets its own ephemeral Neon **branch** off the |
| 132 | shared **project** — which is exactly the point. Branches are |
| 133 | copy-on-write and free; projects aren't. |
| 134 | |
| 135 | ## The whole picture |
| 136 | |
| 137 | ```typescript |
| 138 | // src/Db.ts |
| 139 | import * as Alchemy from "alchemy"; |
| 140 | import * as Drizzle from "alchemy/Drizzle"; |
| 141 | import * as Neon from "alchemy/Neon"; |
| 142 | import * as Effect from "effect/Effect"; |
| 143 | |
| 144 | export const NeonDb = Effect.gen(function* () { |
| 145 | const { stage } = yield* Alchemy.Stack; |
| 146 | |
| 147 | const schema = yield* Drizzle.Schema("app-schema", { |
| 148 | schema: "./src/schema.ts", |
| 149 | out: "./migrations", |
| 150 | }); |
| 151 | |
| 152 | const project = stage.startsWith("pr-") |
| 153 | ? yield* Neon.Project.ref("app-db", { stage: "staging" }) |
| 154 | : yield* Neon.Project("app-db", { |
| 155 | region: "aws-us-east-1", |
| 156 | }); |
| 157 | |
| 158 | const branch = yield* Neon.Branch("app-branch", { |
| 159 | project, |
| 160 | migrations: schema, |
| 161 | }); |
| 162 | |
| 163 | return { project, branch, schema }; |
| 164 | }); |
| 165 | ``` |
| 166 | |
| 167 | ## Deploy |
| 168 | |
| 169 | Deploy `staging` once to materialize the shared project: |
| 170 | |
| 171 | ```sh |
| 172 | alchemy deploy --stage staging |
| 173 | ``` |
| 174 | |
| 175 | Then PR stages can reference it: |
| 176 | |
| 177 | ```sh |
| 178 | alchemy deploy --stage pr-147 |
| 179 | ``` |
| 180 | |
| 181 | Plan reads `staging`'s state, resolves `app-db` against it, and |
| 182 | provisions only the per-stage `Neon.Branch`. Tearing down a PR |
| 183 | stage with `alchemy destroy --stage pr-147` deletes the branch but |
| 184 | leaves the shared project alone — Alchemy doesn't own it from this |
| 185 | stage's perspective. |
| 186 | |
| 187 | ## The same pattern with PlanetScale |
| 188 | |
| 189 | `Resource.ref` isn't Neon-specific. The |
| 190 | [PlanetScale](/planetscale) examples use the identical shape — a |
| 191 | long-lived `PostgresDatabase` owned by a staging tier, referenced |
| 192 | from `pr-*` stages that only own the ephemeral pieces (branch, |
| 193 | role, Hyperdrive, Worker): |
| 194 | |
| 195 | ```typescript |
| 196 | const database = stage.startsWith("pr-") |
| 197 | ? yield* Planetscale.PostgresDatabase.ref("app-db", { |
| 198 | stage: `staging-${stage}`, |
| 199 | }) |
| 200 | : yield* Planetscale.PostgresDatabase("app-db", { |
| 201 | region: { slug: "us-east" }, |
| 202 | clusterSize: "PS_10", |
| 203 | }); |
| 204 | |
| 205 | const branch = yield* Planetscale.PostgresBranch("app-branch", { |
| 206 | database, |
| 207 | migrations: schema, |
| 208 | }); |
| 209 | ``` |
| 210 | |
| 211 | One twist worth stealing: instead of pointing every PR at a single |
| 212 | global `staging`, the ref targets `staging-${stage}` — so `pr-42` |
| 213 | reads from `staging-pr-42`. Each PR gets a parallel staging stage |
| 214 | that owns its database, keeping PR data isolated while the |
| 215 | create-vs-reference split stays the same. Deploy `staging-pr-42` |
| 216 | before `pr-42`, same as the Neon flow. If you'd rather share one |
| 217 | database across all PRs, use `stage: "staging"` exactly as above. |
| 218 | |
| 219 | The MySQL family works identically — `MySQLDatabase.ref`, |
| 220 | `MySQLBranch`, `MySQLPassword`. Full files: |
| 221 | [`cloudflare-planetscale-postgres-drizzle`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-planetscale-postgres-drizzle/src/Db.ts) |
| 222 | and |
| 223 | [`cloudflare-planetscale-mysql-drizzle`](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-planetscale-mysql-drizzle/src/Db.ts). |
| 224 | |
| 225 | ## Cross-stack too |
| 226 | |
| 227 | `Resource.ref` also takes a `stack` option for pulling a resource |
| 228 | out of a *different* stack: |
| 229 | |
| 230 | ```typescript |
| 231 | const project = yield* Neon.Project.ref("app-db", { |
| 232 | stack: "shared-infra", |
| 233 | stage: "prod", |
| 234 | }); |
| 235 | ``` |
| 236 | |
| 237 | The lookup is `{ stack, stage, id }` — change any of them and |
| 238 | you're pointing at a different deployed resource. For pulling a |
| 239 | whole stack's outputs (instead of a single resource) see the |
| 240 | multi-stack section of [Monorepos](/project-structure/monorepo). |
| 241 | |
| 242 | ## When to use which |
| 243 | |
| 244 | | Goal | Use | |
| 245 | | ------------------------------------------- | ---------------------------------------------- | |
| 246 | | Reference a single resource, same stack, different stage | `Resource.ref(id, { stage })` | |
| 247 | | Reference a single resource, different stack | `Resource.ref(id, { stack, stage })` | |
| 248 | | Reference an entire stack's outputs | `Backend.stage[name]` ([guide](/project-structure/monorepo)) | |
| 249 | | Reference an arbitrary deployed Output | [`Output.ref`](/infrastructure-as-code/outputs#ref) | |
| 250 | |
| 251 | All four resolve through the same state store — they just differ |
| 252 | in what shape they return. |
| 253 | |
| 254 | ## Related |
| 255 | |
| 256 | - [Resource](/infrastructure-as-code/resource#ref) — `Resource.ref` reference. |
| 257 | - [Stages](/environments/stages) — naming conventions for `pr-*`, |
| 258 | `dev_*`, `staging`, `prod`. |
| 259 | - [Monorepos](/project-structure/monorepo) — whole-stack references via |
| 260 | typed `Stack` handles. |
| 261 | - [Neon](/neon) and [PlanetScale](/planetscale) — the two database |
| 262 | providers used in this guide. |