Skill: Add a Cloudflare Binding
Workflow
1. Edit wrangler.jsonc
Add the binding configuration. Each binding type has its own top-level key:
KV Namespace
"kv_namespaces": [
{
"binding": "CACHE",
"id": "<namespace-id>" // from: npx wrangler kv namespace create CACHE
}
]
R2 Bucket
"r2_buckets": [
{
"binding": "STORAGE",
"bucket_name": "my-bucket" // from: npx wrangler r2 bucket create my-bucket
}
]
D1 Database
"d1_databases": [
{
"binding": "DB",
"database_name": "my-db",
"database_id": "<database-id>", // from: npx wrangler d1 create my-db
"migrations_dir": "drizzle"
}
]
Queue (Producer)
"queues": {
"producers": [
{
"binding": "MY_QUEUE",
"queue": "my-queue-name"
}
]
}
Secret
Secrets are NOT declared in wrangler.jsonc. They are set via CLI:
npx wrangler secret put MY_SECRET # production
# For local dev, add to .dev.vars:
# MY_SECRET="value"
Plain Variable (vars)
"vars": {
"ENVIRONMENT": "production",
"API_BASE_URL": "https://api.example.com"
}
2. Regenerate types
After editing wrangler.jsonc, always run:
npm run cf-typegen
This updates worker-configuration.d.ts so TypeScript knows about the
new binding. For example, after adding an R2 bucket with binding STORAGE,
env.STORAGE will be typed as R2Bucket.
3. Use the binding
Import env from cloudflare:workers in server-side code:
import { env } from "cloudflare:workers"
Usage patterns per binding type:
KV
// Write
await env.CACHE.put("user:123", JSON.stringify(userData), {
expirationTtl: 3600, // seconds
})
// Read
const raw = await env.CACHE.get("user:123")
const data = raw ? JSON.parse(raw) : null
// Delete
await env.CACHE.delete("user:123")
// List keys
const list = await env.CACHE.list({ prefix: "user:" })
R2
// Upload
await env.STORAGE.put("photos/cat.jpg", fileBody, {
httpMetadata: { contentType: "image/jpeg" },
})
// Download
const object = await env.STORAGE.get("photos/cat.jpg")
if (object) {
const body = object.body // ReadableStream
const contentType = object.httpMetadata?.contentType
}
// Delete
await env.STORAGE.delete("photos/cat.jpg")
// List
const listed = await env.STORAGE.list({ prefix: "photos/", limit: 100 })
D1 (raw, without Drizzle)
const { results } = await env.DB.prepare(
"SELECT * FROM users WHERE id = ?"
).bind(userId).all()
But prefer the Drizzle wrapper (import { db } from "@/db") for D1.
Queue
await env.MY_QUEUE.send({ type: "email", to: "user@example.com" })
// Send batch
await env.MY_QUEUE.sendBatch([
{ body: { type: "email", to: "a@example.com" } },
{ body: { type: "email", to: "b@example.com" } },
])
4. Create the resource (production)
For new projects, the resource must be created in Cloudflare before deploying:
npx wrangler kv namespace create CACHE
npx wrangler r2 bucket create my-bucket
npx wrangler d1 create my-db
Copy the printed IDs into wrangler.jsonc.
For local dev, most bindings work automatically with the Cloudflare Vite
plugin -- no remote resource needed. R2 and KV use local persistence in
.wrangler/state/.
Reference examples in this template
- D1:
wrangler.jsoncd1_databases+src/db/index.ts - R2:
wrangler.jsoncr2_buckets+src/routes/api/files.ts
Common mistakes to avoid
- Wrong: Importing
envin a client component.cloudflare:workersis server-only. - Wrong: Passing
envas a function argument. Import it where needed. - Wrong: Using
process.envfor bindings. That's Node.js. - Wrong: Forgetting to run
npm run cf-typegenafter editingwrangler.jsonc. TypeScript won't know about the new binding. - Wrong: Putting secrets in
wrangler.jsonc. Usewrangler secret putfor production and.dev.varsfor local dev.