Core — required at boot

These are checked in src/index.ts at boot and will throw if missing. Do not ship without them.

DATABASE_URLRequired

PostgreSQL connection string. Read by api and storage.

postgres://cms:cms@postgres:5432/cms
BETTER_AUTH_SECRETRequired

Min 32 chars. Signs session cookies and HMAC-hashes access tokens at rest. Rotating invalidates every token and session.

openssl rand -hex 32
STORAGE_SECRETRequired

Min 32 chars. Shared HMAC secret between api (minting upload tokens) and storage (verifying them). Must match on both services.

openssl rand -hex 32
S3_ENDPOINTRequired

S3-compatible endpoint the storage service talks to.

http://minio:9000
S3_ACCESS_KEYRequired

S3 access key ID for the asset bucket.

S3_SECRET_KEYRequired

S3 secret access key for the asset bucket.

S3_BUCKETRequired

Bucket that holds asset blobs. Keys look like spaces/<id>/assets/<n>/<filename>.

cms

URLs & networking

PUBLIC_URLOptionalDefault: http://localhost:8080

Base URL of the installation. Flows into Better Auth's trusted origins, OIDC redirect URIs, webhook payloads and asset URLs. Set this to the external HTTPS hostname in production.

https://cms.example.com
PUBLIC_HOSTOptionalDefault: localhost

Bare hostname (no scheme, no port). Used by the Caddyfile and as OIDC RP_ID.

cms.example.com
BETTER_AUTH_URLOptional

Override for Better Auth's callback URL. Defaults to PUBLIC_URL — only set if your Better Auth callback origin differs from the public frontend origin.

PORTOptional

Service listen port. Defaults per service: api 3001, storage 3002, admin 3000.

HOSTOptionalDefault: 0.0.0.0

Service bind address. Leave at 0.0.0.0 inside containers; set to 127.0.0.1 for local-only binds.

PROXY_PORTOptionalDefault: 8080

Caddy reverse-proxy listen port.

API_UPSTREAMOptionalDefault: api:3001

Caddyfile upstream target for /api/* routes.

ADMIN_UPSTREAMOptionalDefault: admin:3000

Caddyfile upstream target for / (admin UI).

STORAGE_UPSTREAMOptionalDefault: storage:3002

Caddyfile upstream target for /storage/* routes.

INTERNAL_STORAGE_URLOptionalDefault: http://localhost:3002

URL api uses to reach storage for privacy-flag lookups. Inside Docker set to http://storage:3002.

API_URLOptionalDefault: http://api:3001

URL storage uses to reach the api for privacy cross-checks.

TRUSTED_ORIGINS_EXTRAOptional

Comma-separated list of extra CORS origins beyond PUBLIC_URL. Add staging or custom preview hostnames here.

https://preview.example.com,https://stage.example.com

Auth, sessions, OIDC

Setting OIDC_ISSUER_URL switches the installation into OIDC-only mode — email+password login is disabled automatically.

OIDC_ISSUER_URLOptional

Discovery URL of the OIDC provider. Presence toggles OIDC-only mode.

https://auth.example.com/realms/main
OIDC_CLIENT_IDOptional

OIDC client ID. Required when OIDC_ISSUER_URL is set.

osstblok
OIDC_CLIENT_SECRETOptional

OIDC client secret. Required when OIDC_ISSUER_URL is set.

OIDC_PROVIDER_IDOptionalDefault: oidc

Internal identifier for the provider. Shows up in Better Auth's account table.

OIDC_DISPLAY_NAMEOptionalDefault: Single Sign-On

Label for the login-page SSO button.

OIDC_SCOPESOptionalDefault: openid profile email

Space-separated OIDC scopes to request.

OIDC_TRUST_EMAILOptional

Set to 'true' to trust the IdP's email claim and merge OIDC + email accounts with matching verified emails. Opt-in — off by default.

EMAIL_VERIFICATIONOptional

Set to 'off' to skip email verification during signup. Dev convenience only — leave enabled in production.

DISABLE_SIGNUPOptional

Set to 'true' to disable email signup entirely. OIDC login still works.

SIGNUP_ALLOWED_DOMAINSOptional

Comma-separated list of email domains allowed to sign up. Empty = all domains allowed.

example.com,acme.org
MAX_SPACES_PER_USEROptionalDefault: 0

Max spaces a single user can create. 0 = unlimited.

Storage & image pipeline

S3_REGIONOptionalDefault: us-east-1

S3 region name. Set to the bucket's actual region for real AWS S3; any value works for Minio.

MAX_UPLOAD_BYTESOptionalDefault: 52428800

Max upload file size in bytes. Default is 50 MB.

ALLOWED_CONTENT_TYPESOptional

Comma-separated MIME types accepted for upload. Empty = built-in default list (images, PDF, video, audio, fonts).

ALLOW_AVIFOptional

Set to '1' to enable AVIF output in the image transform pipeline. Off by default because encoding is slow; enable only if you have traffic patterns that benefit.

TRANSFORM_CONCURRENCYOptionalDefault: 4

Concurrent image-transform jobs per storage process. Bump up on machines with many cores; lower on memory-constrained hosts.

PRIVACY_CACHE_TTL_SECOptionalDefault: 300

How long (seconds) storage caches privacy-flag lookups from api before re-checking.

STORAGE_RATE_LIMIT_PER_MINOptionalDefault: 500

Per-IP request budget for the storage service, per minute.

Cache & CDN

REDIS_URLOptional

Redis connection URL. Unset → the cache client is a silent no-op (no try/catch, no errors, just slower responses). Set → CDN responses and link maps are cached with short TTLs.

redis://redis:6379

Outbound fetches & SSRF guard

Every fetch() with a user-influenced URL goes through assertSafeOutboundUrl. These knobs tune what's allowed.

OUTBOUND_ALLOW_LOOPBACKOptionalDefault: 0

SSRF-guard escape hatch. Set to '1' in tests that fetch against a local receiver — never gate by NODE_ENV.

IMPORT_MAX_ASSET_BYTESOptionalDefault: 52428800

Max size in bytes for a single asset downloaded during a Storyblok import.

ASSET_FROM_URL_MAX_BYTESOptional

Max size for POST /v1/spaces/:id/assets/from_url downloads. Defaults to IMPORT_MAX_ASSET_BYTES.

MCP

MCP_TOOL_CALL_LIMITOptionalDefault: 200

Max MCP tool calls per sliding window (per token).

OSSTBLOK_MCP_DEBUGOptional

Set to 'true' for verbose MCP request/response logging. Do not leave on in production — payloads can contain content.

Testing & developer tools

DATABASE_URL_TESTOptionalDefault: postgres://cms:cms@localhost:5433/cms_test

Connection string used only during bun test. Drizzle runs against this DB so real data stays untouched.

NODE_ENVOptional

Set to 'production' to harden OIDC validation and emit SSRF warnings when loopback is allowed. Not a config switch — don't gate business logic on it.

PREVIEW_URLOptionalDefault: http://localhost:3000

Default target for the admin's preview-redirect helper. Point at your frontend demo / docs site.

SMOKE_HOSTOptional

LAN IP used by ./scripts/smoke-test.sh. Auto-detected via 'hostname -I' when unset — override only if auto-detect picks the wrong interface.

Infrastructure defaults

These live in docker-compose.yml and configure the backing containers, not the app code directly. Changing them means rebuilding / recreating the related container.

POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DBOptionalDefault: cms / cms / cms

Credentials and DB name for the Postgres container. DATABASE_URL must match these.

MINIO_ROOT_USER / MINIO_ROOT_PASSWORDOptionalDefault: minioadmin / minioadmin

Minio admin credentials. Replace both in production — Minio boots with these as the literal defaults.

NUXT_PUBLIC_API_BASEOptional

Admin-UI client-side API base URL. Leave blank to use a relative path (same-origin via the proxy).