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

Trace one deploy

Read apps/infra/alchemy.run.ts from top to bottom.ΒΆ

  1. The file imports Alchemy, the Cloudflare provider, Effect, and the Mischief Worker from apps/site/src/worker.ts.ΒΆ
  2. Alchemy.Stack creates the program. Effect.gen gives it a place to yield resources and return outputs.ΒΆ
  3. Cloudflare.providers() supplies Cloudflare operations. Cloudflare.state() lets Alchemy compare this run with prior state.ΒΆ
  4. ALCHEMY_DEV guards resources that should not be touched by local development.ΒΆ
  5. The Zone is named RatstackZone and adopted because ratstack.sh already exists. A Zone normally retains on destroy, and this Stack does not opt into deleting it.ΒΆ
  6. The DNS-AID SVCB and TXT records point agent discovery at the deployed site. They use the adopted Zone’s zoneId.ΒΆ
  7. Cloudflare.DNS.Dnssec keeps DNSSEC active. It is adopted for the same reason as the Zone.ΒΆ
  8. Mischief declares the Worker with ratstack.sh as its custom domain, www.ratstack.sh as a redirect, and port 1337 for dev.ΒΆ
  9. The Worker construction yields its WorkerLoader and four RateLimit bindings: API_PER_IP, EXECUTE_GLOBAL, EXECUTE_PER_IP, and INTEREST_PER_IP. Effect.provide(Cloudflare.Workers.RateLimitBinding) supplies the RateLimit client layer.ΒΆ
  10. apps/site/src/worker.ts also yields the Interest and InterestIndex Durable Objects. EVENTS_ENABLED chooses Basin or basinFoundation: 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.ΒΆ
  11. The Stack yields Website from apps/web/src/website.ts and returns both mischiefUrl and websiteUrl.ΒΆ

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

Try it

  1. Run pnpm infra:plan and read every action. Do not approve a deploy. Identify which resources are noop and which would change.ΒΆ
  2. 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.ΒΆ
  3. Run pnpm infra:dev. Alchemy starts the Worker in local workerd, using the dev port from apps/site/src/worker.ts; the ALCHEMY_DEV branch skips the production Zone and DNS resources.ΒΆ

Read before changing

  1. Read AGENTS.md for pins, commands, boundaries, and the fence39: checks and hooks that reject prohibited code and shortcuts.ΒΆ
  2. Read VISION.md for why the scaffold keeps infrastructure and application contracts explicit.ΒΆ
  3. Read apps/infra/alchemy.run.ts for the Stack and the cloud footprint.ΒΆ
  4. Read apps/infra/package.json for plan, deploy, dev, and destroy scripts.ΒΆ
  5. Read apps/site/src/worker.ts for Worker construction and binding consumers.ΒΆ
  6. Read apps/site/src/rate-limits.ts for native rate-limit types and limits.ΒΆ
  7. Read apps/site/src/sandbox-worker-loader.ts for the fresh-isolate sandbox boundary.ΒΆ
  8. Read .brain/projects/ratstack-sh/deploy-ratstack-sh.svx for real deployment history and failures.ΒΆ
  9. Read .brain/projects/ratstack-sh/research-alchemy-effect-worker.svx for 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.