What is Alchemy?
Alchemy is a TypeScript framework for building cloud applications with Effects and Layers. Your infrastructure and the code that runs on it are written together, type-checked together, and deployed together.
Here is an R2 Bucket and a Worker that serves files from it:
import * as Cloudflare from "@alchemy.run/cloudflare";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/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)),);The Bucket is declared next to the Worker that uses it, and the Worker
asks for it in its own code. Cloudflare.R2.ReadWriteBucket(Uploads)
does two jobs. When you deploy, it gives the Worker access to the
Bucket. When the Worker runs, it returns a typed client, so
bucket.get is checked like any other function call.
On AWS the same kind of line writes the IAM policy and the environment variable for you:
const getItem = yield* AWS.DynamoDB.GetItem(Jobs);// → { Action: ["dynamodb:GetItem"], Resource: [Jobs.tableArn] }// → Jobs_tableName=Jobs-a1b2c3More than just Infrastructure as Code
Section titled “More than just Infrastructure as Code”Infrastructure as Code tools like Terraform, Pulumi, and the CDK only cover the first half of that file: the Bucket and the Worker. The Worker’s code is a separate program that finds the Bucket through an environment variable. Alchemy can be used that way too:
import * as Cloudflare from "@alchemy.run/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 }); },};The two halves meet at one name, Uploads. The handler can’t see how
the Bucket got there, and the infrastructure can’t see what the handler
does with it. Every feature you build crosses that line. A new upload
route needs a Bucket, permission to use it, and the code that calls it,
in two places that have to agree.
Alchemy is built on Infrastructure as Code, but it doesn’t stop there. When the Worker’s code is an Effect that asks for the Bucket itself, there is no name to keep in sync, no policy to write, and no client to set up.
Infrastructure as Effects
Section titled “Infrastructure as Effects”We call this Infrastructure as Effects. Your app is built from Effects and Layers, and the infrastructure it needs is part of them. Their types record the resources it uses and the access it needs, so a missing piece is a compile error, not a failed deploy.
A Worker, Lambda Function, or Container is a Runtime: a resource that carries the code it runs. Its code always has the same shape. Ask for what you need, then return what you expose:
Effect.gen(function* () { // ask for what you need const bucket = yield* Cloudflare.R2.ReadWriteBucket(Uploads);
// return what you expose return { fetch: /* ... */ };});The outer Effect runs when you deploy and again when the Worker starts.
This is the Construction phase. The fetch it returns runs once per
request. This is the Runtime phase.
ReadWriteBucket is a Binding: it connects a resource to the
Runtime that uses it. ReadWriteBucketBinding is the Layer that
decides how. Swap it to reach the Bucket over HTTP instead, 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 tokenUnderneath, it’s Infrastructure as Code
Section titled “Underneath, it’s Infrastructure as Code”Deploying works like Terraform or Pulumi. A Stack is the unit you deploy. It creates resources and returns the outputs you want printed:
import * as Alchemy from "alchemy";import * as Cloudflare from "@alchemy.run/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:
alchemy deploy # dev_$USER by defaultalchemy deploy --stage prodAlchemy creates the Bucket, bundles the Worker, gives it access to the
Bucket, and prints the URL. Each stage is a separate copy of every
resource, so dev and prod never touch each other.
Every deploy reads the last result from state, compares it with your program, shows a plan, and applies only what changed:
A Resource is anything in your cloud that Alchemy manages: a Bucket, a database, a queue, a Worker, a DNS record. Its outputs are typed values you can pass to other resources:
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 manage one kind of resource. Each cloud ships its providers as one Layer, and a Stack takes as many as it needs:
providers: Layer.mergeAll(Cloudflare.providers(), AWS.providers()),Your services own their infrastructure
Section titled “Your services own their infrastructure”Because infrastructure is created inside Effects, a service’s Layer
can create what it needs. Here is a JobService that stores
jobs in a KV Namespace:
export const JobServiceKV = Layer.effect( JobService, Effect.gen(function* () { const Jobs = yield* Cloudflare.KV.Namespace("Jobs"); const kv = yield* Cloudflare.KV.ReadWriteNamespace(Jobs); return { getJob: (id: string) => kv.get<Job>(id, "json") }; }),);The KV Namespace, the access to it, and the code that reads it ship
together. Any Runtime that provides JobServiceKV gets all three. Your
business logic only sees JobService, so moving to 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)); // a DynamoDB Table insteadThis is why Alchemy reaches past infrastructure into your application.
Things you would normally run as separate systems become Layers. A durable workflow is plain effect/workflow code, and
the Layer you provide picks where it runs:
Effect.gen(function* () { const greet = yield* Workflows.RunWorkflow(Greet); // ...}).pipe( Effect.provide(GreetLive.pipe(Layer.provideMerge(Cloudflare.Workflows.layerDurableObjectEngine()))), Effect.provide(GreetLive.pipe(Layer.provideMerge(Cloudflare.Workflows.NativeEngine))),);The same idea runs through the rest of the docs. APIs are typed RPC and HTTP services between your Runtimes. Domain models your business as commands, events, and views and runs them on Durable Objects. Telemetry traces every request, and its dashboards and alerts are resources you deploy with it. Tests deploy your app to a stage of its own, run against it, and tear it down.