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.filename },
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: Alchemy.StackProps<NoInfer<Cloudflare.Providers>>, eff: Effect.Effect<void, ConfigError, Alchemy.StackServices | Cloudflare.Providers>): Effect.Effect<Alchemy.CompiledStack<void, any>, ConfigError, never> (+2 overloads)
export Stack
Stack
(
"MyApp",
{
providers:
import Layer
Layer
.
const empty: Layer.Layer<never, never, never>

An empty layer that provides no services, cannot fail, has no requirements, and performs no construction or finalization work.

When to use

Use as the no-op branch when conditionally composing layers.

Example (Disabling optional lifecycle work)

import { Console, Layer } from "effect"
declare const flag: boolean
const StartupLogLive = flag
? Layer.effectDiscard(Console.log("application starting"))
: Layer.empty

@seeeffectDiscard for running an effect while providing no services

@categoryconstructors

@since2.0.0

empty
,
Error ts(2322) ― Type 'Layer<never, never, never>' is not assignable to type 'Layer<NoInfer<Providers>, never, StackServices>'. Type 'Providers' 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

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

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}`
})

@categoryconstructors

@since2.0.0

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>;
domains?: Alchemy.Input<Cloudflare.R2BucketCustomDomain[] | undefined>;
lifecycleRules?: Alchemy.Input<Cloudflare.R2BucketLifecycleRule[] | undefined>;
} | undefined) => Effect.Effect<...> (+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 Binding connects a resource to a Worker or Lambda. A single bind() call hands your handler a typed client and — at deploy time — wires up the permissions, environment variables, and platform bindings that client needs:

const bucket = yield* Cloudflare.R2Bucket.bind(Bucket);
// later, inside fetch:
yield* bucket.put("hello.txt", "world");

bucket is the resource presented as a typed SDK. There’s no env.BUCKET, no BUCKET_NAME lookup, and no hand-written IAM policy — the binding is the client.

On AWS, bind() emits least-privilege IAM scoped to the exact resource ARN. On Cloudflare, it attaches the native Worker binding (R2, KV, D1, Durable Object). The runtime API is identical, so code written against one consumes the other.

Bindings also enable circular references between resources — Worker A can hold a typed client to Worker B and vice versa — which plain props, a directed acyclic graph, can’t express.

See Bindings for event sources, sinks, and the deploy-time mechanics.

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.

A Provider teaches Alchemy how to manage a resource type. Behind every resource is a provider implementing four lifecycle operations:

  • read — look up the resource’s live state in the cloud. It tells the engine whether the resource exists and whether we own it.
  • diff — compare the desired props against the previous ones and decide whether the change is a no-op, an in-place update, or a full replacement.
  • reconcile — make the cloud match the desired state. One flow handles first-time creation, updates, and adoption: observe the live state, create the resource if it’s missing, sync any drifted fields, and return the output attributes.
  • delete — remove the resource. Idempotent, so “already gone” counts as success.

The engine drives these in a plan → apply loop: read and diff build the plan, then reconcile and delete apply it in dependency order.

Providers are Effect Layers, wired into a Stack with Cloudflare.providers() or AWS.providers():

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

The type system checks the wiring — using a Cloudflare resource without Cloudflare.providers(), or providing the wrong cloud’s providers, is a compile-time error. To support a new cloud or third-party API, see Writing a Custom Resource Provider.

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.filename },
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",
env: { 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.