Skip to content

What is Alchemy?

Alchemy is an Infrastructure-as-Effects framework. It extends Infrastructure-as-Code by combining your cloud resources and the application logic that uses them into a single, type-safe program powered by Effect.

Infrastructure-as-Code vs Infrastructure-as-Effects

Section titled “Infrastructure-as-Code vs Infrastructure-as-Effects”

Traditional IaC tools like Terraform, Pulumi, and CDK separate infrastructure definitions from application code. You write your infrastructure in one place and your business logic in another, then wire them together with environment variables, ARNs, and config files.

Alchemy takes a different approach: infrastructure and logic are Effects in the same program.

// alchemy.run.ts — infrastructure and logic in one program
import * as Alchemy from "alchemy";
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import Worker from "./src/worker.ts";
const Bucket = Cloudflare.R2Bucket("Bucket");
export default Alchemy.Stack(
"MyApp",
{ providers: Cloudflare.providers() },
Effect.gen(function* () {
const bucket = yield* Bucket;
const worker = yield* Worker;
return { url: worker.url };
}),
);
// src/worker.ts — the Worker binds the Bucket directly
import * as Cloudflare from "alchemy/Cloudflare";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
import { Bucket } from "./bucket.ts";
export default Cloudflare.Worker(
"Worker",
{ main: import.meta.path },
Effect.gen(function* () {
const bucket = yield* Cloudflare.R2Bucket.bind(Bucket);
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 });
}),
};
}),
);

The Bucket resource, the Worker resource, and the runtime fetch handler all live in the same codebase, composed with the same yield* syntax. There’s no separate “infra” project — it’s one program.

Alchemy uses TypeScript and Effect’s type system to catch mistakes at compile time. If you forget to provide the right providers for your resources, the compiler tells you:

import * as
import Alchemy
Alchemy
from "alchemy";
import * as
import Cloudflare
Cloudflare
from "alchemy/Cloudflare";
import * as
import Effect
Effect
from "effect/Effect";
import * as
import Layer
Layer
from "effect/Layer";
export default
import Alchemy
Alchemy
.
Stack<void, Cloudflare.Providers>(stackName: string, options: {
providers: Layer.Layer<NoInfer<Cloudflare.Providers>, never, StackServices>;
}, eff: Effect.Effect<void, never, StackServices | Cloudflare.Providers>): Effect.Effect<CompiledStack<void, any>, never, never> (+2 overloads)
export Stack
Stack
(
"MyApp",
{
providers:
import Layer
Layer
.
const empty: Layer.Layer<never, never, never>

A Layer that constructs an empty Context.

This layer provides no services and can be used as a neutral element in layer composition or as a starting point for building layers.

@example

import { Layer } from "effect"
const emptyLayer = Layer.empty

@since2.0.0

@categoryconstructors

empty
,
Error ts(2322) ― Type 'Layer<never, never, never>' is not assignable to type 'Layer<NoInfer<Providers>, never, StackServices>'. Type 'ProviderCollectionShape<"Cloudflare">' is not assignable to type 'never'.
},
import Effect
Effect
.
const gen: <Effect.Effect<Cloudflare.R2Bucket, never, Cloudflare.Providers>, void>(f: () => Generator<Effect.Effect<Cloudflare.R2Bucket, never, Cloudflare.Providers>, void, never>) => Effect.Effect<void, never, Cloudflare.Providers> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

@example

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

@categoryCreating Effects

gen
(function* () {
const
const bucket: Cloudflare.R2Bucket
bucket
= yield*
import Cloudflare
Cloudflare
.
const R2Bucket: (id: string, props?: {
name?: Alchemy.Input<string | undefined>;
storageClass?: Alchemy.Input<Cloudflare.R2Bucket.StorageClass | undefined>;
jurisdiction?: Alchemy.Input<Cloudflare.R2Bucket.Jurisdiction | undefined>;
locationHint?: Alchemy.Input<Cloudflare.R2Bucket.Location | undefined>;
} | undefined) => Effect.Effect<Cloudflare.R2Bucket, never, Cloudflare.Providers> (+2 overloads)
R2Bucket
("Bucket");
}),
);

This error goes away when you provide Cloudflare.providers(). The type system ensures every resource has its provider wired up before you can deploy.

A Resource is any cloud entity managed by Alchemy — buckets, databases, queues, workers, IAM roles, DNS records, and more. Each resource is declared as an Effect and yield*-ed inside a Stack:

const bucket = yield * Cloudflare.R2Bucket("Bucket");
const db = yield * Cloudflare.D1Database("DB");
const queue = yield * AWS.SQS.Queue("Jobs");

Resources are just descriptions until they’re yielded. You can declare them in separate files and import them from anywhere:

src/bucket.ts
export const Bucket = Cloudflare.R2Bucket("Bucket");
src/worker.ts
import { Bucket } from "./bucket.ts";
const bucket = yield * Cloudflare.R2Bucket.bind(Bucket);

A Stack is a collection of resources deployed together. Every deploy targets a stage — an isolated environment like dev, prod, or pr-42:

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

Each stage has its own resources with distinct physical names, so environments never interfere with each other.

Providers tell Alchemy how to talk to a cloud platform. They’re expressed as Effect Layers and checked by the type system:

// Cloudflare
Alchemy.Stack(
"App",
{
providers: Cloudflare.providers(),
} /* ... */,
);
// AWS
Alchemy.Stack(
"App",
{
providers: AWS.providers()(),
} /* ... */,
);

If your Stack uses Cloudflare resources but you provide AWS providers, TypeScript catches the mismatch at compile time.

Alchemy supports two styles for writing Workers:

Effect style — your runtime code is an Effect, with typed errors, composable retries, and bindings resolved through the Effect system:

export default Cloudflare.Worker(
"Worker",
{ main: import.meta.path },
Effect.gen(function* () {
const bucket = yield* Cloudflare.R2Bucket.bind(Bucket);
return {
fetch: Effect.gen(function* () {
// Effect-native runtime code
}),
};
}),
);

Async style — your runtime code is a standard async fetch handler. Bindings are passed as props and you get a typed env object via InferEnv:

alchemy.run.ts
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
bindings: { Bucket },
});
src/worker.ts
import type { WorkerEnv } from "../alchemy.run.ts";
export default {
async fetch(request: Request, env: WorkerEnv) {
const object = await env.Bucket.get("key");
return new Response(object?.body ?? null);
},
};

Both styles use the same infrastructure declarations, the same CLI, and the same deployment pipeline. Choose whichever fits your team.