Skip to content

Commit 205e1cf

Browse files
committed
[Workers for Platforms] Simplify custom bindings docs with R2 examples and clearer intent
1 parent c30c593 commit 205e1cf

1 file changed

Lines changed: 67 additions & 114 deletions

File tree

  • src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration

‎src/content/docs/cloudflare-for-platforms/workers-for-platforms/configuration/custom-bindings.mdx‎

Lines changed: 67 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Custom bindings
33
pcx_content_type: how-to
4-
description: Route resource operations through a platform-owned Worker to control per-tenant access.
4+
description: Intercept User Worker binding calls to log usage, enforce limits, and run platform-owned logic before operations reach a Cloudflare resource.
55
sidebar:
66
order: 6
77
products:
@@ -17,18 +17,26 @@ import {
1717
TabItem,
1818
} from "~/components";
1919

20-
A custom binding routes a User Worker's resource operations through your Platform Worker before they reach the underlying resource. You wrap a Cloudflare resource (for example, a [D1](/d1/) database) inside a [`WorkerEntrypoint`](/workers/runtime-apis/bindings/service-bindings/rpc/#the-workerentrypoint-class) class and use [`props`](/workers/runtime-apis/context/#props) to pass the real resource connection to your Platform Worker at upload time.
20+
Custom bindings let your platform intercept calls that User Workers make to Cloudflare resource bindings. Instead of calling the underlying resource directly, each operation first passes through logic in a Platform Worker that you control.
2121

22-
The User Worker calls the resource API as usual. It does not know that the binding routes through your Platform Worker first.
22+
For example, when a User Worker calls `env.BUCKET.put()`, your Platform Worker could:
23+
24+
- Measure the size of the upload and record it for billing.
25+
- Check whether the customer has exceeded their storage limit.
26+
- Reject objects that do not meet your platform's requirements.
27+
- Forward allowed uploads to the underlying R2 bucket.
28+
29+
From the User Worker's perspective, `BUCKET` still behaves like a normal R2 binding. The User Worker does not need to know about your interception logic or have direct access to the underlying resource.
2330

2431
## Use cases
2532

26-
Use this pattern when you deploy [User Workers](/cloudflare-for-platforms/workers-for-platforms/) and need to:
33+
Use custom bindings when you need to run platform-owned logic around resource operations:
2734

28-
- **Enforce query policy** — inspect and filter operations before they reach the resource.
29-
- **Scope access per tenant** — use `this.ctx.props` to select a tenant-specific resource or apply tenant-specific rules.
30-
- **Log or meter usage** — record every operation for auditing or billing.
31-
- **Rate-limit operations** — throttle operations per tenant or per plan tier.
35+
- **Log or meter usage** — Record operations, bytes, or other usage data for auditing and billing.
36+
- **Enforce limits** — Apply quotas or rate limits based on the customer or plan tier.
37+
- **Add custom logic** — Validate, transform, or reject operations according to your platform's requirements.
38+
- **Scope access per tenant** — Select the resource assigned to a customer and prevent access to other tenants' resources.
39+
- **Enforce resource policy** — Restrict which operations a User Worker can perform before they reach the resource.
3240

3341
## Supported resources
3442

@@ -37,59 +45,76 @@ Use this pattern when you deploy [User Workers](/cloudflare-for-platforms/worker
3745
| [D1](/d1/) | Available |
3846
| [R2](/r2/) | Not yet available |
3947

40-
## The pattern
48+
The conceptual examples below use R2 because its `get()` and `put()` operations make the interception pattern easy to understand. The runnable implementation uses D1, which is currently the supported resource.
4149

42-
Every custom binding follows the same three-step structure, regardless of the underlying resource.
50+
## How custom bindings work
4351

44-
### Step 1: Define the Platform Worker
52+
There are three parts to a custom binding:
4553

46-
Export a class that extends `WorkerEntrypoint`. This class wraps the target resource and implements a method that the binding calls for every operation.
54+
1. Define the platform-owned logic that handles intercepted resource operations.
55+
2. Give that logic the underlying resource and tenant-specific context it needs.
56+
3. Attach the custom binding so the User Worker can call the standard resource API.
4757

48-
```ts
49-
import { WorkerEntrypoint } from "cloudflare:workers";
58+
### Step 1: Define where intercepted calls are handled
5059

51-
export class MyCustomBinding extends WorkerEntrypoint {
52-
async query(params) {
53-
// Inspect params, then forward or reject.
54-
return this.ctx.props.RESOURCE.query(params);
55-
}
56-
}
57-
```
60+
Your platform needs a trusted place to receive each resource operation before it reaches the underlying resource. Define this logic as methods on a [`WorkerEntrypoint`](/workers/runtime-apis/bindings/service-bindings/rpc/#the-workerentrypoint-class) exported by your Platform Worker.
61+
62+
The method that Cloudflare invokes depends on the resource. Using R2 as a conceptual example, an upload made with `env.BUCKET.put()` would be handled by a `put()` method in your Platform Worker.
63+
64+
Inside that method, you can run your own logic. For example, you could measure the upload, record usage, check the customer's storage quota, and either reject the operation or forward it to the real bucket.
5865

59-
Your policy logic runs inside this method. To forward an operation, call the corresponding method on the resource passed through `props`. To reject an operation, return an error.
66+
For D1, resource operations are passed to a `query()` method. Refer to the [D1 implementation](#d1-implementation) for runnable code.
6067

61-
### Step 2: Access tenant context
68+
### Step 2: Provide the resource and tenant context
6269

63-
When you upload a User Worker, you pass `props` in the binding configuration. These values are trusted and platform-owned — the User Worker cannot read or change them.
70+
The interception logic needs to know which resource and platform rules apply to the User Worker. Pass this trusted information through [`props`](/workers/runtime-apis/context/#props) when you configure the custom binding during the User Worker upload.
6471

65-
Use `this.ctx.props` inside the Platform Worker to read per-tenant values such as:
72+
For example, `props` could contain:
6673

67-
- The resource connection for a specific tenant.
68-
- A tenant or customer ID.
69-
- The maximum operations allowed per minute.
70-
- The plan tier.
74+
- The underlying resource assigned to the customer.
75+
- A customer or tenant ID.
76+
- The customer's plan tier.
77+
- A rate limit or storage quota.
7178

72-
### Step 3: Call from the User Worker
79+
The Platform Worker reads these values from `this.ctx.props`. The User Worker cannot read or modify them.
7380

74-
The User Worker calls the standard resource API (for example, `env.DB.prepare().all()` for D1). The `binding_interface` field on the binding configuration makes the service binding present the native resource API to the User Worker.
81+
Using the R2 example, your Platform Worker could receive the customer's bucket, tenant ID, and storage limit through `props`. Its `put()` method could then apply that customer's limits before forwarding the upload.
7582

76-
The User Worker does not need any code changes. It does not know the binding routes through your Platform Worker.
83+
### Step 3: Call the resource from the User Worker
84+
85+
The User Worker uses the standard API for the resource. It does not call your Platform Worker directly.
86+
87+
Using R2 as a conceptual example, the User Worker would call:
88+
89+
```js
90+
await env.BUCKET.put("image.png", image);
91+
```
92+
93+
The `binding_interface` configuration makes the binding expose the resource's native API. Cloudflare routes the operation to the corresponding method in your Platform Worker, where your custom logic runs.
94+
95+
For D1, the User Worker continues to call methods such as `env.DB.prepare().bind().all()`. No changes are required to its database code.
7796

7897
## D1 implementation
7998

80-
D1 custom bindings use a `query()` method on the Platform Worker. The D1 binding translates the standard D1 API calls (`prepare()`, `bind()`, `all()`) into a single `query()` RPC call that your Platform Worker receives.
99+
D1 custom bindings use a `query()` method in the Platform Worker. Cloudflare translates the User Worker's standard D1 calls, such as `prepare()`, `bind()`, and `all()`, into a `query()` call.
100+
101+
Your `query()` method can inspect the statements, record usage, enforce limits, or reject the operation. If the operation is allowed, it forwards the request to the D1 database provided through `this.ctx.props`.
81102

82103
### Request flow
83104

84-
1. **User Worker** — calls `env.DB.prepare().bind().all()` like a normal D1 binding.
85-
2. **Platform Worker** — receives the call as a single `query()` invocation. Your code inspects, logs, rate-limits, rejects, or forwards it.
86-
3. **D1 database** — the real database, passed to the Platform Worker through `props`.
105+
1. The User Worker calls `env.DB.prepare().bind().all()` as it would with a normal D1 binding.
106+
2. Cloudflare sends the operation to the Platform Worker's `query()` method.
107+
3. The Platform Worker inspects the query and applies your custom logic.
108+
4. The Platform Worker either returns an error or forwards the query to `this.ctx.props.D1`.
109+
5. The result is returned to the User Worker through the standard D1 API.
110+
111+
If the Platform Worker rejects the operation, the User Worker receives a `D1_ERROR`.
87112

88113
<CustomBindingsDiagram />
89114

90115
### Set up a D1 custom binding
91116

92-
The steps below use placeholders like `<YOUR_ACCOUNT_ID>` and `<YOUR_DATABASE_ID>`. Replace them with your own values.
117+
This procedure assumes that you already have a Workers for Platforms dispatch namespace and dynamic dispatch Worker. If you do not, complete the [Workers for Platforms get started guide](/cloudflare-for-platforms/workers-for-platforms/get-started/) first.
93118

94119
#### 1. Create the Platform Worker
95120

@@ -203,7 +228,7 @@ wrangler deploy
203228

204229
#### 2. Write the User Worker
205230

206-
The User Worker calls `env.DB.prepare().bind().all()` as usual. Save this file as `worker.js` — the file name must match the `main_module` value you send in step 4.
231+
The User Worker calls `env.DB.prepare().bind().all()` as usual. Save this file as `worker.js` — the file name must match the `main_module` value used when you upload the User Worker.
207232

208233
<Tabs syncKey="workersExamples"> <TabItem label="JavaScript" icon="seti:javascript">
209234

@@ -265,15 +290,7 @@ export default {
265290

266291
</TabItem> </Tabs>
267292

268-
#### 3. Create a dispatch namespace
269-
270-
User Workers are uploaded into a dispatch namespace and invoked through a [dynamic dispatch Worker](/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch/). They are not routable directly.
271-
272-
```sh
273-
wrangler dispatch-namespace create custom-binding-ns
274-
```
275-
276-
#### 4. Upload the User Worker with the custom binding
293+
#### 3. Upload the User Worker with the custom binding
277294

278295
Configure `env.DB` as a service binding to the Platform Worker, then add two fields that turn it into a custom D1 binding:
279296

@@ -286,7 +303,7 @@ Upload with the [Workers for Platforms script upload API](/api/resources/workers
286303

287304
```bash
288305
curl -X PUT \
289-
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/dispatch/namespaces/custom-binding-ns/scripts/user-worker" \
306+
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/dispatch/namespaces/$NAMESPACE_NAME/scripts/user-worker" \
290307
-H "Authorization: Bearer $API_TOKEN" \
291308
-F 'metadata={
292309
"main_module": "worker.js",
@@ -321,73 +338,9 @@ The multipart field name (`worker.js`) becomes the module name and must match `m
321338
Only `type: "d1"` is accepted inside `$binding`. Any other binding type returns a `400`.
322339
:::
323340

324-
#### 5. Deploy a dispatch Worker
325-
326-
Create a dispatch Worker that forwards requests into the namespace:
327-
328-
<Tabs syncKey="workersExamples"> <TabItem label="JavaScript" icon="seti:javascript">
329-
330-
```js title="src/index.js"
331-
export default {
332-
async fetch(request, env) {
333-
const userWorker = env.DISPATCHER.get("user-worker");
334-
return userWorker.fetch(request);
335-
},
336-
};
337-
```
338-
339-
</TabItem> <TabItem label="TypeScript" icon="seti:typescript">
340-
341-
```ts title="src/index.ts"
342-
interface Env {
343-
DISPATCHER: DispatchNamespace;
344-
}
345-
346-
export default {
347-
async fetch(request, env): Promise<Response> {
348-
const userWorker = env.DISPATCHER.get("user-worker");
349-
return userWorker.fetch(request);
350-
},
351-
} satisfies ExportedHandler<Env>;
352-
```
353-
354-
</TabItem> </Tabs>
355-
356-
Add a Wrangler configuration for the dispatch Worker:
357-
358-
<WranglerConfig>
359-
360-
```toml
361-
name = "custom-binding-dispatcher"
362-
main = "src/index.js"
363-
compatibility_date = "$today"
364-
365-
[[dispatch_namespaces]]
366-
binding = "DISPATCHER"
367-
namespace = "custom-binding-ns"
368-
```
369-
370-
</WranglerConfig>
371-
372-
Deploy the dispatch Worker:
373-
374-
```sh
375-
wrangler deploy
376-
```
377-
378-
#### 6. Test the binding
379-
380-
Send a read and a write to the dispatch Worker URL:
381-
382-
```bash
383-
# Allowed: the SELECT is forwarded to D1.
384-
curl "https://custom-binding-dispatcher.<YOUR_SUBDOMAIN>.workers.dev"
385-
386-
# Rejected: the DELETE returns a D1_ERROR to the User Worker.
387-
curl -X DELETE "https://custom-binding-dispatcher.<YOUR_SUBDOMAIN>.workers.dev"
388-
```
341+
#### 4. Test the binding
389342

390-
To watch interception in real time, tail the Platform Worker in another terminal:
343+
Send requests through your dynamic dispatch Worker. To watch interception in real time, tail the Platform Worker in another terminal:
391344

392345
```sh
393346
wrangler tail custom-binding-platform-worker

0 commit comments

Comments
 (0)