Skip to content

Single Stack

One alchemy.run.ts at the workspace root deploys every app in the monorepo (examples/monorepo-single-stack):

Terminal window
.
├── 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.tsx

The entire Stack:

alchemy.run.ts
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 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.

Each app declares its own Worker in its own package:

backend/src/Service.ts
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.

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.

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:

frontend/src/main.tsx
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.

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.

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:

apps/api/Api.ts
import * as Cloudflare from "alchemy/Cloudflare";
export const Api = Cloudflare.Worker("Api", {
main: new URL("./src/worker.ts", import.meta.url).href,
});
alchemy.run.ts
const api = yield* Api;
Terminal window
alchemy deploy

One plan, one apply, one state file. Both apps go up together; both come down together with alchemy destroy.

  • 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 HttpApi schema and client between the Worker and the frontend.