Secrets and variables
What each user supplies, and what you attach to the worker — the two kinds, and which is which.
A module can need a credential for two quite different reasons, and Odel handles them in two different ways. Confusing them is the most common way to wire a module wrong, and it fails quietly rather than loudly — so this page starts here rather than with the UI.
What the user supplies. You declare the names you need; each user provides their own value when they connect; the values arrive with every request. Per-user, resolved per call.
What you supply. Static values attached to the worker itself, identical for every caller on every call. No user ever sees or provides one. These come in two independent sets — one for your development deployment, one for production.
| The user's | Yours | |
|---|---|---|
| Who provides the value | Each user, when they connect | You, once |
| Varies by caller | Always | Never — one value for everyone |
| When it's resolved | Fetched for each request | Attached at deploy, fixed until you change it |
| Where it's declared | Required Secrets, Overview tab | — |
| Where the value is set | The user's install form, or their vault | Secrets & Variables, Deployments tab |
| Separate dev and prod | No — one value per user | Yes — two sets, configured separately |
| How you read it | getRequiredSecret(extra, 'NAME') | ctx.env.NAME |
| What it's for | Bring-your-own credentials, and per-user config | Your API keys — the account you own and pay for; config that's the same for everybody |
Declaring what you need from users
The Required Secrets list on the Overview tab is the first half of the user lane: it's where
you, the creator, state which values a user must supply. Odel already puts identity in every
request — userId, displayName, and the rest of the context
— without you asking. Required Secrets is for everything beyond that.
It holds names only, never values. Each name you add becomes a field in the dialog a user sees when
they install the module, and whatever they enter comes back to you at
getRequiredSecret(extra, 'NAME').
Nothing on this page's Secrets & Variables card affects that list, and nothing in that list becomes a binding. They are the two halves of two different lanes.
Getting it the wrong way round
The two failure modes look nothing alike, and neither announces itself.
Your own key declared as a required secret. Every user is prompted for a credential to an account that is yours. Most simply can't produce one, and the module looks broken on first use.
A user's key put in a binding. This is the damaging direction. One credential now serves everybody: every caller reaches the same upstream account, on your rate limit and your bill, and the upstream cannot tell your users apart. Whatever comes back belongs to whoever owns the account that key opens — which means one user's data can be handed to another. If a module is meant to work against each user's account, the value has to arrive per request. No binding does that.
The test is one question: would two different users need different values here? Yes means it's theirs — declare it. No means it's yours — bind it.
Where bindings live
Bindings are stored on the deployed Cloudflare Worker itself, not in an Odel database. Dev and production are separate workers in separate namespaces, so they have entirely separate sets — a value added to dev does not exist in production until you put it there.
You can add bindings before you have ever deployed. Odel puts a placeholder worker in place to hold
them, which answers every request with a 503 until a real deployment replaces it.
Adding one
The Secrets & Variables card is on the Deployments tab, with Development and Production tabs of its own. Pick the environment first — whichever tab is showing is the one you're editing.
Then use the Secret or Variable button. The choice is the first thing you make, not a field you fill in later, though the dialog does let you switch before saving. Enter a name and a value, and save.
| Secret | Variable | |
|---|---|---|
| After saving | Never viewable again | Always visible |
| Changing it | Overwrite, or delete and re-add | Overwrite |
| Carried over when you publish | No | Yes, if you select it |
| Use for | API keys, tokens, signing keys | Log levels, feature flags, endpoints, IDs |
The rule of thumb: if seeing the value in a screenshot would matter, it's a Secret.
Reading them in your module
Bindings arrive as the Worker's env — the second argument to fetch. Pass it through to your
handlers, and both lanes are readable side by side:
import { createToolContext, getRequiredSecret } from '@odel/module-sdk';
interface Env {
SERVICE_API_KEY: string; // your binding
LOG_LEVEL: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const server = buildServer(env); // capture env, hand it to your handlers
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
return transport.handleRequest(request);
}
};
// inside a tool handler:
const ctx = createToolContext<Env>(extra, env);
ctx.env.SERVICE_API_KEY; // YOURS — identical on every call, from the portal
getRequiredSecret(extra, 'RESEND_API_KEY'); // THEIRS — this caller's own, from this request
ctx.userId; // who "theirs" refers toThe two lines look alike and mean opposite things. ctx.env is a value you decided at deploy
time; getRequiredSecret(extra, …) is a value this particular user configured, fetched for this
particular request.
They never cross over
A binding is not visible to getRequiredSecret and a user's secret is never on ctx.env. Reach
for the wrong one and you get undefined or a thrown MISSING_SECRET — never the other lane's
value, and never a silent fallback.
Dev, production, and publishing
Deployments from the portal go to your dev worker, and the Inspector calls that one. Publishing promotes the module to production, which is a different worker with its own bindings.
The publish dialog handles the two kinds differently, because Cloudflare will not hand back a secret's value once it's set:
- Variables are listed for you to select, and the ones you pick are copied to production.
- Secrets cannot be copied. If any exist in dev but not production, the dialog names them and warns you, and you add them on the Deployments tab afterwards.
That warning is the last chance to notice. A module published with a secret missing in production deploys fine and fails at the first call.