LumiBaseDocs

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

VariableRequiredDefaultDescription
LUMIBASE_ENV✓—Environment label: development, staging, production
LUMIBASE_RUNTIMEDocker onlycloudflaredocker 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_AUTHLocal dev onlyfalseSet 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_REDIRECTSetup/debug onlyfalseClient-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

VariableRequiredDefaultDescription
LUMIBASE_REQUIRE_SETUP_TOKEN✗ (Node/Docker only)offtrue, 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:

text
[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:

sql
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

VariableRequiredDefaultDescription
ENCRYPTION_KEYBefore 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 v0Which 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)

VariableRequiredDescription
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_URLCF Access onlyCloudflare Access JWKS URL for admin tunnel auth
CF_ACCESS_AUDIENCECF Access onlyCloudflare Access application audience tag

Database

VariableRequiredDescription
DATABASE_URLDocker onlyPostgreSQL 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

VariableRequiredDescription
REDIS_URLDocker onlyRedis 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

VariableRequiredDescription
S3_ENDPOINTDocker/S3 onlyS3-compatible endpoint URL (e.g., MinIO: http://localhost:9000)
S3_ACCESS_KEY_IDDocker/S3 onlyS3 access key
S3_SECRET_ACCESS_KEYDocker/S3 onlyS3 secret key
S3_BUCKETDocker/S3 onlyStorage 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)

VariableRequiredDescription
MEILISEARCH_HOSTIf search enabledMeiliSearch instance URL, e.g. http://meilisearch:7700
MEILISEARCH_API_KEYIf search enabledMeiliSearch master or search API key

AI Copilot

VariableRequiredDescription
LLM_PROVIDER✗LLM provider: openai, anthropic, claude, gemini, nvidia, vertex, workers-ai, echo (default: echo)
OPENAI_API_KEYIf openai providerOpenAI API key
ANTHROPIC_API_KEYIf anthropic / claude providerAnthropic API key
GEMINI_API_KEYIf gemini providerGoogle Gemini API key
NVIDIA_API_KEYIf nvidia providerNVIDIA 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_TOKENIf vertex providerGoogle Cloud OAuth 2.0 bearer (gcloud auth print-access-token). Tokens expire (~1h). Billed to Google Cloud, not AWS.
VERTEX_PROJECT_IDIf vertex providerGoogle 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_GATEWAYWorkers AI onlyCF Workers AI gateway URL

Provider ↔ billing: nvidia and vertex call external clouds — NVIDIA and Google Cloud respectively — so their usage is not covered by AWS credit. nvidia (or a self-hosted NIM via NVIDIA_BASE_URL) and MeiliSearch are the pieces that pair naturally with AWS-hosted infrastructure.


Email (Resend)

VariableRequiredDescription
RESEND_API_KEYIf email enabledResend API key for transactional email
EMAIL_FROMIf email enabledSender address (e.g., [email protected])

SCIM provisioning

VariableRequiredDescription
SCIM_TOKENIf SCIM enabledBearer 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.

VariableRequiredDefaultDescription
LUMIBASE_PRESSURE_LIMITER_ENABLEDDocker onlytrueEnables 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✗250Sampling interval in milliseconds for event-loop pressure checks.
LUMIBASE_PRESSURE_LIMITER_MAX_EVENT_LOOP_DELAY✗1000Maximum observed event-loop delay in milliseconds before API traffic receives 503. Set false to disable this threshold.
LUMIBASE_PRESSURE_LIMITER_MAX_EVENT_LOOP_UTILIZATION✗falseOptional utilization threshold such as 0.99. Disabled by default because sustained useful CPU work can otherwise produce noisy rejections.
LUMIBASE_PRESSURE_LIMITER_RETRY_AFTER✗5Seconds advertised in the Retry-After response header.
LUMIBASE_PRESSURE_LIMITER_EXCLUDED_PATHS✗/health,/metricsComma-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.

VariableRequiredDefaultDescription
LUMIBASE_RATE_LIMIT_MAX✗300Max requests per window per principal, per site.
LUMIBASE_RATE_LIMIT_WINDOW_S✗60Window length in seconds.
LUMIBASE_RATE_LIMIT_DISABLED✗(unset)Set to true to disable the throttle entirely.
LUMIBASE_DELIVER_RATE_LIMIT✗1200Max requests per minute per client IP on the public Delivery API (/api/v1/deliver/*). 0 disables. See Caching — penetration.
LUMIBASE_NEGATIVE_CACHE_TTL✗30Tombstone 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.

VariableRequiredDefaultDescription
LUMIBASE_GQL_MAX_COST✗1000Max static cost accepted per operation.
LUMIBASE_GQL_DEFAULT_LIST_SIZE✗20Cost multiplier for a list field with no literal pagination argument (or a variable one).
LUMIBASE_GQL_MAX_LIST_MULTIPLIER✗100Upper clamp on any single list field's cost multiplier.

Observability

VariableRequiredDescription
PROMETHEUS_ENABLEDDocker onlytrue 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):

BindingTypePurpose
HYPERDRIVEHyperdrivePooled PostgreSQL connections
CONFIG_CACHEKV NamespaceSchema, permission, and settings cache
MEDIAR2 BucketMedia/file object storage
SITE_ROOMDurable ObjectPer-site WebSocket coordination (realtime)
AIWorkers AIWorkers AI binding for workers-ai LLM provider
toml
# 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_SECRET is at least 32 characters and stored as a Wrangler Secret or Docker secret
  • LUMIBASE_DEV_AUTH is NOT set to true
  • Database credentials are stored as secrets, not in .env files committed to git
  • S3_SECRET_ACCESS_KEY is stored as a secret
  • RESEND_API_KEY is stored as a secret
  • CORS is configured to allow only your Studio and consumer domains
Last modified: 26/09/2026