Guides
GuideEnvironment Variables

Environment Variables

Securely store API keys, database URLs, and secrets. Encrypted at rest and only available to your backend functions.

Site vs Global Secrets — two stores, on purpose

There are two places a secret can live, matching the two kinds of functions:

Site variablesGlobal secrets
Belong toone siteyour whole account
Read bythat site's backend functionsaccount functions (Automations → Functions)
Set inthe site's Config tabAutomations → Secrets (or ask the AI)
Typical useper-site keys — each client site's own Stripe key, CRM tokenaccount-wide keys — your AI key, internal tools with no site
Duplicate namesadding a name that already exists is refused — you edit the existing entry to rotate its value, so a working key is never silently replaced

Why two stores, and no automatic fallback? Two reasons. First, the same name often needs different values per site — an agency running three client sites has three different STRIPE_SECRET_KEYs, and a global-only store couldn't express that. Second, explicit beats clever with secrets: a site function can never silently read an account-wide key, and a global automation can never silently pick up some site's value. Each function reads exactly one store — the one matching its scope — so there are no surprises about which secret was used. If a value is genuinely needed in both worlds, you set it in both, deliberately.

The same split explains the two kinds of functions: site backend functions belong to one site — they run with that site's data, members, and secrets, and its pages call them. Account functions belong to you — they span sites (or need no site at all) and are triggered by webhooks, email triggers, and schedules. See Account Functions & Webhooks.

Site Variables — create & use

A site variable belongs to one site and is read by that site's backend functions — the functions its pages call. To create one:

  1. Go to your app's Config tab in the dashboard
  2. Scroll to the Environment Variables section
  3. Enter a name (uppercase with underscores, e.g., STRIPE_SECRET_KEY)
  4. Enter the value (your API key, secret, etc.)
  5. Click Add

Variable names must be alphanumeric with underscores only (e.g., MY_API_KEY). Values are masked in the UI after saving.

Then read it inside the site's backend functions with ctx.env.get():

Which secrets belong here? Env vars are readable by any code in your app (that is the point — your function needs the key). For an email mailbox use Mail Accounts and for a connected integration use connections: in both, the platform performs the privileged action and the credential never enters your function at all.

// Get an env var by name — ctx.env.get returns { value }
const value = await ctx.env.get('STRIPE_SECRET_KEY');

// A function can also WRITE its own config. Write-only, this site only, and
// the name must start with APP_ (so app code can never overwrite your keys).
await ctx.env.set('APP_ONBOARDING_STAGE', 'complete');

const result = await ctx.fetch('https://api.stripe.com/v1/charges', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${value}` },
  body: JSON.stringify({ amount: 1000, currency: 'usd' }),
});

Global Secrets — create & use

A global secret belongs to your whole account and is read by your account functions — the ones triggered by email triggers, webhooks, and schedules, which may have no site at all. To create one:

  1. Go to Automations → Secrets in the dashboard (or just tell the AI: “save my Anthropic key as a global secret”)
  2. Click Add secret, enter the name (e.g. ANTHROPIC_API_KEY) and paste the value
  3. To change a value later, use the edit (pencil) action — adding a second secret with the same name is refused, so a working key is never replaced by accident

Account functions read it with exactly the same syntax site functions use for their own store:

// In an ACCOUNT function (e.g. one an email trigger runs):
const value = await ctx.env.get('ANTHROPIC_API_KEY');

// …or as a {{placeholder}} — resolved before the request is sent:
const res = await ctx.fetch('https://api.anthropic.com/v1/messages', {
  method: 'POST',
  headers: { 'x-api-key': '{{ANTHROPIC_API_KEY}}', 'anthropic-version': '2023-06-01' },
  body: JSON.stringify({ model: 'claude-sonnet-5', max_tokens: 300,
    messages: [{ role: 'user', content: 'Classify this email…' }] }),
});

// Account functions can also store their own state — code may only write
// APP_-prefixed names, so it can never overwrite your operator-set secrets:
await ctx.env.set('APP_LAST_SYNC_AT', new Date().toISOString());

Same function code, different store: ctx.env.get() in a site function reads that site's variables; in an account function it reads your global secrets. A function never has to say which store — its scope decides, and the two never mix.

Placeholder Syntax (both stores)

In any function — site or account — you can use {{VAR_NAME}} placeholders directly in ctx.fetch URLs, headers and bodies. They resolve from the function's own store (site variables in site functions, global secrets in account functions) before the request is sent.

// Placeholders are resolved automatically
const result = await ctx.fetch('{{API_BASE_URL}}/users', {
  headers: {
    'Authorization': 'Bearer {{API_KEY}}',
    'X-Custom-Header': '{{CUSTOM_VALUE}}',
  },
});

Placeholders work in the URL, headers, and body of ctx.fetch calls. They're resolved recursively in nested objects.

Where should a secret live?

The secret is…Put it inWho can write itWho can use it
A key YOU manage (Anthropic, Stripe…)Site Config or Automations → Secretsyou (dashboard or your AI) — never app codeyour functions (read-only)
A value the APP manages at runtimeAPP_* env name via ctx.env.setfunction codefunction code
Sensitive data INSIDE a record (a customer's token, ID number)a table secret fieldwhoever may edit the rownobody programmatically — masked everywhere, reveal-on-demand for authorized people
An email mailbox passwordMail Accounts (Automations → Email)younobody — the platform sends on your behalf; code names the mailbox, never sees the password

The rule behind the whole table: code can never overwrite a secret a human set (function writes are limited to the APP_* namespace at both scopes), and the most protected stores are the ones where the value never enters code at all.

Security (both stores)

AES-256-GCM encryption at rest

Values are encrypted before storage and only decrypted when your backend function executes.

Backend-only by default

Every variable created from the dashboard's Add Variable form is marked secret — available only inside backend functions via ctx.env.get(), never sent to the browser or included in the published bundle. A variable can only be made non-secret (baked into the client bundle at build time) programmatically via isSecret: false — don't do this for anything sensitive.

Masked in UI

After saving, values show only the first 2 and last 2 characters. You can update or delete, but not view the full value.

Tip: Don't use Deno.env or process.env in your functions — they're blocked by the sandbox. Always use ctx.env.get() instead.