Skip to content

Bindings

A Binding connects a Resource to a Worker, Lambda Function, or Container. Functions & Servers showed bindings in action; this page covers the mechanics — what one binding call generates (the IAM policies, the environment variables, and the typed SDK wrapper) and why you never write any of it by hand.

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) is the binding — declared in the Effectful Constructor, used in the interface it returns. bucket is the resource itself, presented as a typed client. There is no env.BUCKET, no BUCKET_NAME lookup — the binding is the SDK, and the Effect.provide on the last line is what supplies it.

A binding has two halves. The declaration is a contract:

const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);

Cloudflare.R2.ReadWriteBucket is a Binding.Service — a callable Context tag. Yielding it declares the capability and adds the tag to the Effect’s requirements; it says nothing about how the capability is satisfied. The entire contract is three lines:

export interface ReadWriteBucket extends Binding.Service<
ReadWriteBucket,
"Cloudflare.R2.ReadWriteBucket",
(bucket: Bucket) => Effect.Effect<ReadWriteBucketClient>
> {}

The implementation is a Layer, provided at the end of the constructor — and there are two interchangeable ones:

}).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)),
}).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketHttp)),

ReadWriteBucketBinding implements the contract as a native Cloudflare binding: at deploy time it registers an r2_bucket workerd binding on the Worker; at runtime it reads that binding straight off the Worker env — no tokens, no HTTP. ReadWriteBucketHttp implements the same contract over Cloudflare’s HTTP API: at deploy time it mints a scoped AccountApiToken whose policies carry exactly the needed permission groups ("Workers R2 Storage Read" / "Workers R2 Storage Write" — least privilege per access level); at runtime the client calls R2 over HTTP with that token. This split is what lets the handler body be written generically, independent of how the binding is satisfied.

ReadWriteBucketBinding’s construction Effect does yield* WorkerEnvironment; yield* Worker, so the Layer carries those requirements — and only a Cloudflare.Worker constructor admits them. Piping the same implementation into an AWS.Lambda.Function is a type error: its constraint admits only Credentials | Region | AWSEnvironment (plus platform services), and the compiler names WorkerEnvironment and Worker as the services it cannot satisfy.

On AWS the same idea collapses to a single Layer per capability — DynamoDB.GetItemHttp, S3.GetObjectHttp — because Lambda has no workerd-style native binding interface to split against. At deploy time the Layer attaches least-privilege IAM policy statements to the Function’s existing role via host.bind; at runtime it calls the service over HTTP through distilled, its requirements (Credentials | Region | HttpClient) satisfied by the Lambda runtime.

Contract yielded in the constructor, implementation chosen with one Effect.provide — this is the seed of the Layers story. Infrastructure Layers covers the Layer.provide composition mechanics.

Bindings work in both handler styles — Effect style binds inside the init Effect; async style declares bindings on env and types them with InferEnv. See Functions & Servers › Effect handlers vs async handlers for the full comparison. The rest of this page uses the Effect style; the deploy-time mechanics are identical in both.

Each call records three things on the platform’s plan — the recording is done by the implementation Layer you provided, so which things get recorded depends on the Layer (a native binding entry, token policies, or IAM statements):

  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(bucket)s3:GetObjectarn:aws:s3:::bucket-name/*
S3.PutObject(bucket)s3:PutObjectarn:aws:s3:::bucket-name/*
SQS.SendMessage(queue)sqs:SendMessageQueue ARN
DynamoDB.GetItem(table)dynamodb:GetItemTable ARN
DynamoDB.PutItem(table)dynamodb:PutItemTable ARN

Multi-resource bindings enumerate every ARN they touch:

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

Bindings capture the resolved Outputs the client needs — the queue URL, bucket name, table name — and serialize them into the Function’s environment under canonical Output keys. You never read these yourself; the typed client resolves them.

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

const bucket = yield* Cloudflare.R2.ReadWriteBucket(Bucket);
const kv = yield* Cloudflare.KV.ReadWriteNamespace(Sessions);

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

Two binding specializations round out the model. An Event Source runs your Function when something happens on a resource — the records arrive as an Effect Stream you can map, filter, and batch. A Sink is the write-side dual — the resource exposed as an Effect Sink that batches into the underlying batch API. Both wire their own permissions, exactly 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);

Each has a dedicated page: Event Sources and Sinks.

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 client

This guarded setup Effect is the body of the implementation Layer (makeBucketBinding for the native Layer, makeHttpBucketBinding for the HTTP one) — the contract itself contains no such code. Phases covers this guard in depth.

Bindings return Effect values. That means Effect.retry, timeout, catchTag — 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 what Layers covers. Two Workers can even bind each other — Circular Bindings, covered later in this category.

  • 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.