Bindings let you control what a Dynamic Worker can access. When you create a Dynamic Worker, you decide exactly what resources and operations it can use.
This allows you to:
- Give each Dynamic Worker its own resources: Partition a KV namespace, R2 bucket, or database so each Dynamic Worker only sees its own data.
- Expose custom capabilities: Define your own methods that Dynamic Workers can call, such as posting to a chat room, sending an email, or querying an internal service. You design the interface, and the Dynamic Worker calls it.
- Restrict and control access: Inspect, transform, or reject calls before they reach the underlying resource.
For apps with public and private routes, refer to Authentication and authorization. The guide checks requests before invocation and permissions inside platform-owned operations.
With custom bindings, you:
- Define the binding implementation in your loader Worker: You create a class with methods. Because this class runs in your loader Worker, you can add authentication and logging, and scope access per customer.
- Pass it to the Dynamic Worker as a binding: The Dynamic Worker calls methods like
this.env.CHAT_ROOM.post("Hello!")without knowing anything about the implementation behind it.
To create a custom binding, your loader Worker needs to implement a WorkerEntrypoint class and export it. The methods you define on this class are the methods the Dynamic Worker can call.
import { WorkerEntrypoint } from "cloudflare:workers";
export class ChatRoom extends WorkerEntrypoint {
async post(text: string): Promise<void> {
// Your implementation here
}
}Your loader Worker then creates an instance of the exported class, called a stub, and passes it into the Dynamic Worker's env.
let chatRoom = ctx.exports.ChatRoom({ props: { roomName: "#bot-chat" } });
let worker = env.LOADER.load({
env: { CHAT_ROOM: chatRoom },
// ...
});From the Dynamic Worker's perspective, CHAT_ROOM looks like a regular binding with methods it can call:
// Inside the Dynamic Worker
await this.env.CHAT_ROOM.post("Hello!");One class can serve many different Dynamic Workers. Instead of defining a separate class for each user, pass in props when you create the stub. props contains information specific to that user.
// Same class, different props per user
let aliceRoom = ctx.exports.ChatRoom({ props: { roomName: "#alice", apiKey: ALICE_KEY } });
let bobRoom = ctx.exports.ChatRoom({ props: { roomName: "#bob", apiKey: BOB_KEY } });When the Dynamic Worker calls a method on the binding, it makes a call back to your loader Worker, where the method runs. Inside that method, you can read the props through this.ctx.props. Only the loader Worker has access to the props. The Dynamic Worker never sees them.
export class ChatRoom extends WorkerEntrypoint<Cloudflare.Env, ChatRoomProps> {
async post(text: string): Promise<void> {
// Props are set when the stub is created — the Dynamic Worker never sees them
let roomName = this.ctx.props.roomName;
await postToChat(roomName, text);
}
}The following example puts it all together. Suppose you are building a platform where AI agents can post to chat rooms. Each agent should only be able to post to its assigned room, and it should never see the API key used to authenticate.
You define a ChatRoom class in your loader Worker. This class has a post method, the only method the Dynamic Worker can call on this binding. Inside this class, you control which room the message goes to, which API key is used, and what name is attached to the message.
import { WorkerEntrypoint } from "cloudflare:workers";
export class ChatRoom extends WorkerEntrypoint<Cloudflare.Env, ChatRoomProps> {
async post(text: string): Promise<void> {
let { apiKey, botName, roomName } = this.ctx.props;
// Prefix the message with the bot's name.
text = `[${botName}]: ${text}`;
// Send it to the chat service.
await postToChat(apiKey, roomName, text);
}
}
type ChatRoomProps = {
apiKey: string;
roomName: string;
botName: string;
};You export one ChatRoom class, but each stub you create can have different props, such as a different room name, API key, or bot name. The props are set when you create the stub, and the Dynamic Worker never sees them.
Now pass it to a Dynamic Worker:
// Create a stub scoped to a specific room.
let chatRoom = ctx.exports.ChatRoom({
props: {
apiKey,
roomName: "#bot-chat",
botName: "Robo",
},
});
let worker = env.LOADER.load({
env: {
CHAT_ROOM: chatRoom,
},
compatibilityDate: "$today",
mainModule: "index.js",
modules: {
"index.js": `
export class Agent extends WorkerEntrypoint {
async run() {
// This is all the Dynamic Worker sees.
await this.env.CHAT_ROOM.post("Hello!");
}
}
`,
},
globalOutbound: null,
});
return worker.getEntrypoint("Agent").run();The agent calls this.env.CHAT_ROOM.post("Hello!"). It has no way to post to a different room, see or use the API key, or change the bot name attached to its messages.
To pass resources like a KV namespace or R2 bucket to a Dynamic Worker, you need to bind the resource to your loader Worker and create a custom binding that wraps it. You can scope access per customer by prefixing keys and defining only the methods you want to expose.
First, bind the KV namespace to your loader Worker. Then in your loader Worker, export a class that uses the KV binding and defines the methods Dynamic Workers can call:
import { WorkerEntrypoint } from "cloudflare:workers";
export class MyStorage extends WorkerEntrypoint<Cloudflare.Env, MyStorageProps> {
// Export this class from your loader Worker
// The Dynamic Worker will be able to call get() and put()
async get(key: string): Promise<string | null> {
// Prefix the key so this customer can only access their own data
return this.env.MY_KV.get(`${this.ctx.props.prefix}:${key}`);
}
async put(key: string, value: string): Promise<void> {
await this.env.MY_KV.put(`${this.ctx.props.prefix}:${key}`, value);
}
}
type MyStorageProps = {
prefix: string;
};Then pass it to the Dynamic Worker with a customer-specific prefix:
// Create a stub scoped to this customer's prefix
let storage = ctx.exports.MyStorage({
props: { prefix: `customer-${customerId}` },
});
let worker = env.LOADER.load({
env: { STORAGE: storage },
// ...
});The Dynamic Worker uses it like any other binding:
// Inside the Dynamic Worker, it sees STORAGE with get and put
let value = await this.env.STORAGE.get("settings");
await this.env.STORAGE.put("settings", "dark-mode");This same pattern works for any resource your loader Worker has access to, such as R2 buckets or D1 databases. Bind the resource to your loader Worker, export a class that uses it, and pass the stub to the Dynamic Worker.
For persistent storage that lives with each Dynamic Worker, refer to Durable Object Facets.
Custom bindings follow a capability-based security model: a Dynamic Worker can only access what you explicitly give it. If it has not received a stub for something, it cannot access it.
This is powered by Workers RPC, also known as Cap'n Web ↗︎, an RPC system designed to pass object references across security boundaries. When a Dynamic Worker receives a stub, it can call that object's methods and each call is an RPC back to the original object in your loader Worker. Stubs have no global identifier and cannot be forged. The only way to obtain one is to receive it.
Capability-based security is essential to the design of most successful sandboxes, though it is usually hidden as an implementation detail. For example, Android has Binder, Chrome has Mojo, and Cloudflare Workers has Cap'n Web. Dynamic Workers directly expose this power to you, the developer, so that you can build your own strong sandbox.