What is Alchemy?
Alchemy is Infrastructure as Code built in pure Effect, with Infrastructure as Effects on top. Infrastructure as Code declares, diffs, and deploys cloud resources the way Terraform or Pulumi does. Infrastructure as Effects lets the code that runs on those resources live in the same program, as typed Effects and Layers.
Here is the whole thing in one file. An R2 Bucket, and a Worker that serves files from it:
import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
export const Uploads = Cloudflare.R2.Bucket("Uploads");
export default Cloudflare.Worker( "Api", { main: import.meta.url }, Effect.gen(function* () { const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads);
return { fetch: Effect.gen(function* () { const obj = yield* bucket.get("hello.txt"); return obj ? HttpServerResponse.text(yield* obj.text()) : HttpServerResponse.text("Not found", { status: 404 }); }), }; }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),);That one file is a complete application. The Bucket is declared next
to the Worker that uses it, and bucket is a typed client the handler
closes over. Let’s start with Infrastructure as Code, then look at what
Infrastructure as Effects adds on top.
Infrastructure as Code
Section titled “Infrastructure as Code”The core of Alchemy is Infrastructure as Code, in the same family as Terraform, Pulumi, CloudFormation, and the CDK.
A Stack is the unit you deploy, an Effect that yields resources and returns the outputs you want printed:
import * as Alchemy from "alchemy";import * as Cloudflare from "alchemy/Cloudflare";import * as Effect from "effect/Effect";import Api from "./src/api.ts";
export default Alchemy.Stack( "MyApp", { providers: Cloudflare.providers(), state: Cloudflare.state() }, Effect.gen(function* () { const api = yield* Api; return { url: api.url }; }),);Deploy it:
bun alchemy deployAlchemy creates the Bucket, bundles the Worker, wires the binding
between them, and prints the URL. Every deploy targets a stage, an isolated environment with its own
copy of every resource, so dev and prod never touch each other:
alchemy deploy # dev_$USER by defaultalchemy deploy --stage prodAlchemy reads the current state, diffs it against your program, shows the plan, and applies it in dependency order. Here is that loop for a Stack with a Bucket, a KV Namespace, and a Worker bound to both:
State is where the result persists between runs, so the next
deploy only touches what changed — the state option above picks
where it’s stored.
Resource
Section titled “Resource”A Resource is a cloud entity in a Stack managed by Alchemy: a bucket, a database, a queue, a Worker, a DNS record. Yield it in the Stack to add it to the plan:
const bucket = yield* Cloudflare.R2.Bucket("Uploads");// bucket.bucketName is an OutputEach Resource has a logical id, "Uploads" here, that Alchemy uses to
track it across deploys. Its outputs are typed values you can pass
into other Resources. Here the physical bucket name Alchemy generated
becomes an environment variable on a Worker:
const bucket = yield* Cloudflare.R2.Bucket("Uploads");
yield* Cloudflare.Worker("Api", { main: "./src/api.ts", env: { BUCKET_NAME: bucket.bucketName },});Provider
Section titled “Provider”A Provider teaches Alchemy how to read, diff, create, update, and delete one resource type. Each cloud ships its providers as an Effect Layer, and a Stack takes as many as it needs:
providers: Layer.mergeAll(Cloudflare.providers(), AWS.providers()),The type system checks the wiring. Yield an AWS resource in a Stack
that only provides Cloudflare.providers() and the program does not
compile. Providers covers the
built-in ones, and
Custom Provider shows how
to write your own.
A superset of Infrastructure as Code
Section titled “A superset of Infrastructure as Code”With only Infrastructure as Code, Alchemy looks like and functions like any other
IaC tool. One file
declares the Bucket and the Worker, passing the Bucket on the Worker’s
env. Another file is the handler, reaching for the Bucket through
that env:
import * as Cloudflare from "alchemy/Cloudflare";
export const Uploads = Cloudflare.R2.Bucket("Uploads");
export const Api = Cloudflare.Worker("Api", { main: "./src/api.ts", env: { Uploads },});
export type ApiEnv = Cloudflare.InferEnv<typeof Api>;import type { ApiEnv } from "../alchemy.run.ts";
export default { async fetch(request: Request, env: ApiEnv) { const obj = await env.Uploads.get("hello.txt"); return obj ? new Response(await obj.text()) : new Response("Not found", { status: 404 }); },};This works, and it is fully supported. InferEnv even types the env
from the declaration, which is more than most IaC tools give you. But
the two files are still held together by the name Uploads, and the
handler knows nothing about how the Bucket got there. Infrastructure
as Effects is what turns those two files into the one at the top of
this page.
Infrastructure as Effects
Section titled “Infrastructure as Effects”Infrastructure as Effects adds two new concepts to Infrastructure as Code: Runtimes and Bindings.
Runtime
Section titled “Runtime”A Runtime is a Resource that carries the code it runs: a Worker, Lambda Function, Container, or Server. That code is always written the same way, as an Effectful Constructor. Bind what you need, then return what you expose:
Effect.gen(function* () { // bind what you need const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads);
// return what you expose return { fetch: /* ... */ };});The outer Effect runs both at deploy time and at cold start; what it
returns runs per request. A Worker returns fetch, a Durable Object
returns its RPC methods, a Workflow returns its run function, and
every Runtime in Alchemy is a variation on that one shape.
Binding
Section titled “Binding”A Binding connects a Resource to the Runtime that uses it:
const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads);// bucket.get / bucket.put / bucket.list, typed end to endIt hands back a typed client and generates whatever that client needs to work. On Cloudflare that is a native Worker binding. On AWS it is an IAM statement scoped to one resource, plus the resource’s name in the Function’s environment:
const getItem = yield* AWS.DynamoDB.GetItem(Jobs);// → { Action: ["dynamodb:GetItem"], Resource: [Jobs.tableArn] }// → Jobs_tableName=Jobs-a1b2c3There is no env.Uploads to reach for and no hand-written policy. The
binding is the SDK.
Layers
Section titled “Layers”Every Binding is a contract paired with a Layer that fulfills it: the handler is written against the contract, and the Layer decides how — swap the Layer and the handler doesn’t change:
Effect.gen(function* () { const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads); // ...}).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)); // native Worker binding}).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketHttp)); // HTTP with a scoped tokenThe same split works for services of your own. A Layer can own Resources and Bindings outright:
export const JobServiceKV = Layer.effect( JobService, Effect.gen(function* () { const Jobs = yield* Cloudflare.KV.Namespace("Jobs"); // a Resource, owned by the Layer const kv = yield* Cloudflare.KV.ReadWriteNamespace(Jobs); // and its Binding return { getJob: (id: string) => kv.get<Job>(id, "json") }; }),);Business logic is implemented against the JobService contract. The
Layer decides the infrastructure behind it, so swapping KV for
DynamoDB is one line:
Effect.gen(function* () { const jobs = yield* JobService; // ...}).pipe(Effect.provide(JobServiceKV)); // a KV Namespace, created and bound}).pipe(Effect.provide(JobServiceDynamo)); // now a DynamoDB Table insteadThe Infrastructure as Effects section goes deeper from here:
Event Sources and
Sinks turn resources into Effect
Streams, Phases pins down
exactly what runs when,
Circular Bindings
lets two Workers call each other, and
Telemetry exports every
request’s traces and logs.