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.
A binding in one line
Section titled “A binding in one line”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.
Effect style vs async style
Section titled “Effect style vs async style”Bindings work in both handler styles a Platform supports. Pick whichever your Worker / Lambda is using.
Effect style — bind() 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:
export type WorkerEnv = Cloudflare.InferEnv<typeof Worker>;
export const Worker = Cloudflare.Worker("Worker", { main: "./src/worker.ts", bindings: { Bucket, KV },});
// src/worker.tsexport 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.
What .bind() does at deploy time
Section titled “What .bind() does at deploy time”Each call records three things on the platform’s plan:
- Permissions — IAM (AWS) or Worker bindings (Cloudflare)
- Environment / configuration — physical names, ARNs, URLs
- A typed SDK wrapper — bundled into the handler
Automatic IAM (AWS)
Section titled “Automatic IAM (AWS)”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.
| Binding | IAM Actions | Resource |
|---|---|---|
S3.GetObject.bind(bucket) | s3:GetObject | arn:aws:s3:::bucket-name/* |
S3.PutObject.bind(bucket) | s3:PutObject | arn:aws:s3:::bucket-name/* |
SQS.SendMessage.bind(queue) | sqs:SendMessage | Queue ARN |
DynamoDB.GetItem.bind(table) | dynamodb:GetItem | Table ARN |
DynamoDB.PutItem.bind(table) | dynamodb:PutItem | Table ARN |
Multi-resource bindings enumerate every ARN they touch:
const get = yield* DynamoDB.GetItem.bind(JobsTable, AuditTable);// → policy enumerates both table ARNs explicitlyAutomatic environment variables
Section titled “Automatic environment variables”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.
Cloudflare bindings
Section titled “Cloudflare bindings”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.
Event sources
Section titled “Event sources”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.
How it works under the hood
Section titled “How it works under the hood”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 whatbucket.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.
All of Effect, on every binding
Section titled “All of Effect, on every binding”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.