Skip to content

Stacks

A Stack is the top-level unit of deployment in Alchemy. It groups resources together, wires up providers, and tracks state across deploys.

A Stack is just an Effect that you export default from a TypeScript file:

import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"MyApp",
{ providers: Cloudflare.providers(), state: Cloudflare.state() },
Effect.gen(function* () {
const bucket = yield* Cloudflare.R2.Bucket("Bucket");
return { bucketName: bucket.bucketName };
}),
);

Alchemy.Stack takes three arguments:

  1. Name — identifies this stack in state storage
  2. Optionsproviders and state, both required (see State Store for the available state layers)
  3. Effect — a generator that declares resources and returns outputs

A stack is not limited to one cloud. Providers are Effect Layers, so to deploy resources from several providers in a single stack, merge their layers with Layer.mergeAll:

import * as Alchemy from "alchemy";
import * as AWS from "alchemy/AWS";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Neon from "alchemy/Neon";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
export default Alchemy.Stack(
"MyApp",
{
providers: Layer.mergeAll(
Cloudflare.providers(),
AWS.providers(),
Neon.providers(),
),
state: Cloudflare.state(),
},
Effect.gen(function* () {
const db = yield* Neon.Project("Db");
const queue = yield* AWS.SQS.Queue("Jobs");
const bucket = yield* Cloudflare.R2.Bucket("Uploads");
return { projectId: db.projectId };
}),
);

The type system tracks which providers the stack’s resources need — declaring an AWS resource without AWS.providers() in the merged layer is a compile-time error.

state is independent of providers: pick one state layer no matter how many clouds the stack deploys to.

See Providers for more on provider layers.

The value returned from the generator becomes the stack output. After a deploy, outputs are printed to the console and available programmatically in tests.

Effect.gen(function* () {
const bucket = yield* Cloudflare.R2.Bucket("Bucket");
const worker = yield* Worker;
return {
bucketName: bucket.bucketName,
url: worker.url,
};
});

The CLI renders them after every successful deploy:

alchemy · dev_samDEPLOY
$

Every deploy targets a stage — an isolated instance of the stack like dev_sam, staging, or prod. The stage defaults to dev_$USER, so each developer gets their own environment automatically. State and physical names are namespaced by stage.

Terminal window
# Each stage = its own state file + its own physical names.
$ alchemy deploy --stage dev_sam # -> myapp-dev_sam-photos-a3f1
$ alchemy deploy --stage pr-147 # -> myapp-pr_147-photos-9b2c
$ alchemy deploy --stage prod # -> myapp-prod-photos-7d4e
# Three independent deployments. Destroying one
# never touches the others.

For naming patterns, isolation, and per-stage configuration see Stages. Credentials per environment are managed via Profiles.

Inside a resource or layer, you can access the current Stack’s metadata via the Stack service:

import { Stack } from "alchemy/Stack";
import * as Console from "effect/Console";
Effect.gen(function* () {
const stack = yield* Stack;
yield* Console.log(stack.name); // "MyApp"
yield* Console.log(stack.stage); // "dev_sam"
const queue = yield* SQS.Queue("Jobs").pipe(
RemovalPolicy.retain(stack.stage === "prod"),
);
});

When you declare a resource at module scope (outside any Effect.gen), there is no yield* available to read the Stack service. But a Resource’s props can themselves be an Effect — Stack.useSync builds one that computes props from the current stack, useful for parameterizing names by stage:

import * as Axiom from "alchemy/Axiom";
import { Stack } from "alchemy/Stack";
export const Traces = Axiom.Dataset(
"Traces",
Stack.useSync(({ stage }) => ({
name: `${stage}-traces`,
kind: "otel:traces:v1" as const,
description: `OTEL traces for stage '${stage}'`,
retentionDays: 30,
})),
);

The function runs once at plan time, after the stage is resolved. Use it anywhere a resource accepts a props object.

A typed Alchemy.Stack handle lets one stack read another’s persisted outputs at plan time:

// backend/src/Stack.ts — typed handle shared across packages
export class Backend extends Alchemy.Stack<
Backend,
{ url: string }
>()("Backend") {}
// frontend/alchemy.run.ts — reads the matching stage of Backend
import { Backend } from "backend";
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 defaults to the current stagesam frontend reads sam backend, pr-42 reads pr-42. Use Backend.stage.<name> to pin to a specific stage instead.

See References for the underlying operators and Monorepo for the end-to-end walkthrough (package layout, schema sharing, deploy ordering).

A stack is just the container — the things it deploys are Resources.

  • Resources — the named entities a stack deploys.
  • Stages — naming, isolation, per-stage configuration.
  • Monorepo — one stack or many across packages.