Bindings
A Binding connects a Resource to a Worker, Lambda Function, or Container. You yield the resource and get back a typed client:
const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);Everything else the connection needs is derived from this line:
- permission grant — least-privilege IAM on AWS, a native binding on Cloudflare
- environment configuration — points the client at the deployed resource: a table ARN, a queue URL, a sensitive API key
A binding in one line
Section titled “A binding in one line”export default Cloudflare.Worker( "Api", { main: import.meta.url }, Effect.gen(function* () { const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
return { fetch: Effect.gen(function* () { yield* bucket.put("hello.txt", "world"); // ... }), }; }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),);yield* ReadWriteBucket(Bucket) declares the binding — bucket is the
resource itself, presented as a typed client. The binding is the SDK.
Effect.provide(ReadWriteBucketBinding) on the last line chooses how
the binding is implemented.
A contract and a Layer
Section titled “A contract and a Layer”A binding has two halves. The declaration is a contract:
const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);ReadWriteBucket is a Binding.Service — a callable Context tag. It
names the capability and says nothing about how it’s satisfied:
export interface ReadWriteBucket extends Binding.Service< ReadWriteBucket, "Cloudflare.R2.ReadWriteBucket", (bucket: Bucket) => Effect.Effect<ReadWriteBucketClient>> {}The implementation is a Layer, and any Layer that satisfies the contract can be provided. Cloudflare ships two interchangeable ones for R2:
}).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),}).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketHttp)),ReadWriteBucketBinding registers a native r2_bucket binding on the
Worker and reads it off the Worker env at runtime.
ReadWriteBucketHttp mints a scoped AccountApiToken at deploy time
and calls R2 over HTTP at runtime. Same contract, same handler code.
The Layer is where the platform lives
Section titled “The Layer is where the platform lives”ReadWriteBucketBinding requires WorkerEnvironment and Worker, so
only a Cloudflare Worker can provide it:
export default AWS.Lambda.Function( "Api", { main: import.meta.url }, handler.pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)), // ✗ Type error: the Layer requires `WorkerEnvironment` and `Worker`, // which a Lambda Function can't provide);On AWS, each capability is a single Layer — DynamoDB.GetItemHttp,
S3.GetObjectHttp — that attaches least-privilege IAM statements at
deploy time and calls the service over HTTP at runtime.
Layers builds on this same tag/Layer split, one level up.
What one binding call does at deploy time
Section titled “What one binding call does at deploy time”The implementation Layer records three things on the plan:
- Permissions — IAM policies (AWS) or native bindings (Cloudflare)
- Configuration — physical names, ARNs, URLs, serialized into the Function’s environment
- A typed SDK client — bundled into the handler
Automatic IAM (AWS)
Section titled “Automatic IAM (AWS)”Each binding maps to specific IAM actions on the exact resource ARNs:
| Binding | IAM Actions | Resource |
|---|---|---|
S3.GetObject(bucket) |
s3:GetObject |
arn:aws:s3:::bucket-name/* |
S3.PutObject(bucket) |
s3:PutObject |
arn:aws:s3:::bucket-name/* |
SQS.SendMessage(queue) |
sqs:SendMessage |
Queue ARN |
DynamoDB.GetItem(table) |
dynamodb:GetItem |
Table ARN |
DynamoDB.PutItem(table) |
dynamodb:PutItem |
Table ARN |
Multi-resource bindings enumerate every ARN they touch:
const batchGet = yield* DynamoDB.BatchGetItem(JobsTable, AuditTable);// → policy enumerates both table ARNs explicitlyAutomatic environment variables
Section titled “Automatic environment variables”Bindings serialize the resolved Outputs the client needs — the queue URL, the bucket name, the table name — into the Function’s environment. The typed client resolves them for you.
Data sources: execute
Section titled “Data sources: execute”Every binding is also invocable at plan time — the shape Terraform
calls a data source and Pulumi an invoke. Capability.execute(...)
runs the operation during plan/deploy resolution (with the stack’s
services, not a deployed host) and returns an
Output you can feed straight into
resource props:
// the raw invoke — Output<ec2.Image | undefined>const image = AWS.EC2.getAmi({ owners: ["amazon"], name: ["al2023-ami-2023.*"] });
// helpers built on it — Output<string>, dies when nothing matchesimageId: AWS.EC2.amazonLinux2023(),Constructing the Output is inert, so execute is safe in composition
code that re-executes inside a deployed bundle. The same capability
bound inside a Function (yield* AWS.EC2.GetAmi(...)) still grants its
IAM and runs at runtime — one contract, both phases. The capability’s
implementation layer must be registered on the stack; the cloud
providers() layers include their plan-executable capabilities.
Event Sources and Sinks
Section titled “Event Sources and Sinks”An Event Source runs
your Function when something happens on a resource — the records arrive
as an Effect Stream. A Sink
is the write side — the resource exposed as an Effect Sink. Both wire
their own permissions, like every other binding:
// Event Source: run this Function on queue messages.yield* SQS.consumeQueueMessages(InboundQueue, (records) => /* Stream */);
// Sink: write to a queue by running a Stream into it.const sink = yield* SQS.QueueSink(OutboundQueue);How it works under the hood
Section titled “How it works under the hood”The deploy-time wiring lives in the binding’s setup Effect, fenced behind a guard:
if (!globalThis.__ALCHEMY_RUNTIME__) { // deploy-time only: register IAM / native bindings / env on the host yield* host.bind`${resource}`(/* … */);}// always: return the typed runtime clientPhases covers the guard in depth.
All of Effect, on every binding
Section titled “All of Effect, on every binding”Bindings return Effect values, so Effect.retry, timeout, and
catchTag 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)),);And because every binding has the same shape, you can hide one behind a service interface and swap implementations without touching handler code — that’s Layers. Two Functions can even bind each other — Circular Bindings.
Where next
Section titled “Where next”- Event Sources — bindings that trigger your Function. Next page.
- Sinks — bindings you write Streams into.
- Phases — when the deploy-time wiring runs vs the runtime client.
- Layers — hide bindings behind a service interface.