Auto-generated by
scripts/gen-env-docs.ts. Do not edit by hand — change the zod schema inapps/api/src/config/schema.tsor the comment in.env.exampleand re-runbun run gen:env-docs. CI verifies the generated file is up to date.
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
APP_DISPLAY_NAME |
string | Lode Server |
no | Human-readable display name. Shown in HTML title, TOTP issuer, etc. |
APP_NAME |
string | lode-server |
no | Application slug — must match /^[a-z][a-z0-9-]*$/. Used as the backup filename prefix, localStorage namespace, and (via VITE_APP_NAME) the frontend build. |
APP_URL |
url | — | no | Public URL used to build OAuth callback URLs. REQUIRED in production: forwarded request headers are not trusted there. Optional in development: falls back to the inbound request's Host header. |
AUDIT_RETENTION_DAYS |
number | 0 |
no | Audit retention in days. 0 = keep forever. The schema default is 0 to preserve "never sweep" semantics for the literal value, but production deployments should set a finite value so audit_events does not grow unbounded. 365 (one year) is a reasonable starting point. |
BACKUP_EXPORT_MIN_INTERVAL_SECONDS |
number | 300 |
no | Minimum seconds between successful service-token backup exports. Throttles a leaked backup token from being turned into a DOS lever (repeated full-DB reads amplify WAL pressure). 0 disables the gate. |
BASE_PATH |
string | — | no | URL prefix the app is mounted under. Unset (default) means the app is served at root: SPA at "/" and API at "/api". When set, "app", "/app", and "/app/" all normalise to "/app" — SPA at "/app/", API at "/app/api". Match this with the reverse-proxy mount. |
CORS_ORIGIN |
string | — | no | CORS (required in production). Comma-separated list of allowed origins. In development, requests through the dev URL router (nsl) are same-origin and do not need this set. |
CREATE_RATE_LIMIT_EXEMPT |
string | — | no | Resources to leave unthrottled, comma-separated. Names are the singular collection names derived from the route table — group, job, member, resource-group, token, totp, tuple. |
CREATE_RATE_LIMIT_PER_HOUR |
number | 600 |
no | |
CREATE_RATE_LIMIT_PER_MINUTE |
number | 60 |
no | Per-user caps on content creation. The limiter finds the creating routes itself: a POST whose path also answers GET is creating a member of that collection, so a new module is covered as soon as it mounts. Counted per resource — group, member, token, tuple, ... — so a burst of tuples does not spend the budget for creating groups. The minute window bounds a burst; the hour window bounds the sustained rate a burst limit alone would still allow. Rejected requests keep counting, so sustained abuse escalates out of the minute window into the hour one. The counters live in the rate_limits table rather than in memory, so they survive a restart — and an eviction on runtimes that evict the app when idle. Either at 0 disables that window; both at 0 disables the gate. |
CRON_ACTIONS_ENABLED |
string | — | no | Opt-in list (comma-separated) of cron actions whose spec.defaultEnabled is false. Each entry is matched against an action's spec.name; listed actions are registered and surfaced in the catalog, otherwise they stay unregistered (and jobs referencing them fail with UNKNOWN_ACTION). Actions shipped with defaultEnabled: true (the common case) are always registered regardless of this list. The built-in shell action ships with defaultEnabled: false because it runs sh -c <command> with the API process's UID and has no sandbox — a compromise of one admin account would otherwise pivot to host RCE without an operator opt-in. To enable: CRON_ACTIONS_ENABLED=shell (add more names comma-separated as you ship custom actions that follow the same opt-in pattern). |
CRON_ENABLED |
enum(true,false) | false |
no | Cron scheduler gate. When false (default) the Baker timer is NOT started and the built-in log-cleanup default is NOT auto-seeded. Admins can still create / edit / delete cron jobs through the UI and fire manual triggers — they just won't run on schedule until this is set to true and the API is restarted. The UI shows an amber "scheduler is off" banner while the flag is false. See docs/modules/cron.md for the runtime model + shell-action security notes before turning this on. Refused at boot on a runtime without a resident event loop (Cloudflare Workers) — see develop/runtime.md. |
DATA_DIR |
string | — | no | Top-level directory for persistent data (DB, uploads, logs). Anchors the relative paths below. Resolution order mirrors the lode SDK's dataDir() (DATA_DIR > LODE_DIR > ROOT_DIR), but the lode/root dirs are used as a base with a data/ subdir (e.g. ${LODE_DIR}/data); DATA_DIR, when set, is the data dir itself. An absolute DB_PATH / LOG_FILE / FILE_STORAGE_LOCAL_ROOT still overrides individually. |
DB_ENCRYPTION |
enum(true,false) | false |
no | Database-at-rest encryption. Defaults to false for a friction-free dev experience. Turn on (DB_ENCRYPTION=true) before any production deploy — the setup flow then derives a master keypair and DEK. See docs/develop/deployment.md and docs/modules/encryption.md. |
DB_PATH |
string | data/db/app.db |
no | Database (relative to DATA_DIR, or absolute) |
DEFAULT_ADMIN |
string | — | no | Comma-separated identities that are always admins. A matching login is granted admin on its first login and on every later one, whether or not other admins exist. Entries containing "@" match only an email the IdP marks verified; other entries match the username. The setting grants and never revokes, so demoting a listed admin in the UI lasts only until their next login — remove them from this list instead. |
ENABLE_EXPERIMENTAL_DEK_ROTATION |
enum(true,false) | false |
no | Opt-in flag for the experimental DEK-rotation flow. When false (default) the rotation endpoints respond with 501 Not Implemented. |
FILE_GC_INTERVAL_SECONDS |
number | 3600 |
no | Sweep interval for the GC. 0 disables the periodic sweep entirely (orphans accumulate; admin runs a manual sweep via the API). |
FILE_GC_MODE |
enum(async,sync) | async |
no | GC mode. async (default) — releases only decrement ref_count; a background sweeper deletes the blob + the files row once per FILE_GC_INTERVAL_SECONDS. sync — foreground request also deletes the blob (used by tests and local-only deployments). |
FILE_PRESIGN_ENABLED |
enum(true,false) | true |
no | When true and the active driver implements presignDownload, file downloads 302 to a short-lived signed URL instead of streaming through the API. Permission is enforced at signing time via the consumer hook. |
FILE_PRESIGN_TTL_SECONDS |
number | 300 |
no | TTL for signed URLs in seconds. Short by design: a leaked URL stays valid only briefly; re-issuing requires the consumer permission hook to pass again. |
FILE_STORAGE_DRIVER |
string | local |
no | Active storage backend. Built-in: local. Downstream projects can register additional drivers (e.g. s3, azure-blob) and switch here. |
FILE_STORAGE_LOCAL_ROOT |
string | data/uploads/files |
no | On-disk root for the local driver. Resolved against the project root when relative. |
HOME_PAGE |
enum(off,base,root) | off |
no | Where the SPA's home page (apps/web/src/app/routes/-home.tsx) lives. off — none; "/" goes to the app's overview (default) base — at ${BASE_PATH}/, following the base path root — at the origin's "/", outside BASE_PATH Without a BASE_PATH, base and root are the same. With root and a BASE_PATH, the home page is the only SPA page outside the prefix. |
HOST |
string | 0.0.0.0 |
no | Bind address. 0.0.0.0 is fine inside containers / behind a reverse proxy; restrict to 127.0.0.1 for host-local deployments. |
HTTP_ACTION_ALLOW_PRIVATE |
enum(true,false) | false |
no | Allow the http-request cron action to reach private / loopback / link-local destinations (127/8, 10/8, 172.16/12, 192.168/16, 169.254/16, ::1, fc00::/7). Default false to block SSRF-style pivots into cloud metadata endpoints and internal services from a compromised admin session. Enable for legitimate internal pings (e.g. talking to a sidecar on localhost). |
HTTP_ACTION_TIMEOUT_SECONDS |
number | 30 |
no | Per-execution timeout (seconds) for the http-request cron action. Fetch is aborted via AbortSignal.timeout after this many seconds. |
LOCAL_LOGIN |
enum(true,false) | false |
no | Let admins add users in the admin UI (Users → Add user) and let them sign in with a username and password, so the deployment does not depend on an identity provider. Unlike single-user mode it hides nothing: with OIDC configured the login page offers both. Passwords are stored as PBKDF2 hashes; the username locks after repeated failures; two-step verification applies. An OIDC login never takes over a password account by username — only a verified email the IdP vouches for links the two. Ignored while SINGLE_USER_MODE is on. |
LOG_FILE |
string | data/logs/app.log |
no | Path the pino destination writes to when LOG_TO_STDOUT=false. Resolved against the project root when relative. Honours SIGHUP for logrotate hand-off (see docs/develop/operations.md). |
LOG_LEVEL |
enum(debug,info,warn,error) | info |
no | Logging |
LOG_TO_STDOUT |
enum(true,false) | false |
no | When true, write JSON log lines to stdout instead of LOG_FILE — preferred for container deployments that capture stdout/stderr at the runtime level. |
MASTER_PASSWORD_FILE |
string | — | no | Path to a single-use file containing the master password for unattended unlock. Mode must be 0600. The API reads the file at boot, POSTs to /api/encryption/unlock on loopback, then deletes the file. See docs/develop/operations.md § Sealed-file unlock. |
MAX_ATTACHMENTS_PER_RESOURCE |
number | 20 |
no | Per-resource attachment count cap. Defaults to 20. |
MAX_UPLOAD_BYTES |
number | 10485760 |
no | Per-file cap (bytes). Defaults to 10 MB. Applies to every upload-capable module. |
NODE_ENV |
enum(development,production,test) | development |
no | Deployment lifecycle. development enables hot dev paths; production turns on the stricter boot guards (CORS_ORIGIN + APP_URL required, example-sentinel refusals); test is reserved for the e2e harness. |
OAUTH_AUTHORIZE_URL |
url | — | no | Explicit endpoint overrides — only needed when the IdP does not expose OIDC discovery. Each must be a full HTTPS URL. Authorization endpoint the browser is redirected to to start login. |
OAUTH_CLIENT_ID |
string | — | no | OAuth/OIDC is read from environment variables at runtime, not from the settings database. Use OAUTH_ISSUER for discovery or provide all endpoints. Client id registered at the IdP for this deployment. |
OAUTH_CLIENT_SECRET |
string | — | no | Client secret. Keep out of public configuration management; pull from a secrets store and inject as env var at runtime. |
OAUTH_GROUPS_CLAIM |
string | — | no | Claim carrying the user's group names (userinfo first, then the id_token). When set, every login makes the user's memberships in IdP-managed groups match the claim: missing groups are created as IdP-managed, and the user leaves IdP groups no longer listed. Admin-created (local) groups are never touched, and IdP-managed groups refuse manual member edits and renames. A login whose tokens lack the claim leaves memberships unchanged. |
OAUTH_ISSUER |
url | — | no | OIDC issuer URL — when set the API hits <issuer>/.well-known/openid- configuration and auto-fills the four endpoints below. Production discovery hits are cached next to the DB for boot-time fallback. |
OAUTH_PKCE |
enum(true,false) | true |
no | Authorization Code with PKCE. Leave true unless the IdP forbids PKCE. |
OAUTH_SCOPES |
string | openid profile email |
no | Space-separated scopes requested at login. Add the scope your IdP gates a groups claim behind (often groups) when using OAUTH_GROUPS_CLAIM. |
OAUTH_TOKEN_URL |
url | — | no | Token endpoint the server posts to to exchange the code for tokens. |
OAUTH_USERINFO_URL |
url | — | no | Userinfo endpoint the server calls with the access token to load claims used to upsert the user row. |
OIDC_LOGOUT_URL |
url | — | no | RP-Initiated Logout endpoint. When set the /account/auth/logout flow redirects the browser here after revoking the local session so the IdP also tears down its SSO cookie. |
PORT |
number | 3000 |
no | Port the API listens on. The bundled SPA is served from the same port. |
RAW_API_RATE_LIMIT_PER_MINUTE |
number | 120 |
no | Per-client cap on the raw API (${BASE_PATH}/api/raw/*), the bare surface other services integrate with. That surface carries no security headers, CSRF guard, CORS policy or session middleware; this limit is the only protection applied to every route on it. Counted per IP in a one-minute window. 0 = unlimited. |
REGISTRY_BLOB_DRIVER |
enum(local,r2) | local |
no | Where asset bytes live. local writes under REGISTRY_BLOB_LOCAL_ROOT (Bun); r2 uses the PACKAGES bucket binding (Cloudflare Workers). |
REGISTRY_BLOB_LOCAL_ROOT |
string | data/uploads/packages |
no | On-disk root for the local blob driver. |
REGISTRY_MANIFEST_MAX_VERSIONS |
number | 50 |
no | How many published versions a package manifest lists, newest first. Versions a channel points at are always listed. |
REGISTRY_MAX_ASSET_BYTES |
number | 524288000 |
no | Largest single asset in bytes. 500 MiB is both the default and the ceiling; a larger value is refused at boot. |
REGISTRY_MAX_PACKAGES_PER_USER |
number | 50 |
no | Platform default for how many packages one user may own. An administrator can override it per user. |
REGISTRY_MAX_STORAGE_BYTES_PER_USER |
number | 10737418240 |
no | Platform default for the total asset bytes one user may store (10 GiB), counting uploads in flight. |
REGISTRY_UPLOAD_SWEEP_INTERVAL_SECONDS |
number | 3600 |
no | How often expired multipart uploads are aborted, in seconds. 0 disables the sweep. |
REGISTRY_UPLOAD_TTL_SECONDS |
number | 86400 |
no | Lifetime in seconds of a multipart upload that is never completed. |
SERVICE_TOKEN_BACKUP |
string | — | no | Bearer token authorising backup export over POST /api/backup/export-via-token. Keep distinct from SERVICE_TOKEN_METRICS so a leaked scraper credential cannot also exfiltrate the database. |
SERVICE_TOKEN_METRICS |
string | — | no | Bearer credentials for tooling that cannot use a session cookie. Each scope is independent so a leaked metrics scraper credential cannot also exfiltrate the database. Each value must be ≥ 32 chars; generate with: openssl rand -hex 32 Bearer token authorising scrapes of /api/metrics (read-only, Prometheus). |
SESSION_MAX_AGE |
number | 86400 |
no | Session lifetime in seconds; seeded into the settings table on first run. |
SHELL_ACTION_TIMEOUT_SECONDS |
number | 300 |
no | Per-execution timeout (seconds) for the shell cron action. Child process is killed via SIGTERM (then SIGKILL after a 5s grace). |
SINGLE_USER_EMAIL |
string | — | no | Optional email used for audit / display (defaults to @local). |
SINGLE_USER_MODE |
enum(true,false) | false |
no | Enable to authenticate against a username + password kept in the env instead of an OIDC provider. Registration and password change are not exposed; rotate by editing these variables and restarting the API. When SINGLE_USER_MODE=true the OAuth /login route is disabled and the SPA renders a local form. |
SINGLE_USER_NAME |
string | — | no | Optional display name shown in the UI (defaults to the username). |
SINGLE_USER_PASSWORD_HASH |
string | — | no | Inline alternative when a file is not practical. Every $ in the hash must be backslash-escaped, e.g. |
SINGLE_USER_PASSWORD_HASH_FILE |
string | — | no | Path to a file containing the password hash. Recommended over the inline variable below because Bun's dotenv parser expands $VAR (even inside '...'), so an inline hash needs every $ backslash-escaped. Accepted hash formats (auto-detected by prefix): bcrypt (user:hash is recognised and the leading user: is stripped. Easiest one-liner: mkdir -p .secret && htpasswd -B -n -b admin 'my-password' > .secret/admin.hash |
SINGLE_USER_USERNAME |
string | — | no | Username for the single-user account. Compared case-insensitively. |
TRUST_PROXY |
enum(true,false) | false |
no | When true, honour the rightmost X-Forwarded-For entry (and, as a fallback only, X-Real-IP) for client-IP resolution. Default is false: forwarding headers are ignored and the connection peer IP is used. Only enable behind a sanitising proxy that strips client-supplied forwarding headers. |
TRUSTED_PROXY_IPS |
string | — | no | Comma-separated CIDR allow-list of immediate proxy peer addresses. Forwarding headers are honoured only when the TCP peer matches one of these ranges. Empty (default) means "any peer is trusted". Recommended in production: set this to the load-balancer / ingress subnet. |
UPLOADS_TOTAL_BYTES |
number | 0 |
no | Total disk quota (bytes) across all attachment tables. 0 = unlimited. When set, uploads beyond it return 413 QUOTA_EXCEEDED. |
These are read directly from the environment, not through loadConfig,
so they have no schema type or default.
| Variable | Read by | Description |
|---|---|---|
LODE_DIR |
apps/api/src/config.ts |
Set by the lode supervisor to the instance directory it manages; data then defaults to ${LODE_DIR}/data. Not normally set by hand. |
ROOT_DIR |
apps/api/src/root.ts |
Install root: where the bundled SPA (dist/) and migrations (drizzle/) are found, and the last fallback base for DATA_DIR. Detected from the layout when unset (the packaged artifact's directory, or the monorepo root in development); set it only to run from somewhere else. Read before config is parsed. |