Learn Alchemy
SkillCloud & infraLearn how Alchemy 2 turns an Effect program into a planned Cloudflare deployment.
Use Learn Alchemy in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add Learn Alchemy and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the Learn Alchemy skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
What this skill tells your AI
The instructions your AI receives, as published by joelhooks/rat-stack in skills/learn-alchemy/SKILL.md and read by ahel’s review.
Alchemy 2 declares and deploys cloud resources through Effect programs. Effect describes work with typed failures and required dependencies. One program declares resources, bindings, 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 Goodwin 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/mischief/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/mischief/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/mischief/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/mischief/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/mischief/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/mischief/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/mischief/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/mischief/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/mischief/src/worker.ts; theALCHEMY_DEVbranch skips the production Zone and DNS resources.
Read before changing
- Read
AGENTS.mdfor pins, commands, boundaries, and the fence: 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/mischief/src/worker.tsfor Worker construction and binding consumers. - Read
apps/mischief/src/rate-limits.tsfor native rate-limit types and limits. - Read
apps/mischief/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.
Signals
- GitHub stars
- 95
- Forks
- 5
- Last commit
- Oct 2026
Advanced
- Item type
- skill
- Key
learn-alchemy- Source
- github.com/joelhooks/rat-stack
github.com/joelhooks/rat-stack
Related picks
Skill · mattpocock
The pick for TypeScripttypescript-pro
Skill · jeffallan
The pick for TypeScriptreact-component-performance
Skill · davila7
The pick for Reactreact-doctor
Skill · millionco
The pick for Reactcloudflare
Skill · cloudflare
The pick for Cloudflarevigilante-issue-implementation-on-terraform
Skill · aliengiraffe
The pick for Terraform