Single Stack
One alchemy.run.ts at the workspace root deploys every app in the monorepo
(examples/monorepo-single-stack):
.├── package.json # workspaces: ["frontend", "backend"]├── alchemy.run.ts # the one Stack — deploys both apps├── backend/│ └── src/│ └── Service.ts # the Worker — this file is its own entry└── frontend/ ├── index.html # Vite app └── src/ └── main.tsxThe entire Stack:
import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import { Path } from "effect/Path";import Service from "./backend/src/Service.ts";
export default Alchemy.Stack( "Monorepo", { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const backend = yield* Service; const path = yield* Path;
const website = yield* Cloudflare.Website.Vite("Website", { rootDir: path.resolve(import.meta.dirname, "frontend"), env: { VITE_API_URL: backend.url.as<string>(), }, });
return { backendUrl: backend.url.as<string>(), websiteUrl: website.url.as<string>(), }; }),);Run alchemy deploy (or alchemy dev) from the workspace root and both apps
go up in one plan, tracked in one state file. The rest of this page explains
the two lines that make the monorepo shape work: main: import.meta.url and
rootDir.
Anchor every path
Section titled “Anchor every path”Anchor every path to the file that declares it, using import.meta:
| Declaring | Use |
|---|---|
| a Worker whose defining file is its entry | main: import.meta.url |
| a Worker with a separate entry file | main: new URL("./src/worker.ts", import.meta.url).href |
| a Vite website in a subdirectory | rootDir: path.resolve(import.meta.dirname, "frontend") |
| a custom Worker entry inside a Vite app | vite: { main: "src/worker.ts" } — resolves from rootDir |
import.meta.url and import.meta.dirname always point at the file being
evaluated, so the resource resolves the same way no matter which directory
alchemy runs from.
The backend Worker
Section titled “The backend Worker”Each app declares its own Worker in its own package:
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
export default class Service extends Cloudflare.Worker<Service>()( "Service", { main: import.meta.url }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("Hello from the backend")), }; }),) {}main: import.meta.url makes this file its own entry — a file URL, absolute
by construction, so the root alchemy.run.ts can import Service from
anywhere and bundling still finds the right module.
The example serves a typed HttpApi instead of a plain fetch handler and
shares its typed client with the frontend — see
Effect HTTP API for that pattern.
The Vite website
Section titled “The Vite website”const website = yield* Cloudflare.Website.Vite("Website", { rootDir: path.resolve(import.meta.dirname, "frontend"),});rootDir is Vite’s root — where index.html, vite.config.ts, and the
app source live. It defaults to process.cwd(), which in a monorepo is the
workspace root, so point it at the app’s directory explicitly. Anchoring with
import.meta.dirname keeps it correct regardless of where alchemy runs.
Wire the Worker’s URL into the frontend
Section titled “Wire the Worker’s URL into the frontend”const website = yield* Cloudflare.Website.Vite("Website", { rootDir: path.resolve(import.meta.dirname, "frontend"), env: { VITE_API_URL: backend.url.as<string>(), },});backend.url is an Output<string> — passing it into env makes Alchemy
deploy the Worker first, then build the website with the resolved URL baked
in. The frontend reads it like any other Vite env var:
const API_URL = import.meta.env.VITE_API_URL;This is the payoff of the single-stack shape: the frontend consumes the backend’s URL directly off the in-memory Output — no cross-stack references, no deploy ordering to manage.
Custom Worker entry inside the Vite app
Section titled “Custom Worker entry inside the Vite app”By default Website.Vite deploys the server entry your framework produces.
If the app ships its own Worker module (extra handlers, Durable Objects),
point vite.main at it:
const website = yield* Cloudflare.Website.Vite("Website", { rootDir: path.resolve(import.meta.dirname, "frontend"), vite: { main: "src/worker.ts", },});vite.main is the one relative path that does not resolve from the
workspace root — it resolves from rootDir, so src/worker.ts means
frontend/src/worker.ts.
Add more apps
Section titled “Add more apps”The pattern scales by repetition: declare each app’s Worker in its own
package with an anchored main, import them all into the one Stack, and
yield* each one. A Worker whose entry is a separate file anchors it with
new URL:
import * as Cloudflare from "alchemy/Cloudflare";
export const Api = Cloudflare.Worker("Api", { main: new URL("./src/worker.ts", import.meta.url).href,});const api = yield* Api;Deploy
Section titled “Deploy”alchemy deployOne plan, one apply, one state file. Both apps go up together; both come
down together with alchemy destroy.
Where next
Section titled “Where next”- Monorepo — the high-level chooser.
- Multiple Stacks — split the deploy per package when cadences diverge.
- File layout — the single-package layout this builds on.
- Effect HTTP API — share a typed
HttpApischema and client between the Worker and the frontend.