Environment Variables
How environment variables work on mctx. Server-level and version-level variables, encryption, runtime injection, and security best practices.
Your server probably needs API keys, database URLs, or other secrets. Here's how to store them securely and use them in your code.
mctx encrypts environment variables at rest and injects them into your server at deploy time. You never check secrets into your repository.
A Real Example
Say your weather tool needs an OpenWeatherMap API key. Here's the full flow:
1. Add the variable in the dashboard. Go to your server page at /dev/servers/[id], scroll to Server Environment Variables, and add:
| Key | Value |
|---|---|
OPENWEATHER_API_KEY | abc123your-key |
Click Add, then Save Environment Variables. mctx encrypts and stores the value, then redeploys your server with the new variable.
2. Use it in code. Your variable is available on the env parameter:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=London&appid=${env.OPENWEATHER_API_KEY}`,
);
// ...
},
};That's it. The variable is encrypted in storage, decrypted at deploy time, and available to your code at runtime.
Server Variables vs. Version Variables
mctx gives you two levels of environment variables:
Server variables apply to every version you deploy. Use these for secrets that don't change between versions -- API keys, database credentials, webhook secrets.
Version variables apply to a single version and override server variables when keys match. Use these when you need different configuration for different versions.
| Level | Applies to | Precedence | When to use |
|---|---|---|---|
| Server | All versions | Lower | API keys, shared credentials |
| Version | Specific version | Higher | Version-specific config changes |
Example: Migrating to a New API
You're upgrading from OpenWeatherMap v2 to v3. The new API uses a different key:
- Server variable:
OPENWEATHER_API_KEY= your v2 key (used by v1.0.0) - Version variable on v2.0.0:
OPENWEATHER_API_KEY= your v3 key (overrides the server variable)
Both versions run simultaneously, each with the right credentials.
Setting Variables
Environment variables are injected at deploy time, not at runtime. Changing a variable requires redeployment.
Server-level
- Go to your server detail page (
/dev/servers/[id]) - Scroll to Server Environment Variables
- Enter key and value
- Click Add
- Click Save Environment Variables
Saving triggers automatic redeployment of all live versions with the new values.
Version-level
- Go to your server detail page
- Select a version from Version History
- Scroll to Version Environment Variables
- Enter key and value
- Click Save Version Variables
Using Variables in Code
Variables arrive on the env parameter of your Cloudflare Workers handler. Define a TypeScript interface that matches your dashboard variables for type safety:
interface Env {
OPENWEATHER_API_KEY: string;
DATABASE_URL: string;
MAX_RETRIES?: string; // optional -- all values are strings
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const apiKey = env.OPENWEATHER_API_KEY;
const maxRetries = env.MAX_RETRIES ? parseInt(env.MAX_RETRIES, 10) : 3;
if (!apiKey) {
return new Response("Server misconfigured", { status: 500 });
}
// Use your variables
const response = await fetch("https://api.openweathermap.org/...", {
headers: { Authorization: `Bearer ${apiKey}` },
});
// ...
},
};All values are strings. Parse numbers and booleans yourself. Always handle the case where a variable might be missing.
Reading Variables with process.env
If you are used to writing Node.js code, you may reach for process.env. That works too — mctx enables nodejs_compat mode for every tenant worker, which makes process.env available alongside the env parameter.
Both patterns access the same variables you set in the dashboard:
// via the env parameter (Cloudflare Workers style)
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const apiKey = env.OPENWEATHER_API_KEY;
// ...
},
};// via process.env (Node.js style)
export default {
async fetch(request: Request): Promise<Response> {
const apiKey = process.env.OPENWEATHER_API_KEY;
// ...
},
};Choose whichever style fits your codebase. The env parameter gives you type safety when you define an Env interface. process.env is familiar if you are migrating existing Node.js code.
The module-scope gotcha
Read environment variables lazily. Variables read at module scope — outside any function — will be empty strings. Read them inside request handlers or on first use.
Here is the pattern that breaks silently:
// WRONG — module scope, runs before request context is available
const apiKey = process.env.OPENWEATHER_API_KEY; // "" — always empty
export default {
async fetch(request: Request): Promise<Response> {
// apiKey is "" here, even though the variable is set in the dashboard
const response = await fetch(`https://api.example.com?key=${apiKey}`);
// ...
},
};And the correct pattern:
// CORRECT — read inside the handler, runs during request handling
export default {
async fetch(request: Request): Promise<Response> {
const apiKey = process.env.OPENWEATHER_API_KEY; // correct value here
const response = await fetch(`https://api.example.com?key=${apiKey}`);
// ...
},
};Why this happens: Cloudflare Workers evaluates your module code before any request arrives. At that point the runtime has not yet made environment variables available. Only when a request comes in does the runtime inject variables into the execution context. Code at module scope runs too early.
The env parameter avoids this problem entirely because the handler function only runs during a request. But if you use process.env, the same rule applies: read inside a function, not at the top level of the module.
Module-scope I/O
The same restriction applies to any I/O at module load time. Do not make HTTP calls, database queries, or async initialization at module scope:
// WRONG — HTTP call at module scope
const config = await fetch("https://api.example.com/config").then((r) => r.json());
export default {
async fetch(request: Request): Promise<Response> {
// config may be stale or fail silently
},
};Use lazy initialization instead — initialize on the first request and cache for subsequent ones:
// CORRECT — lazy initialization
let config: Config | null = null;
async function getConfig(): Promise<Config> {
if (!config) {
config = await fetch("https://api.example.com/config").then((r) => r.json());
}
return config;
}
export default {
async fetch(request: Request): Promise<Response> {
const cfg = await getConfig(); // initialized on first request, cached after
// ...
},
};Naming Rules
Variable keys must:
- Start with a letter (A-Z)
- Contain only uppercase letters, digits, and underscores
- Be at most 64 characters
Valid: API_KEY, DATABASE_URL, OPENAI_API_KEY, WEBHOOK_SECRET_2024
Invalid: api_key (lowercase), 123_KEY (starts with number), API-KEY (hyphen), my.api.key (dots)
Reserved Keys and Prefixes
Using a reserved key or prefix causes a validation error when saving. The dashboard shows "Key X is reserved" or "Keys starting with X are reserved" — rename your variable to something else.
The following keys are reserved and cannot be used:
| Key | Category |
|---|---|
ASSETS | Platform namespace — static asset binding used by Cloudflare Workers |
NODE_ENV | System variable — runtime environment identifier managed by the platform |
PATH | System variable — executable search path set by the OS |
HOME | System variable — home directory path set by the OS |
USER | System variable — current user identity set by the OS |
SHELL | System variable — active shell path set by the OS |
PWD | System variable — working directory path set by the OS |
TERM | System variable — terminal type set by the OS |
LANG | System variable — locale setting managed by the OS |
LC_ALL | System variable — locale override managed by the OS |
Keys starting with these prefixes are also reserved:
| Prefix | Category |
|---|---|
CF_ | Platform namespace — Cloudflare internal bindings |
WRANGLER_ | Platform namespace — Wrangler CLI tooling |
CLOUDFLARE_ | Platform namespace — Cloudflare SDK and tooling |
Editing and Deleting
To update a variable: Find it in the list, click Edit, enter the new value, click Set, then Save Environment Variables. You can't rename a key -- delete and recreate it instead.
To delete a variable: Find it in the list, click Delete, then Save Environment Variables.
Both actions trigger redeployment. If your code depends on a variable you just deleted, it will break at runtime. Make sure your code handles missing variables gracefully before removing them.
Security
Your secrets are handled carefully:
- Encrypted at rest using AES-256-GCM via the WebCrypto API
- Decrypted only during deployment -- plaintext values are injected into your worker at deploy time
- Never exposed in the dashboard UI (shows masked values), API responses, server logs, or error messages
- Isolated per tenant by the Cloudflare Workers runtime
You don't need to do anything extra to get this protection. It happens automatically.
External Secret Managers
mctx does not integrate with external secret managers such as AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager. All secrets are stored and managed within the mctx platform. If your workflow requires dynamic secret retrieval at runtime, fetch it from the external service inside your request handler using a service account credential stored as an mctx environment variable.
Keeping Your Code Safe
// Don't log secrets
console.log(`API Key: ${env.API_KEY}`); // bad
// Log a safe reference instead
console.log(`Using API key: ${env.API_KEY.slice(0, 4)}...`); // okCreate separate API keys per service. If one gets compromised, you rotate just that one:
env.OPENAI_API_KEY; // not env.API_KEY
env.ANTHROPIC_API_KEY; // separate key per service
env.WEATHER_API_KEY; // easy to rotate individuallyRotating a Secret
Follow this sequence to rotate a sensitive variable with minimal risk:
- Generate the new credential in the external service. Do not revoke the old one yet — both will be valid during the transition window.
- Update the variable in the mctx dashboard. Go to your server page, find the variable, click Edit, enter the new value, click Set, then Save Environment Variables.
- Wait for redeployment to finish. Your server status changes from Deploying back to Live — this typically takes 30–60 seconds. Do not revoke the old credential until this completes.
- Test that your server works with the new credential. Make a real request to your App and confirm a successful response. Check server logs if needed.
- Revoke the old credential in the external service once you have confirmed the new one is working.
Why wait before revoking: If you revoke the old credential before redeployment completes, requests handled by the old worker instance will start failing. The brief overlap keeps your server healthy throughout the rotation.
Redeployment Behavior
Server variable changes redeploy all live versions. This typically takes 30-60 seconds.
Version variable changes redeploy only that specific version.
During redeployment, your server status changes to Deploying and returns to Live when complete.
Troubleshooting
Variable is undefined in code
Your env.MY_VAR returns undefined.
- Check the key name matches exactly (case-sensitive)
- Make sure you clicked Save Environment Variables after adding it
- Wait for redeployment to finish (check your server status in the dashboard)
process.env variable is always an empty string
Your process.env.MY_VAR returns "" even though the variable is set in the dashboard.
You are reading it at module scope. Move the read inside your handler function. See The module-scope gotcha for the correct pattern.
Redeployment stuck or failed
Your server shows Deploying for too long, or shows Error.
- Check server logs for error messages
- Verify the variable value is valid for your use case (e.g., the API key actually works)
- Test credentials with the external service before adding them to mctx
Version override not working
The version is still using the server-level variable.
- Confirm the version variable was saved (not just added)
- Check you selected the right version from the version history
- Trigger a manual redeployment if needed
Value Constraints
Values are stored and delivered as strings. There are no restrictions on the characters a value may contain — Unicode, special characters, and punctuation are all accepted.
Newlines in values: Multi-line values (for example, PEM-encoded private keys) are supported. Paste the full value including newlines into the dashboard field. mctx preserves the content exactly.
Variable count: There is no documented hard limit on the number of variables per server. Keep the total payload reasonable — very large numbers of variables or very long individual values increase deploy time slightly.
| Limit | Value |
|---|---|
| Key length | 64 characters |
| Value length | 10,000 characters |
Frequently Asked Questions
Are variables inherited by child processes?
No. Cloudflare Workers does not use a traditional process model, so there are no child processes in the conventional sense. Your worker runs as an isolated V8 isolate — not a forked OS process. Environment variables are not propagated via fork()/exec() mechanics.
If you spawn a subprocess using the Node.js child_process module (available in nodejs_compat mode), environment variables from the Workers runtime are not automatically available in that subprocess. Pass the values explicitly as arguments or via its env option if needed.
For all standard request-handling code, access variables through env.MY_VAR or process.env.MY_VAR inside your handler — no inheritance mechanics are involved.
See Also
- package.json Configuration - Server metadata and entry point
- Server Requirements - Handler format and code structure
- Server Logs - Debugging at runtime
See something wrong? Report it or suggest an improvement — your feedback helps make these docs better.