Skip to content

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:

src/api.ts
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.

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:

alchemy.run.ts
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:

Terminal window
bun alchemy deploy

Alchemy 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:

Terminal window
alchemy deploy # dev_$USER by default
alchemy deploy --stage prod

Alchemy 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:

~/my-app
$

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.

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 Output

Each 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 },
});

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.

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:

alchemy.run.ts
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>;
src/api.ts
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 adds two new concepts to Infrastructure as Code: Runtimes and Bindings.

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.

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 end

It 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-a1b2c3

There is no env.Uploads to reach for and no hand-written policy. The binding is the SDK.

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 token

The 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 instead

The 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.