Odel

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'sYours
Who provides the valueEach user, when they connectYou, once
Varies by callerAlwaysNever — one value for everyone
When it's resolvedFetched for each requestAttached at deploy, fixed until you change it
Where it's declaredRequired Secrets, Overview tab
Where the value is setThe user's install form, or their vaultSecrets & Variables, Deployments tab
Separate dev and prodNo — one value per userYes — two sets, configured separately
How you read itgetRequiredSecret(extra, 'NAME')ctx.env.NAME
What it's forBring-your-own credentials, and per-user configYour 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.

SecretVariable
After savingNever viewable againAlways visible
Changing itOverwrite, or delete and re-addOverwrite
Carried over when you publishNoYes, if you select it
Use forAPI keys, tokens, signing keysLog 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 to

The 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.

On this page