Environment Variables Reference
For AI agents: All required variables must be set before starting the CMS API. Missing required variables will cause startup to fail with an explicit error message.
This page documents every environment variable and Cloudflare binding used by LumiBase.
Core runtime
| Variable | Required | Default | Description |
|---|---|---|---|
LUMIBASE_ENV | ✓ | — | Environment label: development, staging, production |
LUMIBASE_RUNTIME | Docker only | cloudflare | docker for Node.js/Docker; Cloudflare Workers infers from bindings |
JWT_SECRET | ✓ | — | Secret for signing/verifying application JWTs. Min 32 chars. For Cloudflare production, set with wrangler secret put JWT_SECRET --env production; never commit or place in [env.production.vars]. |
LUMIBASE_DEV_AUTH | Local dev only | false | Set to true to bypass Logto auth in local dev. Never enable in production; pnpm release:check fails if production resolves to true. |
LUMIBASE_REALTIME_ENABLED | ✗ | — | Not implemented. Nothing reads this yet; setting it has no effect. Tracked as the "explicit enablement" goal in realtime implementation. |
VITE_LUMIBASE_ALLOW_ADMIN_PATH_REDIRECT | Setup/debug only | false | Client-side opt-in that allows convenience redirects to the private admin path. Keep unset in production except for a temporary controlled setup/debug window. |
The admin path is not configured by an environment variable. It is chosen during the Setup Wizard and stored in the database, so it can be rotated without a redeploy — and so it never sits in a build artifact.
It is private operational state. Do not expose it through VITE_* environment
variables or client build metadata, and do not automatically redirect
public/setup routes to it in production. See
Private admin path.
First-run setup
| Variable | Required | Default | Description |
|---|---|---|---|
LUMIBASE_REQUIRE_SETUP_TOKEN | ✗ (Node/Docker only) | off | true, 1 or yes makes POST /api/v1/setup/complete demand a one-time setup token. Turn it on for any instance that someone other than you can reach before setup is finished — otherwise whoever reaches the port first creates the admin account. |
With the flag on, every process that serves HTTP (LUMIBASE_PROCESS_ROLE web or
all) mints the token as it starts, before it accepts requests, and prints it once:
[lumibase-cms] SETUP_TOKEN=<token>
Paste it into the setup wizard, or send it as setupToken to /setup/complete. Only
its SHA-256 hash is stored, and the hash is cleared when setup completes.
A restart does not print a new token: the stored hash is kept so a token you already have stays valid, and the startup log says so instead. If you lost the line, clear the hash and restart — the next start prints a fresh token:
UPDATE lumibase_system_state SET setup_token_hash = NULL WHERE id = 'singleton';
Several replicas starting at once print exactly one token between them, so read it from whichever replica printed it.
Cloudflare Workers: not supported. A Worker has no process start to mint the token
in, so with the flag on /setup/complete answers 503 SETUP_TOKEN_NOT_ISSUED with an
explanation instead of accepting a token. Leave the flag unset there and complete setup
right after deploying.
Encryption
| Variable | Required | Default | Description |
|---|---|---|---|
ENCRYPTION_KEY | Before first use | — | Base64 of 32 random bytes (openssl rand -base64 32). Wraps encrypted item fields and TOTP two-factor seeds. Resolved as key version v0. Set as a secret (wrangler secret put ENCRYPTION_KEY --env production); never commit it or place it in [env.production.vars] |
ENCRYPTION_KEY_<id> | ✗ | — | Versioned key material, e.g. ENCRYPTION_KEY_v1, used for rotation |
ENCRYPTION_ACTIVE_KEY_ID | ✗ | only configured key, else v0 | Which key version encrypts new data |
ENCRYPTION_KEY[_<id>]_FILE | ✗ | — | Path to read the key from instead of the variable (Docker secrets). The direct variable wins |
"Before first use" means before anyone enrolls in 2FA or writes an encrypted
field — not before boot. Without a key, enrollment fails closed with a 500
rather than a typed error.
Once seeds exist, a key can never be removed. TOTP seeds pin the key version that wrapped them and are not re-wrapped by the envelope migration, so retiring an old key locks those users out of their second factor. Rotation is also not a remedy for a leaked key. See Encryption key operations.
Authentication (Logto)
| Variable | Required | Description |
|---|---|---|
LOGTO_ENDPOINT | ✓ | Logto instance URL (e.g., https://your-tenant.logto.app) |
LOGTO_APP_ID | ✓ | Logto application ID |
LOGTO_APP_SECRET | ✓ | Logto application secret |
LOGTO_JWKS_URI | ✗ | Override JWKS URL (auto-derived from LOGTO_ENDPOINT if not set) |
CF_ACCESS_CERTS_URL | CF Access only | Cloudflare Access JWKS URL for admin tunnel auth |
CF_ACCESS_AUDIENCE | CF Access only | Cloudflare Access application audience tag |
Database
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Docker only | PostgreSQL connection string (e.g., postgres://user:pass@host:5432/lumibase) |
DATABASE_POOL_MIN | ✗ | Minimum DB pool connections (default: 2) |
DATABASE_POOL_MAX | ✗ | Maximum DB pool connections (default: 10) |
DATABASE_SSL | ✗ | true to require SSL for DB connection |
For Cloudflare Workers, use the HYPERDRIVE binding (see Cloudflare Bindings).
Cache
| Variable | Required | Description |
|---|---|---|
REDIS_URL | Docker only | Redis connection string (e.g., redis://localhost:6379) |
CACHE_TTL_SCHEMA | ✗ | Schema cache TTL in seconds (default: 60) |
CACHE_TTL_PERMISSIONS | ✗ | Permission cache TTL in seconds (default: 300) |
CACHE_TTL_SETTINGS | ✗ | Settings cache TTL in seconds (default: 60) |
Object storage
| Variable | Required | Description |
|---|---|---|
S3_ENDPOINT | Docker/S3 only | S3-compatible endpoint URL (e.g., MinIO: http://localhost:9000) |
S3_ACCESS_KEY_ID | Docker/S3 only | S3 access key |
S3_SECRET_ACCESS_KEY | Docker/S3 only | S3 secret key |
S3_BUCKET | Docker/S3 only | Storage bucket name (default: lumibase-media) |
S3_REGION | ✗ | S3 region (default: us-east-1) |
S3_PUBLIC_URL | ✗ | Public base URL for assets (e.g., CDN URL pointing to the bucket) |
Search (MeiliSearch)
| Variable | Required | Description |
|---|---|---|
MEILISEARCH_HOST | If search enabled | MeiliSearch instance URL, e.g. http://meilisearch:7700 |
MEILISEARCH_API_KEY | If search enabled | MeiliSearch master or search API key |
AI Copilot
| Variable | Required | Description |
|---|---|---|
LLM_PROVIDER | ✗ | LLM provider: openai, anthropic, claude, gemini, nvidia, vertex, workers-ai, echo (default: echo) |
OPENAI_API_KEY | If openai provider | OpenAI API key |
ANTHROPIC_API_KEY | If anthropic / claude provider | Anthropic API key |
GEMINI_API_KEY | If gemini provider | Google Gemini API key |
NVIDIA_API_KEY | If nvidia provider | NVIDIA hosted-inference key (build.nvidia.com / NIM). Billed by NVIDIA. |
NVIDIA_BASE_URL | ✗ | Override the NVIDIA endpoint — e.g. a self-hosted NIM container (http://nim:8000/v1). Defaults to https://integrate.api.nvidia.com/v1. |
VERTEX_ACCESS_TOKEN | If vertex provider | Google Cloud OAuth 2.0 bearer (gcloud auth print-access-token). Tokens expire (~1h). Billed to Google Cloud, not AWS. |
VERTEX_PROJECT_ID | If vertex provider | Google Cloud project id that owns the Vertex AI models. |
VERTEX_LOCATION | ✗ | Vertex AI region (default: us-central1). |
LLM_MODEL | ✗ | Model name override (e.g., gpt-4.1-nano, claude-3-5-haiku-latest, gemini-3.5-flash, meta/llama-3.1-8b-instruct) |
WORKERS_AI_GATEWAY | Workers AI only | CF Workers AI gateway URL |
Provider ↔ billing:
nvidiaandvertexcall external clouds — NVIDIA and Google Cloud respectively — so their usage is not covered by AWS credit.nvidia(or a self-hosted NIM viaNVIDIA_BASE_URL) and MeiliSearch are the pieces that pair naturally with AWS-hosted infrastructure.
Email (Resend)
| Variable | Required | Description |
|---|---|---|
RESEND_API_KEY | If email enabled | Resend API key for transactional email |
EMAIL_FROM | If email enabled | Sender address (e.g., [email protected]) |
SCIM provisioning
| Variable | Required | Description |
|---|---|---|
SCIM_TOKEN | If SCIM enabled | Bearer token for /scim/v2/ endpoint authentication |
Pressure limiter (Docker / Node.js)
The Docker CMS process includes an overload guard for the Node.js event loop. When enabled and the process is saturated, API requests fail fast with HTTP 503, a SERVICE_UNAVAILABLE JSON envelope, Retry-After, and X-Lumi-Overload instead of queueing until the container becomes unresponsive. The guard bypasses /health and /metrics by default so operators can still inspect the instance.
| Variable | Required | Default | Description |
|---|---|---|---|
LUMIBASE_PRESSURE_LIMITER_ENABLED | Docker only | true | Enables the Node.js overload guard. Set false only as a temporary mitigation while you scale or optimize the overloaded path. |
LUMIBASE_PRESSURE_LIMITER_SAMPLE_INTERVAL | ✗ | 250 | Sampling interval in milliseconds for event-loop pressure checks. |
LUMIBASE_PRESSURE_LIMITER_MAX_EVENT_LOOP_DELAY | ✗ | 1000 | Maximum observed event-loop delay in milliseconds before API traffic receives 503. Set false to disable this threshold. |
LUMIBASE_PRESSURE_LIMITER_MAX_EVENT_LOOP_UTILIZATION | ✗ | false | Optional utilization threshold such as 0.99. Disabled by default because sustained useful CPU work can otherwise produce noisy rejections. |
LUMIBASE_PRESSURE_LIMITER_RETRY_AFTER | ✗ | 5 | Seconds advertised in the Retry-After response header. |
LUMIBASE_PRESSURE_LIMITER_EXCLUDED_PATHS | ✗ | /health,/metrics | Comma-separated path prefixes that should keep serving during overload checks. |
Rate limiting
A fixed-window throttle guards the authenticated API. It keys per principal (user → API key → IP) and is scoped per site. When exceeded it returns HTTP 429 with a RATE_LIMITED envelope plus X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After. It fails open if the cache is unavailable, so it is defence-in-depth rather than a hard quota.
Large imports can trip the default budget. Raise LUMIBASE_RATE_LIMIT_MAX and prefer the bulk endpoint — see Data import.
| Variable | Required | Default | Description |
|---|---|---|---|
LUMIBASE_RATE_LIMIT_MAX | ✗ | 300 | Max requests per window per principal, per site. |
LUMIBASE_RATE_LIMIT_WINDOW_S | ✗ | 60 | Window length in seconds. |
LUMIBASE_RATE_LIMIT_DISABLED | ✗ | (unset) | Set to true to disable the throttle entirely. |
LUMIBASE_DELIVER_RATE_LIMIT | ✗ | 1200 | Max requests per minute per client IP on the public Delivery API (/api/v1/deliver/*). 0 disables. See Caching — penetration. |
LUMIBASE_NEGATIVE_CACHE_TTL | ✗ | 30 | Tombstone TTL (seconds) for confirmed absences on public read paths, before ±20% jitter. 0 disables. |
GraphQL
Every GraphQL operation is validated before it runs against a depth limit and a static cost limit, rejecting abusively deep or wide queries at parse time (CWE-770). Depth is a compile-time constant (12); cost is tunable. Raise LUMIBASE_GQL_MAX_COST if a legitimate query is rejected. See graphql-api-spec.md.
| Variable | Required | Default | Description |
|---|---|---|---|
LUMIBASE_GQL_MAX_COST | ✗ | 1000 | Max static cost accepted per operation. |
LUMIBASE_GQL_DEFAULT_LIST_SIZE | ✗ | 20 | Cost multiplier for a list field with no literal pagination argument (or a variable one). |
LUMIBASE_GQL_MAX_LIST_MULTIPLIER | ✗ | 100 | Upper clamp on any single list field's cost multiplier. |
Observability
| Variable | Required | Description |
|---|---|---|
PROMETHEUS_ENABLED | Docker only | true to expose /metrics endpoint |
LOG_LEVEL | ✗ | Log level: debug, info, warn, error (default: info) |
LOG_FORMAT | ✗ | json (default) or pretty (local dev) |
Cloudflare Bindings
Configure these in apps/cms/wrangler.toml (not environment variables):
| Binding | Type | Purpose |
|---|---|---|
HYPERDRIVE | Hyperdrive | Pooled PostgreSQL connections |
CONFIG_CACHE | KV Namespace | Schema, permission, and settings cache |
MEDIA | R2 Bucket | Media/file object storage |
SITE_ROOM | Durable Object | Per-site WebSocket coordination (realtime) |
AI | Workers AI | Workers AI binding for workers-ai LLM provider |
# wrangler.toml excerpt
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<your-hyperdrive-id>"
[[kv_namespaces]]
binding = "CONFIG_CACHE"
id = "<your-kv-namespace-id>"
[[r2_buckets]]
binding = "MEDIA"
bucket_name = "lumibase-media"
[durable_objects]
bindings = [{ name = "SITE_ROOM", class_name = "SiteRoom" }]
Security checklist
Before deploying to production, verify:
-
JWT_SECRETis at least 32 characters and stored as a Wrangler Secret or Docker secret -
LUMIBASE_DEV_AUTHis NOT set totrue - Database credentials are stored as secrets, not in
.envfiles committed to git -
S3_SECRET_ACCESS_KEYis stored as a secret -
RESEND_API_KEYis stored as a secret - CORS is configured to allow only your Studio and consumer domains