Environment variables

Configure secrets and vars — .dev.vars locally, wrangler secrets in production, env bindings inside functions.

Last updated:

A Lunora app reads its configuration (API keys, signing secrets, allowlists) from the Cloudflare Worker's env. There are two kinds of value:

KindWhere it livesVisible in the dashboardUse for
varvars block in wrangler.jsoncyes (plain text)non-secret config, feature flags
secretwrangler secret put (encrypted, write-only)noAPI keys, signing secrets

Both surface the same way at runtime, as properties on env. The only difference is how you set them and whether the value is encrypted at rest. Use a secret for anything sensitive; use a var for non-secret config you're happy to see in plain text.

Locally — .dev.vars

Local secrets and vars live in .dev.vars (a dotenv-style file), which wrangler dev and the Vite dev server load automatically. .dev.vars is gitignored — never commit it. Instead commit a .dev.vars.example listing the keys your worker needs, with placeholder values:

# .dev.vars.example
AUTH_SECRET="replace-with-openssl-rand-hex-32"
AUTH_URL="http://localhost:5173"
STORAGE_SECRET="replace-with-openssl-rand-hex-32"

Auto-scaffolding

When you run lunora dev (or start Vite) without a .dev.vars, @lunora/config offers to generate one from the example: secret-looking placeholders (*_SECRET, *_TOKEN, …) are filled with fresh random values, everything else is copied verbatim. If .dev.vars exists but is missing keys the example lists (say, you just enabled an addon), it offers to append just those. Non-interactive runs (CI) skip the prompt.

You can also manage keys by hand:

lunora env set RESEND_API_KEY "re_..."   # write a single key
lunora env list                          # show configured keys
lunora env doctor                        # check .dev.vars against its example

lunora env doctor reports missing keys, still-unset placeholders, and stray extras, and exits non-zero when anything is actionable, so it works as a CI gate or a pre-dev sanity check.

In production — secrets and vars

Set secrets with wrangler secret put (you'll be prompted for the value; it's encrypted and never echoed back):

wrangler secret put AUTH_SECRET
wrangler secret put RESEND_API_KEY
wrangler secret put STORAGE_SECRET

Or push everything from your local .dev.vars at once:

lunora env push --yes

Set non-secret vars declaratively in wrangler.jsonc; they ship with the Worker and are visible in the dashboard:

{
    // ...
    "vars": {
        "AUTH_URL": "https://app.example.com",
        "LOG_LEVEL": "info",
    },
}

This is exactly how, for example, the security layer reads its CORS allowlist from config without a code change: set LUNORA_ALLOWED_ORIGINS as a var and the worker picks it up at request time (see Security).

Lunora never logs secret values, and the wrangler-validator plugin warns when a binding marked secret: true in your schema is missing from your secret list, so a forgotten wrangler secret put is a build-time warning, not a production 500.

Accessing env inside your app

Cloudflare hands the Worker its bindings as the env object. In Lunora you wire configuration from env at the worker entry (into createWorker, the generated DO factory thunks, and adapter setup) rather than reading globals inside handlers:

// src/server/index.ts
import { createAuth } from "@lunora/auth";

const auth = createAuth({
    secret: env.AUTH_SECRET, // a secret from `wrangler secret put`
    baseURL: env.AUTH_URL, // a var from wrangler.jsonc
    providers: [providers.github({ clientId: env.GH_CLIENT_ID, clientSecret: env.GH_CLIENT_SECRET })],
});

The same env flows into the binding thunks the generated factories accept, for example a Vectorize binding (env) => ({ "docs-body": env.DOCS_BODY }) or a KV namespace createKv({ namespace: env.KV }). This keeps every secret read in one place (the entry), so a handler never reaches for a global and your functions stay testable against the in-memory harness.

Adapters built on this env plumbing then expose the live capability on the function ctx (ctx.vectors, ctx.ai, ctx.storage, and so on), so your queries/mutations/actions consume configured services without touching raw bindings themselves.

The rule: never commit secrets

  • .dev.vars is gitignored. Commit .dev.vars.example (placeholders only).
  • Production secrets go through wrangler secret put (encrypted), not into wrangler.jsonc's vars (plain text, committed).
  • Run lunora env doctor in CI to catch a checked-in placeholder or a missing key before it ships.

See also

  • Deployment — the full secrets-and-deploy flow
  • Security — env-driven CORS / CSRF configuration
  • Testing — why keeping env at the entry keeps handlers testable
  • @lunora/config — the .dev.vars grammar, scaffolder, and wrangler validator