Skip to content

Configuration

CairnCMS is configured primarily through environment variables. The same variables work whether you set them in a .env file, in your shell, in a Docker Compose file, or in a deployment platform’s environment-config UI.

By default, CairnCMS reads .env from the current working directory. The path can be overridden with CONFIG_PATH, which accepts more than just .env text files:

  • .env (default) — standard env-format key/value pairs
  • .json — a JSON object whose keys become environment variables
  • .yaml / .yml — a YAML object with the same shape
  • .js — a JavaScript module that exports either an object or a function (processEnv) => object that builds the config dynamically

The merge order is: built-in defaults, then process.env, then the config file. The config file is applied last and therefore wins for any key it defines. To let a value from process.env take effect, leave the key out of the file. This is the inverse of the 12-factor convention; it lets a checked-in config file remain authoritative without operators needing to remember which env vars the host might have set.

The cairncms init scaffold writes a starter .env to cairncms/.env with random secrets and a default admin password. Operators usually edit that file before first deploy and source-control everything except secret values.

For Docker deployments, the same variables come from the environment: block of the Compose service or from a referenced env file. Treat the values as secrets. Never commit a .env file with real credentials to source control.

Most environment variables are auto-coerced based on a known internal type map: PORT is read as a number, DB_NAME as a string, RATE_LIMITER_ENABLED as a boolean, and so on. For variables that aren’t in the type map, CairnCMS infers the type from the value ("true"/"false" to boolean, numeric strings to numbers, JSON-shaped strings to objects).

To force a specific type, prefix the value with the type name and a colon:

Terminal window
DB_PASSWORD="string:1234567890"
CACHE_TTL="string:5m"
DB_EXCLUDE_TABLES="array:spatial_ref_sys,sysdiagrams"

Recognized prefixes: string, number, regex, array, json. There is no boolean: prefix; for boolean-typed variables, the auto-inference of "true" / "false" is sufficient. Use a prefix when the inferred type would be wrong, for example, when a numeric-looking password should stay a string.

Variables that control how CairnCMS listens for requests:

  • HOST — interface to bind. Default 0.0.0.0.
  • PORT — port to listen on. Default 8055.
  • PUBLIC_URL — the externally-reachable URL of the instance. Used to construct asset URLs, redirect targets, and email links. Set this whenever the instance is reachable through a hostname or path that isn’t http://localhost:8055.
  • SERVE_APP — whether to serve the admin app at /admin. Default true. Set to false for headless API deployments where no operator UI is needed.
  • GRAPHQL_INTROSPECTION — whether the GraphQL schema is introspectable. Default true. Disable in production if you do not want unauthenticated clients to enumerate the schema.
  • MAX_PAYLOAD_SIZE — the maximum request body size. Default 1mb. Increase when receiving large uploads or imports.
  • MAX_RELATIONAL_DEPTH — how deeply nested a single query can fetch related data. Default 10.
  • MAX_BATCH_MUTATION — limit on items in a batch create/update/delete. Default unlimited.
  • QUERYSTRING_PARSE_DEPTH — maximum nesting depth parsed from URL query strings. Default 10.
  • QUERYSTRING_ARRAY_LIMIT — maximum number of indexed query-string array entries parsed as arrays. Default 500. Raise this if clients send large indexed arrays such as wide _in filters. Lower values reduce query parsing resource exposure.
  • ROBOTS_TXT — the body served at /robots.txt. Default disallows all crawlers.
  • ROOT_REDIRECT — where requests to / redirect. Default ./admin.
  • SERVER_SHUTDOWN_TIMEOUT — milliseconds to wait for in-flight requests during shutdown. Default 1000.
  • IP_TRUST_PROXY — whether Express should trust X-Forwarded-For headers. Default true (assumes a reverse proxy in front).
  • IP_CUSTOM_HEADER — alternative header to read the client IP from, when not using X-Forwarded-For.

CairnCMS uses pino for structured logging.

  • LOG_LEVELfatal, error, warn, info, debug, trace, or silent. Default info. Use debug or trace for verbose request and operation traces.
  • LOG_STYLEpretty (default, human-readable) or raw (line-delimited JSON, suitable for log shippers and aggregation systems).

Additional LOGGER_* variables pass through to the underlying pino logger options, and LOGGER_HTTP_* variables pass through to the HTTP request logger. Use these for tuning that is not exposed at the top level (e.g., custom serializers, redaction paths, request/response shape filtering).

CairnCMS supports SQLite, PostgreSQL, MySQL, MariaDB. The connection is configured through:

  • DB_CLIENTsqlite3 for SQLite, pg for PostgreSQL, mysql for both MySQL and MariaDB. MariaDB does not have a separate client value; it shares MySQL’s underlying driver.
  • DB_HOST / DB_PORT / DB_DATABASE / DB_USER / DB_PASSWORD — connection details for server-based databases.
  • DB_FILENAME — path to the SQLite database file. SQLite-only.
  • DB_SSL__* / DB_POOL__* — SSL options and connection pool tuning. Use the double-underscore syntax to express nested config (DB_SSL__REJECT_UNAUTHORIZED=false, DB_POOL__MIN=2). Any key in the DB_* prefix passes through to the underlying Knex connection object.
  • DB_EXCLUDE_TABLES — comma-separated table names that CairnCMS should ignore even if it can read them. Default skips Postgres geometry housekeeping (spatial_ref_sys) and SQL Server diagrams (sysdiagrams).

When using MySQL or MariaDB, set the database, table, and column character set to utf8mb4. The default utf8 in older MySQL versions cannot store characters that take four bytes in UTF-8 — including most CJK characters and almost all emoji.

Terminal window
DB_CHARSET="utf8mb4"

If you are connecting to an existing MySQL database, also confirm at the server level that character_set_server=utf8mb4 and that any pre-existing tables have been altered if they were originally created with utf8. Otherwise inserts of unsupported characters will fail or silently truncate.

CairnCMS supports multiple storage backends. The active backend list and per-backend config:

  • STORAGE_LOCATIONS — comma-separated list of storage locations. Each location’s settings are scoped under STORAGE_<LOCATION>_* variables. Default local.
  • STORAGE_<LOCATION>_DRIVERlocal, s3, gcs, azure, or cloudinary.

Per-driver settings vary. For local disk:

Terminal window
STORAGE_LOCATIONS="local"
STORAGE_LOCAL_DRIVER="local"
STORAGE_LOCAL_ROOT="./uploads"

For S3 (or any S3-compatible service such as MinIO, DigitalOcean Spaces, or Cloudflare R2):

Terminal window
STORAGE_LOCATIONS="s3"
STORAGE_S3_DRIVER="s3"
STORAGE_S3_KEY="..."
STORAGE_S3_SECRET="..."
STORAGE_S3_BUCKET="..."
STORAGE_S3_REGION="us-east-1"
STORAGE_S3_ENDPOINT="https://s3.amazonaws.com"

For Google Cloud Storage:

Terminal window
STORAGE_LOCATIONS="gcs"
STORAGE_GCS_DRIVER="gcs"
STORAGE_GCS_KEY_FILENAME="/path/to/service-account.json"
STORAGE_GCS_BUCKET="..."

For Azure Blob Storage:

Terminal window
STORAGE_LOCATIONS="azure"
STORAGE_AZURE_DRIVER="azure"
STORAGE_AZURE_CONTAINER_NAME="..."
STORAGE_AZURE_ACCOUNT_NAME="..."
STORAGE_AZURE_ACCOUNT_KEY="..."

For Cloudinary:

Terminal window
STORAGE_LOCATIONS="cloudinary"
STORAGE_CLOUDINARY_DRIVER="cloudinary"
STORAGE_CLOUDINARY_CLOUD_NAME="..."
STORAGE_CLOUDINARY_API_KEY="..."
STORAGE_CLOUDINARY_API_SECRET="..."

Any STORAGE_<LOCATION>_* key beyond the named ones above passes through to the driver. ACL settings, server-side encryption options, health-check thresholds, and other driver-specific knobs are all set this way.

Multiple locations can be configured simultaneously and individual files can live on different backends. The Storage field on each file record records which location holds the bytes.

  • KEY — required. A unique identifier for this CairnCMS instance, surfaced as the service ID in server info and health-check responses. Not used to sign tokens or name cookies — that is SECRET’s job.
  • SECRET — required. Random secret used to sign access and refresh tokens. Treat as a credential. Changing it invalidates every existing token.
  • ACCESS_TOKEN_TTL — short-lived access token lifetime. Default 15m.
  • REFRESH_TOKEN_TTL — refresh token lifetime. Default 7d.
  • REFRESH_TOKEN_COOKIE_NAME — name of the refresh-token cookie. Default cairncms_refresh_token.
  • REFRESH_TOKEN_COOKIE_SECURE — set to true for production over HTTPS. Default false.
  • REFRESH_TOKEN_COOKIE_SAME_SITEstrict, lax, or none. Default lax.
  • REFRESH_TOKEN_COOKIE_DOMAIN — domain to scope the cookie to, for cross-domain SSO setups.
  • LOGIN_STALL_TIME — milliseconds failed logins wait before responding, to mitigate timing attacks. Default 500.
  • PASSWORD_RESET_URL_ALLOW_LIST / USER_INVITE_URL_ALLOW_LIST — comma-separated URLs allowed for password reset and user invite links.
  • AUTH_PROVIDERS — comma-separated list of SSO providers to enable.
  • AUTH_DISABLE_DEFAULT — when true, the built-in email/password login is disabled and only configured SSO providers can authenticate.

Per-provider configuration uses the pattern AUTH_<PROVIDER>_*. For example, AUTH_GOOGLE_DRIVER=openid declares an OpenID Connect provider named google, and subsequent AUTH_GOOGLE_CLIENT_ID, AUTH_GOOGLE_CLIENT_SECRET, and other driver-specific variables configure the connection. The driver-specific variable shape passes through to the underlying library (Passport strategies for OAuth2 and OpenID, ldapjs for LDAP, samlify for SAML). See the Auth guide for setup examples across the common providers.

Password hashing options are passed through to argon2 via HASH_* variables (e.g., HASH_MEMORY_COST, HASH_TIME_COST, HASH_PARALLELISM). Defaults match argon2id recommendations; only override if you have measured the impact on login latency.

These variables seed the initial admin user when cairncms bootstrap runs against an empty database. After bootstrap they have no effect, but the values typically stay in .env so a reproducible re-bootstrap (against a fresh database) yields the same admin.

  • ADMIN_EMAIL — email for the initial admin account.
  • ADMIN_PASSWORD — password for the initial admin account.

If either is unset, cairncms bootstrap prompts interactively for both.

CairnCMS sets standard security response headers via Helmet. Most can be tuned per deployment.

  • HSTS_ENABLED — emit the Strict-Transport-Security header. Default false. Enable in production when serving over HTTPS.
  • HSTS_* — pass-through to Helmet’s HSTS options (HSTS_MAX_AGE, HSTS_INCLUDE_SUBDOMAINS, HSTS_PRELOAD).
  • CONTENT_SECURITY_POLICY_* — pass-through to Helmet’s CSP option shape. The default CSP allows the admin app to load its own assets and connect to the API origin.
  • ASSETS_CONTENT_SECURITY_POLICY — separate CSP applied only to /assets/* responses. Useful when serving user-uploaded content with a stricter policy than the rest of the API.
  • IMPORT_IP_DENY_LIST — comma-separated exact IP addresses blocked from URL imports (SSRF defense). Matches are literal string comparisons; CIDR notation is not supported. Default blocks 0.0.0.0 and the EC2/cloud metadata endpoint 169.254.169.254. The 0.0.0.0 entry has a special meaning: when present, all loopback addresses and any address bound to the host’s own network interfaces are also blocked. To block other addresses, list each one explicitly.

For the full Helmet option shape, see the Helmet documentation.

Disabled by default. Enable when a frontend on a different origin needs to call the API.

  • CORS_ENABLEDtrue to enable. Default false.
  • CORS_ORIGINtrue to reflect the request origin, false to disable, or a comma-separated list of allowed origins.
  • CORS_METHODS — allowed methods. Default GET,POST,PATCH,DELETE.
  • CORS_ALLOWED_HEADERS — allowed request headers. Default Content-Type,Authorization.
  • CORS_EXPOSED_HEADERS — response headers exposed to the browser. Default Content-Range.
  • CORS_CREDENTIALS — whether credentials (cookies, auth headers) can be included on cross-origin requests. Default true.
  • CORS_MAX_AGE — preflight cache duration in seconds. Default 18000.

Three independent rate limiters are available: per-IP, global, and pressure-based. All three are off by default.

  • RATE_LIMITER_ENABLED — per-IP limiter switch. Default false.
  • RATE_LIMITER_POINTS — requests allowed per duration window. Default 50.
  • RATE_LIMITER_DURATION — window length in seconds. Default 1.
  • RATE_LIMITER_STOREmemory (single-process), redis (distributed), or memcache.
  • RATE_LIMITER_GLOBAL_* — same shape, applied across all callers globally rather than per-IP.

For multi-instance deployments behind a load balancer, the redis store is required for the limit to be shared across processes. Per-store details (Redis connection settings, healthcheck thresholds) live under RATE_LIMITER_*_REDIS, RATE_LIMITER_*_MEMCACHE, and RATE_LIMITER_*_HEALTHCHECK_THRESHOLD. Both the per-IP and global limiters accept the same option set under their respective prefixes.

A third limiter monitors Node event-loop and memory pressure and returns 503 Service Unavailable when the process is overloaded. The goal is to give load balancers a fast failure signal under genuine saturation rather than letting requests queue behind an unresponsive server.

  • PRESSURE_LIMITER_ENABLED: pressure limiter switch. Default false.
  • PRESSURE_LIMITER_SAMPLE_INTERVAL: sampling cadence in milliseconds. Default 250.
  • PRESSURE_LIMITER_MAX_EVENT_LOOP_UTILIZATION: fraction of event-loop utilization above which the process is considered overloaded. Default 0.99.
  • PRESSURE_LIMITER_MAX_EVENT_LOOP_DELAY: maximum event-loop delay in milliseconds. Default 500.
  • PRESSURE_LIMITER_MAX_MEMORY_RSS: maximum process resident set size in bytes. Default false (disabled).
  • PRESSURE_LIMITER_MAX_MEMORY_HEAP_USED: maximum heap-used memory in bytes. Default false (disabled).
  • PRESSURE_LIMITER_RETRY_AFTER: value sent in the Retry-After response header on 503 responses. Default false (header is not set).

The CairnCMS default is opt-in. Pressure limiting is the most workload-sensitive of the three rate limiters. Long-running queries, imports, exports, image transforms, ETL jobs, and deployments on small container CPU limits can briefly saturate the event loop or RSS without the process actually being in trouble. Dry-run against a representative workload before enabling.

The limiter is per Node process. In multi-instance deployments (Cloud Run, Kubernetes, multi-container Compose), each container monitors its own pressure independently. The middleware does not coordinate across instances and does not signal readiness or liveness failure to the orchestrator. A 503 from this limiter tells the immediate caller to retry. It does not remove the instance from the load balancer rotation.

CairnCMS has two distinct caches:

  • Response caching — caches the result of API requests. Off by default; turn on with CACHE_ENABLED=true.
  • Schema and permissions caching — caches the internal schema model and permissions tables. On by default and runs even when response caching is disabled.

Variables for the response cache:

  • CACHE_ENABLEDtrue to enable response caching. Default false.
  • CACHE_STOREmemory, redis, or memcache.
  • CACHE_TTL — default cache duration. Default 5m.
  • CACHE_AUTO_PURGE — when true, the cache invalidates automatically on writes to relevant collections.
  • CACHE_CONTROL_S_MAXAGE — value for the Cache-Control: s-maxage directive on cached responses, in seconds. Default 0.
  • CACHE_STATUS_HEADER — name of the response header indicating cache hit/miss. Unset by default (no header emitted).
  • CACHE_VALUE_MAX_SIZE — maximum size in bytes for a cacheable response. Larger responses bypass the cache. Default false (no limit).
  • CACHE_SKIP_ALLOWED — when true, callers can bypass the cache by sending a Cache-Control: no-store request header. Default false.
  • CACHE_HEALTHCHECK_THRESHOLD — latency threshold in milliseconds; the cache is reported unhealthy when its operations exceed this. Defaults to 150 ms when unset.

Variables for the system (schema/permissions) cache:

  • CACHE_SCHEMA — cache the schema metadata. Default true.
  • CACHE_PERMISSIONS — cache the permissions tables. Default true.
  • CACHE_SYSTEM_TTL — TTL for the schema and permissions cache. Unset by default (cache lives for the process lifetime, invalidated on schema or permission writes).
  • CACHE_NAMESPACE — Redis key prefix. Default system-cache. Set per-instance when multiple CairnCMS deployments share the same Redis.

When using Redis, each subsystem has its own connection variable rather than a shared one:

  • CACHE_REDIS — connection string for the response cache.
  • CACHE_REDIS_HOST / CACHE_REDIS_PORT / CACHE_REDIS_PASSWORD — broken-out fields, used as an alternative to CACHE_REDIS.
  • RATE_LIMITER_REDIS — connection string for the rate limiter.

For multi-instance deployments, the cache, rate limiter, and messenger Redis usually point at the same instance but are configured independently so a deployment can split them apart if needed.

The messenger is the inter-process communication layer that broadcasts events like cache invalidations across CairnCMS instances. Single-instance deployments use the in-memory store; horizontally-scaled deployments need Redis.

  • MESSENGER_STORElocal (single-process) or redis (multi-instance). Default local.
  • MESSENGER_NAMESPACE — Redis pub/sub channel prefix. Default cairncms.
  • MESSENGER_REDIS — connection string for the messenger Redis.
  • MESSENGER_REDIS_HOST / MESSENGER_REDIS_PORT / MESSENGER_REDIS_PASSWORD — broken-out fields, used as an alternative to MESSENGER_REDIS.
  • EMAIL_FROM — sender address for all transactional emails. Required for any email feature.
  • EMAIL_TRANSPORT — transport mechanism: sendmail, smtp, mailgun, sendgrid, or ses. Default sendmail.
  • EMAIL_VERIFY_SETUP — verify transport credentials at startup. Default true.

Per-transport settings live under EMAIL_<TRANSPORT>_*.

For SMTP:

Terminal window
EMAIL_TRANSPORT="smtp"
EMAIL_SMTP_HOST="smtp.example.com"
EMAIL_SMTP_PORT="587"
EMAIL_SMTP_USER="..."
EMAIL_SMTP_PASSWORD="..."
EMAIL_SMTP_SECURE="false"

Additional EMAIL_SMTP_* keys (such as EMAIL_SMTP_NAME, EMAIL_SMTP_POOL, EMAIL_SMTP_IGNORE_TLS, and the EMAIL_SMTP_TLS_* family) pass through to nodemailer’s SMTP transport options.

For sendmail (default):

Terminal window
EMAIL_TRANSPORT="sendmail"
EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
EMAIL_SENDMAIL_NEW_LINE="unix"

For Mailgun:

Terminal window
EMAIL_TRANSPORT="mailgun"
EMAIL_MAILGUN_API_KEY="..."
EMAIL_MAILGUN_DOMAIN="mg.example.com"
EMAIL_MAILGUN_HOST="api.mailgun.net"

EMAIL_MAILGUN_HOST defaults to api.mailgun.net; override to api.eu.mailgun.net for EU-region accounts.

For SendGrid:

Terminal window
EMAIL_TRANSPORT="sendgrid"
EMAIL_SENDGRID_API_KEY="..."

For Amazon SES:

Terminal window
EMAIL_TRANSPORT="ses"
EMAIL_SES_CREDENTIALS__ACCESS_KEY_ID="..."
EMAIL_SES_CREDENTIALS__SECRET_ACCESS_KEY="..."
EMAIL_SES_REGION="us-east-1"

EMAIL_SES_* variables pass through to the AWS SDK v3 SESv2 client configuration. Use the double-underscore syntax for nested fields.

For email-template customization beyond transport configuration, see Email templates.

Asset transformation settings:

  • ASSETS_CACHE_TTL — how long transformed assets stay cached client-side. Default 30d.
  • ASSETS_TRANSFORM_MAX_CONCURRENT — concurrent transformation jobs. Default 25.
  • ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION — maximum side-length for transformed images. Default 6000.
  • ASSETS_TRANSFORM_MAX_OPERATIONS — chained operations per transform request. Default 5.
  • ASSETS_TRANSFORM_TIMEOUT — per-job timeout. Default 7500ms.
  • ASSETS_INVALID_IMAGE_SENSITIVITY_LEVEL — how strictly to reject corrupted images. error, warning, or none. Default warning.
  • FILE_METADATA_ALLOW_LIST — comma-separated list of EXIF and IFD tag paths to extract from uploaded image files. Default includes camera make, model, F-number, exposure, focal length, and ISO. Restrict further or expand depending on your privacy and metadata-display requirements.
  • RELATIONAL_BATCH_SIZE — chunk size for batched relational queries. Default 25000. Larger values reduce round trips at the cost of memory; smaller values trade more queries for lower memory use.
  • EXPORT_BATCH_SIZE — chunk size for streaming exports. Default 5000. Tunes memory pressure on large exports.
  • EXTENSIONS_PATH — root folder for local extensions, custom migrations, and email templates. Default ./extensions.
  • EXTENSIONS_AUTO_RELOAD — when true, the API watches extension files and reloads on change. Default false. Disabled in development; see the Creating extensions page for the full caveat.
  • EXTENSIONS_CACHE_TTLCache-Control max-age applied to the /extensions/* bundle responses. Unset by default (no client cache).
  • PACKAGE_FILE_LOCATION — directory containing the project package.json (used to discover npm-installed extensions). Default ..

The Run Script flow operation runs user-supplied JavaScript inside an isolated V8 sandbox without require() or host APIs. Two env vars govern the isolate’s resource budget; one governs which environment variables the script can read.

  • FLOWS_RUN_SCRIPT_MAX_MEMORY — memory limit for a single Run Script invocation, in MB. Default 32. Scripts that exceed the limit are aborted.
  • FLOWS_RUN_SCRIPT_TIMEOUT — wall-clock timeout for a single Run Script invocation, in milliseconds. Default 10000. Scripts that exceed the limit are aborted.
  • FLOWS_ENV_ALLOW_LIST — comma-separated list of environment variable names the Run Script operation is allowed to read via process.env. Default unset (no environment variables exposed to scripts).

If you lose access to an admin account, the CLI can set a new password directly:

Terminal window
cairncms users passwd --email admin@example.com --password new-strong-password

This bypasses the password policy and any 2FA enrollment on the user. The command runs against the configured database and updates the password hash in place.

For containerized deployments, run the command inside the running container:

Terminal window
docker compose --project-directory cairncms -f cairncms/docker-compose.yml \
exec cairncms cairncms users passwd --email admin@example.com --password new-strong-password

If a user account is locked because of too many failed login attempts, an admin can unlock it by setting the user’s Status to Active in the User Directory.

  • Deployment covers running CairnCMS in production.
  • Security hardening covers HTTPS, secrets management, and other production-safety configuration.
  • Auth covers SSO, two-factor, and session details that intersect with the auth-related variables above.