learn-alchemy
Learn how Alchemy 2 turns an Effect program into a planned Cloudflare deployment.
소스 정보
- 저장소
- joelhooks/mischief
- 최근 소스 활동
- 2026년 10월 10일 21:06
- 감지된 SKILL.md 언어
- 영어
- 스타
- 0
- 포크
- 0
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
SKILL.md 표시 중
SKILL.md
소스 지침 · 읽기 전용 미리보기- name
- learn-alchemy
- description
- Learn how Alchemy 2 turns an Effect program into a planned Cloudflare deployment.
- plain
- Alchemy 2 runs an Effect program as a plan: review its actions, then deploy to a named stage.
- diagram
- Stack program │ resources, bindings ▼ plan create update adopt noop │ review first ▼ deploy --stage prod default: live_$USER
# Learn Alchemy
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", ...)` in `apps/infra/alchemy.run.ts`.
- **Provider** supplies the cloud API implementation. `Cloudflare.providers()` appears in the Stack options in `apps/infra/alchemy.run.ts`.
- **State** records the resources that a Stack already owns. Rat-stack uses `Cloudflare.state()` in `apps/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_SANDBOX` WorkerLoader and four RateLimit bindings in `apps/mischief/src/worker.ts`.
- **Plan** shows differences between declared resources and stored deployment state before a provider changes anything. Read it with the `plan` script in `apps/infra/package.json`.
- **Stage** names an isolated deployment state. Rat-stack's production stage is `prod`; the unflagged default is `live_$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.sh` Zone and its DNSSEC setting use `adopt(true)` in `apps/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 in `apps/infra/alchemy.run.ts`.
## 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/mischief/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/mischief/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:
```ts
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.
```ts
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`:
```sh
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 typecheck` does not stop Alchemy from producing a deploy plan or bundle. Run `pnpm turbo run check test build` first 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.json` and 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.ts` uses 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
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/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.
3. Run `pnpm infra:dev`. Alchemy starts the Worker in local workerd, using the `dev` port from `apps/mischief/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 fence: 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/mischief/src/worker.ts` for Worker construction and binding consumers.
6. Read `apps/mischief/src/rate-limits.ts` for native rate-limit types and limits.
7. Read `apps/mischief/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.
GitHub에서 보기