Learn Alchemy
Alchemy 2 declares and deploys cloud resources through Effect programs. Effect describes work with typed failures36 and required dependencies. One program declares resources, bindings23, deployment state, and the Worker that uses them.ΒΆ
A service is a named interface for one job. A Layer builds services and supplies their construction dependencies. A contract defines an actionβs name, input, output, failure, and metadata.ΒΆ
Start with this repoβs Cloudflare resources. Alchemy also supports other providers. Learn the vocabulary to write infrastructure and check planned changes.ΒΆ
Sam Goodwin7 describes an Effect as a promise with more information: its return value, errors, and requirements. His Alchemy shorthand is βif it compiles, it should deploy and run.β Use compilation for feedback. Check cloud API failures during deployment.ΒΆ
The vocabulary
- Alchemy Stack groups one infrastructure program and its outputs. Rat-stack exports
Alchemy.Stack("RatStack", ...)inapps/infra/alchemy.run.ts.ΒΆ - Provider supplies the cloud API implementation.
Cloudflare.providers()appears in the Stack options inapps/infra/alchemy.run.ts.ΒΆ - State records the resources that a Stack already owns. Rat-stack uses
Cloudflare.state()inapps/infra/alchemy.run.ts.ΒΆ - Resource is a declared cloud object such as a Zone, DNS record, DNSSEC setting, or Worker. Each gets a stable logical name in
apps/infra/alchemy.run.ts.ΒΆ - Binding is a typed runtime value attached to a Worker. Rat-stack declares the
CODE_SANDBOXWorkerLoader and four RateLimit bindings inapps/site/src/worker.ts.ΒΆ - Plan shows differences between declared resources and stored deployment state before a provider changes anything. Read it with the
planscript inapps/infra/package.json.ΒΆ - Stage names an isolated deployment state. Rat-stackβs production stage is
prod; the unflagged default islive_$USER.ΒΆ - Profile holds provider credentials for the Alchemy CLI. This repo uses an Alchemy profile, not environment variables, for Cloudflare authentication.ΒΆ
- Adopt means take ownership of an existing resource explicitly. The existing
ratstack.shZone and its DNSSEC setting useadopt(true)inapps/infra/alchemy.run.ts.ΒΆ - Dev is local workerd execution with
ALCHEMY_DEV=true. Rat-stack uses it to skip the production Zone and DNS resources inapps/infra/alchemy.run.ts.ΒΆ
Trace one deploy
Read apps/infra/alchemy.run.ts from top to bottom.ΒΆ
- The file imports Alchemy, the Cloudflare provider, Effect, and the
MischiefWorker fromapps/site/src/worker.ts.ΒΆ Alchemy.Stackcreates the program.Effect.gengives it a place to yield resources and return outputs.ΒΆCloudflare.providers()supplies Cloudflare operations.Cloudflare.state()lets Alchemy compare this run with prior state.ΒΆALCHEMY_DEVguards resources that should not be touched by local development.ΒΆ- The Zone is named
RatstackZoneand adopted becauseratstack.shalready exists. A Zone normally retains on destroy, and this Stack does not opt into deleting it.ΒΆ - The DNS-AID SVCB and TXT records point agent discovery at the deployed site. They use the adopted Zoneβs
zoneId.ΒΆ Cloudflare.DNS.Dnsseckeeps DNSSEC active. It is adopted for the same reason as the Zone.ΒΆMischiefdeclares the Worker withratstack.shas its custom domain,www.ratstack.shas a redirect, and port 1337 for dev.ΒΆ- The Worker construction yields its WorkerLoader and four RateLimit bindings:
API_PER_IP,EXECUTE_GLOBAL,EXECUTE_PER_IP, andINTEREST_PER_IP.Effect.provide(Cloudflare.Workers.RateLimitBinding)supplies the RateLimit client layer.ΒΆ apps/site/src/worker.tsalso yields the Interest and InterestIndex Durable Objects.EVENTS_ENABLEDchoosesBasinorbasinFoundation: both keep the bucket, stream, and visitor salt; enabled adds the catalog, sink, and pipeline. The Worker supplies the subscriber delivery implementation when it starts, outside core.ΒΆ- The Stack yields
Websitefromapps/web/src/website.tsand returns bothmischiefUrlandwebsiteUrl.ΒΆ
The outputs omit the production DNS declarations:ΒΆ
export default Alchemy.Stack(
"RatStack",
{ providers: Cloudflare.providers(), state: Cloudflare.state() },
Effect.gen(function* stack() {
const mischief = yield* Mischief;
const website = yield* Website;
return { mischiefUrl: mischief.url, websiteUrl: website.url };
})
); The Stack is an Effect. Importing it describes work. The CLI runs it during a plan or deploy.ΒΆ
Follow the Worker bindings
apps/site/src/worker.ts exports class Mischief extends Cloudflare.Worker<Mischief>()(...) {} with main: import.meta.url, so Alchemy bundles the default Worker export. Its construction Effect yields bindings before it returns the fetch implementation. Alchemy populates Cloudflare.WorkerEnvironment dynamically; this repo keeps the expected binding shape in local types at that boundary.ΒΆ
const stageRateLimits = rateLimitDeclarations(yield * Stage);
yield * Cloudflare.WorkerLoader("CODE_SANDBOX");
yield * Cloudflare.RateLimit("API_PER_IP", stageRateLimits.API_PER_IP);
yield * Cloudflare.RateLimit("EXECUTE_GLOBAL", stageRateLimits.EXECUTE_GLOBAL);
yield * Cloudflare.RateLimit("EXECUTE_PER_IP", stageRateLimits.EXECUTE_PER_IP);
yield *
Cloudflare.RateLimit("INTEREST_PER_IP", stageRateLimits.INTEREST_PER_IP);
const environment = yield * Cloudflare.WorkerEnvironment;
// SAFETY: the Worker construction declares every RateLimitBindings member and CODE_SANDBOX before reading this environment.
const bindings = environment as RateLimitBindings & {
readonly CODE_SANDBOX: WorkerLoaderBinding;
}; apps/site/src/rate-limits.ts keeps the four native binding names and their numeric namespace IDs together. rateLimitsFrom calls the native .limit({ key }) method and turns a failed runtime call into a defect instead of failing open.ΒΆ
apps/site/src/sandbox-worker-loader.ts consumes the CODE_SANDBOX binding. It loads a fresh Dynamic Worker with limits and globalOutbound: null, so the code-mode Worker can compute and call declared capabilities without reaching the network. A capability is one named action with shared value definitions and a server-side implementation.ΒΆ
Sam describes the constructor as a Layer: declare dependencies in the Effect, use them, then return the implementation. In his React analogy, dependencies are the hooks at the top. The returned Worker is the component.ΒΆ
Declarations and consumers share one construction Effect. The environment assertion relies on the declared bindings. Check missing bindings in the plan and at runtime; typecheck alone cannot prove they exist.ΒΆ
Plan, then deploy
The scripts live in apps/infra/package.json:ΒΆ
pnpm infra:plan
pnpm infra:deploy
pnpm infra:dev
pnpm infra:destroy Run the plan first. It shows create, update, adopt, and noop actions before a provider is changed. Review replacements, domains, DNS, bindings, and stage before approving a deploy.ΒΆ
Production is stage prod. Pass --stage prod to the Alchemy CLI. An unflagged deploy defaults to live_$USER, which can create a second Worker instead of updating production. Never run an indiscriminate stage destroy. Name the stage explicitly and read its plan first.ΒΆ
Alchemy v2 represents outputs as values whose contents become available during deployment. Resources can still reference one another. Sam compares this to Pulumiβs output model. The engine sees the graph before it acts. The engine orders resource operations without hand-written await chains.ΒΆ
Why not wrangler.toml or Terraform
Wrangler is a good Cloudflare-specific tool. This repo needs one Effect program that declares the Zone, DNS records, DNSSEC, Worker, and the bindings consumed by the Worker, so Alchemy is the chosen boundary.ΒΆ
Terraform would keep infrastructure and the Effect Worker in separate languages and graphs. Alchemy can infer the Worker binding wiring from the same construction code that consumes it, while still showing a plan first.ΒΆ
Keep an existing tool when it already owns the state and the team knows its review path. Choose Alchemy here because the typed application and the cloud graph are one lesson.ΒΆ
Gotchas from this repo
- The Worker bundle uses Alchemyβs Rolldown path, and Nodeβs type stripping is outside the normal TypeScript program. A red
pnpm typecheckdoes not stop Alchemy from producing a deploy plan or bundle. Runpnpm turbo run check test buildfirst and inspect the failing task. Do not use a deploy to bypass a red gate.ΒΆ - Cloudflare API changes have broken Alchemy releases before. Check the pinned version in
apps/infra/package.jsonand the generated pins page before changing it.ΒΆ - The repo pins Alchemy to a beta. Alpha and beta APIs drift. Read the current pins and vendored source before copying an example from elsewhere.ΒΆ
- Cloudflare rejected string rate-limit namespace IDs during a deploy even though the local type allowed them.
apps/site/src/rate-limits.tsuses numeric IDs and documents the boundary.ΒΆ adopt(true)authorizes adoption. Without it, Alchemy refuses to take over an existing Zone. Keep adoption narrow and never assume a resource is safe to destroy.ΒΆ- Profiles carry Cloudflare credentials. Do not add provider tokens to
.env, source files, or Worker bindings for CLI convenience.ΒΆ
Try it
- Run
pnpm infra:planand read every action. Do not approve a deploy. Identify which resources arenoopand which would change.ΒΆ - On a throwaway branch, add one temporary RateLimit binding in
apps/site/src/worker.ts, consume it, and run the Worker typecheck. Remove it after seeing how the declaration and consumer stay aligned. Do not deploy it.ΒΆ - Run
pnpm infra:dev. Alchemy starts the Worker in local workerd, using thedevport fromapps/site/src/worker.ts; theALCHEMY_DEVbranch skips the production Zone and DNS resources.ΒΆ
Read before changing
- Read
AGENTS.mdfor pins, commands, boundaries, and the fence39: checks and hooks that reject prohibited code and shortcuts.ΒΆ - Read
VISION.mdfor why the scaffold keeps infrastructure and application contracts explicit.ΒΆ - Read
apps/infra/alchemy.run.tsfor the Stack and the cloud footprint.ΒΆ - Read
apps/infra/package.jsonfor plan, deploy, dev, and destroy scripts.ΒΆ - Read
apps/site/src/worker.tsfor Worker construction and binding consumers.ΒΆ - Read
apps/site/src/rate-limits.tsfor native rate-limit types and limits.ΒΆ - Read
apps/site/src/sandbox-worker-loader.tsfor the fresh-isolate sandbox boundary.ΒΆ - Read
.brain/projects/ratstack-sh/deploy-ratstack-sh.svxfor real deployment history and failures.ΒΆ - Read
.brain/projects/ratstack-sh/research-alchemy-effect-worker.svxfor the Worker shape and local-dev research.ΒΆ
Name the resource, request a plan, and check the diff.ΒΆ
Last changed 2026-10-10 in 978dccc. Source on GitHub. Change log.
Unlinked mentions (17)
- add-a-store β Follow its source-first rules before changing Effect or Alchemy code.
- Agent rules for rat-stack (AGENTS.md) β ts, authenticated through Alchemy profiles, never through env vars in this repo
- Analytics β Mischief declares MischiefCrashTail as an Alchemy tailConsumers entry.
- Build and run rat-stack (README.md) β Vendored source mirrors cover Effect, effect-solutions, XState, and Alchemy.
- CAFE names the Cloudflare, Alchemy, Foldkit and Effect stack β Alchemy reads the same program to deploy and to test.ΒΆ
- Cartridges β Add with one package and one Layer or Alchemy Stack line.ΒΆ
- Effect + Alchemy peer patterns β 117, Alchemy 2.0.0-beta.79, and XState 6.0.0-alpha.59.
- Effect + Alchemy peers β Targeted discovery also checks XState 6 alpha and @xstate/effect independently of Alchemy; single-line peers qualify when their machine or runtime seam is relevant.
- Effect 4 study: September 2026 β @effect/language-service is on in every Node project (lalph, ghui, t3code, alchemy peer).
- find-peers β A relevant XState runtime or integration can qualify without Alchemy.
- gardener β Alchemy declares and deploys cloud resources through Effect programs.
- learn-rat-stack β An Alchemy Stack groups one cloud infrastructure program and its outputs.
- profile-a-worker β Read it from the Alchemy state or the dashboard; do not guess it.ΒΆ
- Purpose and boundaries of rat-stack (VISION.md) β The goal is to build the best Effect + Alchemy application that we can, as close to a perfect Effect application as we can get.
- rat-stack β CAFE news and the CAFE directory: reviewed Cloudflare, Alchemy, Foldkit and Effect news and projects
- The plan model β He says Alchemy v2 adopted output values that can connect to other resources before deployment.
- uncomplect β Alchemy binding requirements let types check resource dependencies.