Skip to content

Binding

A Binding connects a Resource to a Platform — a Worker, a Lambda Function, a Container. One bind() call generates the IAM policies, the environment variables, and the typed SDK wrapper your handler uses. You don’t write any of that by hand.

This page covers the mechanics. For the bigger picture of how bindings fit into a Platform’s runtime, see Platform › Bindings in action.

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

bucket here is the resource itself, presented as a typed client. There is no env.BUCKET, no BUCKET_NAME lookup — the binding is the SDK.

Bindings work in both handler styles a Platform supports. Pick whichever your Worker / Lambda is using.

Effect stylebind() inside the init Effect, returns a typed handle:

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("key");
// ...
}),
};
}),
);

Async style — declare bindings on the resource, type the env with InferEnv:

alchemy.run.ts
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", {
main: "./src/worker.ts",
bindings: { Bucket, KV },
});
// src/worker.ts
export default {
async fetch(request: Request, env: WorkerEnv) {
const obj = await env.Bucket.get("key");
// ...
},
};

The rest of this page walks through the Effect style; the same deploy-time mechanics apply to both.

Each call records three things on the platform’s plan:

  1. Permissions — IAM (AWS) or Worker bindings (Cloudflare)
  2. Environment / configuration — physical names, ARNs, URLs
  3. A typed SDK wrapper — bundled into the handler

Each binding maps to specific IAM actions on the exact resource ARNs. Alchemy generates least-privilege policies — Resource: "*" is only used when the API genuinely doesn’t support resource-level scoping.

BindingIAM ActionsResource
S3.GetObject.bind(bucket)s3:GetObjectarn:aws:s3:::bucket-name/*
S3.PutObject.bind(bucket)s3:PutObjectarn:aws:s3:::bucket-name/*
SQS.SendMessage.bind(queue)sqs:SendMessageQueue ARN
DynamoDB.GetItem.bind(table)dynamodb:GetItemTable ARN
DynamoDB.PutItem.bind(table)dynamodb:PutItemTable ARN

Multi-resource bindings enumerate every ARN they touch:

const get = yield* DynamoDB.GetItem.bind(JobsTable, AuditTable);
// → policy enumerates both table ARNs explicitly

Bindings inject the env vars the SDK wrapper needs — BUCKET_NAME, QUEUE_URL, TABLE_ARN, etc. You don’t read these yourself; the typed wrapper takes care of it.

On Cloudflare, the same call attaches a native Worker binding (R2, KV, D1, Durable Object…) instead of an IAM policy:

const bucket = yield* Cloudflare.R2Bucket.bind(Bucket);
const kv = yield* Cloudflare.KVNamespace.bind(Sessions);

The runtime API is identical to the AWS counterpart — code that consumes one consumes the other.

An event source is a binding that triggers your function when something happens on a resource (DynamoDB stream, SQS message, S3 object event, etc.):

yield* DynamoDB.stream(table, {
streamViewType: "NEW_AND_OLD_IMAGES",
startingPosition: "LATEST",
batchSize: 10,
}).process((stream) =>
stream.pipe(
Stream.map((record) => JSON.stringify(record)),
Stream.run(sink),
),
);

Event sources work like regular bindings — they attach the necessary permissions and the event source mapping in one call.

Internally each binding splits into two layers — and which one runs depends on the phase:

  • Binding.Service — the runtime SDK wrapper that gets bundled into your function. This is what bucket.get(...) actually calls.
  • Binding.Policy — the deploy-time logic that emits IAM, Worker bindings, and env vars. This is not included in the runtime bundle.

At plantime the Policy layer is provided, so bind() records what the function needs. At runtime the Policy layer is absent, so the same call resolves to just the lightweight Service wrapper. The runtime bundle stays small because none of the planning code ships.

See Plantime and Runtime › Binding.Service vs Binding.Policy for a deeper look.

Bindings return Effect values. That means Effect.retry, timeout, catchTag, Stream, Sink — they all just work, with typed error channels:

const sendWithRetry = enqueue({ MessageBody: msg }).pipe(
Effect.retry({ times: 3, schedule: Schedule.exponential("100 millis") }),
Effect.timeout("5 seconds"),
Effect.catchTag("ThrottlingException", () => Effect.succeed(undefined)),
);

Because every binding is an Effect with the same shape, you can hide them behind a service interface and swap implementations without touching handler code. That’s exactly what Layers is about — read on.