Multiple Stacks
Each package owns its own alchemy.run.ts
(examples/monorepo-multi-stack):
.├── package.json # workspaces: ["frontend", "backend"]├── backend/│ ├── alchemy.run.ts # Backend Stack — deploys the Worker│ ├── package.json # name: "backend"│ └── src/│ ├── Service.ts # the Worker│ └── Stack.ts # typed handle — outputs { url: string }└── frontend/ ├── alchemy.run.ts # Frontend Stack — references Backend ├── package.json # depends on "backend": "workspace:*" └── src/ └── main.tsx # reads import.meta.env.VITE_API_URLThere is no shared plan and no live connection between the Stacks. The backend deploys and persists its outputs to the state store; the frontend’s plan reads those persisted outputs back through a typed Stack handle. That one indirection is what buys independent deploys, independent destroys, and independent CI — at the cost of a deploy order.
Three small files make it work.
The typed Stack handle
Section titled “The typed Stack handle”import * as Alchemy from "alchemy";
export class Backend extends Alchemy.Stack< Backend, { url: string; }>()("Backend") {}The handle is the contract between the two Stacks: it names the Stack
("Backend") and declares its output shape ({ url: string }). Both sides
import this class — the backend to deploy against it, the frontend to
reference it — so TypeScript enforces the contract in both directions.
The backend Stack
Section titled “The backend Stack”import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import Service from "./src/Service.ts";import { Backend } from "./src/Stack.ts";
export default Backend.make( { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const api = yield* Service; return { url: api.url.as<string>(), }; }),);Backend.make is Alchemy.Stack bound to the handle: if the returned
object doesn’t match { url: string }, this file fails to typecheck. On
deploy, the resolved url is persisted to the state store as the Stack’s
output.
The frontend Stack
Section titled “The frontend Stack”import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import { Backend } from "backend";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "Frontend", { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const backend = yield* Backend;
const website = yield* Cloudflare.Website.Vite("Website", { env: { VITE_API_URL: backend.url, }, });
return { url: website.url.as<string>(), }; }),);yield* Backend reads the backend’s persisted outputs from the state store
at plan time and returns them fully typed — backend.url is the string
the handle declared. It resolves the same stage the frontend is being
deployed to: the sam frontend reads the sam backend, pr-42 reads
pr-42.
The import works because frontend/package.json depends on
"backend": "workspace:*" — the handle travels through the workspace like
any other export.
Deploy in order
Section titled “Deploy in order”cd backend && alchemy deploy --stage samcd frontend && alchemy deploy --stage samThe backend must be deployed to the stage first — if it isn’t, the
frontend’s plan fails with InvalidReferenceError because there are no
persisted outputs to read. Destroy in reverse: frontend first, then backend.
Each alchemy.run.ts runs from its own package directory, so relative paths
(main, rootDir) resolve against the package — the
root-relative path trap
of the single-stack layout doesn’t apply here.
Pin to a specific stage
Section titled “Pin to a specific stage”The bare yield* Backend (same stage on both sides) is the right default.
To break stage symmetry — for example, every frontend preview points at the
production backend — pin with Backend.stage.<name>:
Effect.gen(function* () { const backend = yield* Backend; const backend = yield* Backend.stage.prod; // ...})Backend.stage is a proxy keyed by stage name — any string works
(Backend.stage.staging, Backend.stage["pr-42"]).
Where next
Section titled “Where next”- Monorepo — the high-level chooser.
- Single Stack — the one-Stack alternative: one plan, one state file, paths anchored to the root.
- References — when to split Stacks,
how a reference resolves (typing, plan-time state reads,
InvalidReferenceError), and stage pinning in depth.