Bindings and Environment
A binding is a name. You declare it once in wrangler.jsonc against a real Cloudflare resource, you write the same name into a config slot, and Cloudflare puts the live object on the Worker env under that name when a request arrives.
Overview
Section titled “Overview”Nothing in plumix.config.ts holds a database handle. d1({ binding: "DB" }) stores the two-character string DB, because the config module evaluates on your machine during the build and no Worker env exists yet. The handler resolves the string against the real env on the first request of an isolate and reuses the live handle after that.
That indirection is what lets one config serve every deploy. Production, a preview branch and your laptop all run the same bundle with a different DB bound behind the same name.
Three config slots take a binding name, and each expects a different kind of resource.
database: d1({ binding })reads a D1 database declared underd1_databases.storage: r2({ binding })reads an R2 bucket declared underr2_buckets.kv: kv({ binding })reads a Workers KV namespace declared underkv_namespaces.
A fourth name is not yours to choose. The admin’s static files reach the browser through a Fetcher that has to be bound as ASSETS, and the section below says what breaks otherwise.
Plain values live on the same env. Anything in the wrangler vars block arrives as a string, and so does anything set with wrangler secret put. Some Cloudflare adapters look for particular keys there by convention, which is the last section on this page.
Quickstart
Section titled “Quickstart”Add an R2 bucket to the recipe site so the heroImage media field has somewhere to store files.
-
Create the bucket.
Terminal window wrangler r2 bucket create recipes-media -
Declare it. In
wrangler.jsonc:{"r2_buckets": [{"binding": "MEDIA","bucket_name": "recipes-media",},],}bindingis the name your code sees.bucket_nameis the name Cloudflare knows. They may differ, and R2’s S3 API addresses the bucket bybucket_namerather than by the binding. -
Name it in the config slot.
storage: r2({ binding: "MEDIA" }), spelled exactly as the wrangler entry spells it. -
Regenerate the env types.
Terminal window plumix typesThis forwards to
wrangler types, which reads your wrangler config and writesworker-configuration.d.tsdeclaring one member ofCloudflare.Envper binding. -
Let the dev server restart itself. Saving
wrangler.jsoncin step 2 already did it.@cloudflare/vite-pluginwatches the wrangler config path and.dev.vars, and calls Vite’s own restart when either changes, so a new binding is bound without a manual restart. The watcher listens for changes to a file that exists, not for a new one, so restart by hand the first time you add.dev.vars.
What checks the names
Section titled “What checks the names”The handler core builds for the runtime adapter walks the database, storage and kv slots for the bindings they declared as required, then asserts every one of those names is present on env. The check runs once per handler, which on Workers means once per isolate, and the result is memoized, so it costs nothing after the first request.
A failure produces one 500 response naming every missing binding at once.
{ "error": "bindings_missing", "message": "[plumix] missing required env bindings: DB, MEDIA. Declare them in the runtime's configuration and ensure the names match the slot config.", "missing": ["DB", "MEDIA"]}The body carries the detail on purpose. A missing binding is deploy metadata rather than user input, so an operator with no access to wrangler tail can still read what went wrong over plain HTTP.
Past that check, two of the three adapters validate the shape of what they found. r2() probes for a put method and kv() probes for a get, so a name bound to the wrong kind of resource fails with a message naming the binding. d1() only checks that something is there. Point it at a KV namespace or an R2 bucket and it takes the object, and the failure surfaces later, when the first query runs, as an error from Drizzle rather than from the adapter.
The ASSETS binding
Section titled “The ASSETS binding”Static files reach the browser through Workers Assets, and Plumix expects that Fetcher to be bound as ASSETS.
{ "assets": { "directory": ".plumix/public", "binding": "ASSETS", "not_found_handling": "none", },}This name is convention rather than configuration. Bind the Fetcher as something else and requests to /_plumix/admin/* fall through to an admin-not-available response, because the dispatcher has no other way to reach the staged SPA. not_found_handling: "none" matters too. It hands an unmatched path back to the Worker so Plumix can route it, rather than letting the asset layer answer with a 404 first.
Typing the env
Section titled “Typing the env”Two mechanisms fill in the type of the env your resolvers receive, and they cover different things.
wrangler types covers whatever the wrangler config declares. It emits members of the Cloudflare.Env interface, and importing @plumix/runtime-cloudflare makes PlumixEnv extend that interface. So a binding you declared in wrangler.jsonc becomes typed on env with no further work, once you have run plumix types.
Everything else you declare yourself, through the one augmentation specifier Plumix uses.
import type { PlumixEnv } from "plumix";
declare module "plumix" { interface PlumixEnv { readonly CF_ZONE_ID: string; readonly CF_CACHE_PURGE_TOKEN: string; }}
export function purgeToken(env: PlumixEnv): string { return env.CF_CACHE_PURGE_TOKEN;}Put that beside your config. Augmenting "plumix" is the whole rule, and reaching for @plumix/core here would open a second, separate interface that neither half sees. The file has to be a module of its own, which the import type line above makes it, and it has to be inside the same TypeScript program. Once it is, the merged members are visible everywhere in that program, so declare each key once and never repeat the block per file.
Vars against secrets
Section titled “Vars against secrets”The wrangler vars block is for values that may sit in your repository. The Turnstile site key on a demo deploy is one, because it ships in the page HTML anyway. Anything else belongs in a secret, and both arrive on env as strings, indistinguishable at read time.
Wrangler will also assert that a secret exists before a deploy goes out, if you list it:
{ "secrets": { "required": ["TURNSTILE_SECRET_KEY"], },}That declares the name, never the value. Secrets covers how the value gets there and how a slot reads it.
Keys the Cloudflare adapters read by convention
Section titled “Keys the Cloudflare adapters read by convention”Two adapters look for particular env keys, and for both the env is a fallback the config can beat. Set the keys as vars or as secrets, whichever the value warrants. The CDN provider is not among them: cloudflare() takes its zone id and purge token as required config, which is why the block above declares those two keys for a resolver to read.
| Key | Read by | Effect when absent |
|---|---|---|
MEDIA_PUBLIC_URL_BASE |
r2({ binding: "MEDIA" }), images() |
Object URLs resolve to null and images are served untransformed |
<BINDING>_PUBLIC_URL_BASE |
r2({ binding }) for any other binding name |
Same, for that bucket |
<BINDING>_BUCKET |
r2({ binding }) |
Presigned uploads stay off |
CF_ACCOUNT_ID |
r2() |
Presigned uploads stay off |
R2_ACCESS_KEY_ID |
r2() |
Presigned uploads stay off |
R2_SECRET_ACCESS_KEY |
r2() |
Presigned uploads stay off |
The four R2 credential keys are read as a set. A partial set leaves presigned uploads switched off rather than failing at the moment someone tries to upload a hero image. cloudflare() behaves the same way with its credential pair, which is what makes the cdn slot safe to leave in the config of a workers.dev deploy.
Local development
Section titled “Local development”plumix dev runs the Worker under workerd with a local stand-in for every binding, persisted under .wrangler/ in your project. The placeholder database_id the scaffolder writes is enough for all of it. Nothing touches your Cloudflare account until you pass --remote to a migration or run plumix deploy.
That is also why the first remote deploy needs wrangler d1 create and a real database_id. Local dev never asked for one, so the omission stays invisible until deploy time.
Related
Section titled “Related”Cloudflare Workers documents what each adapter does with the binding once it has resolved. Secrets covers the values that must not appear in wrangler.jsonc at all. Project Structure is where the generated .plumix/ directory and the rest of a scaffolded repository are catalogued.
Next steps
Section titled “Next steps”Read Secrets next, since half the env keys above are credentials. Configuration lists every config slot, including the ones no binding backs. For the shape of the whole deploy, go back to Overview.