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.
  • GRAPHQL_QUERY_TOKEN_LIMIT — maximum tokens allowed in one GraphQL document. Default 5000. CairnCMS rejects larger documents before validation or execution. Accepts a whole number of 1 or greater; invalid or imprecise values prevent startup. Higher limits allow larger documents but increase parser resource use. CairnCMS does not define a universally safe maximum.
  • MAX_PAYLOAD_SIZE — the maximum request body size. Default 1mb. Increase when receiving large uploads or imports. For a per-file upload cap, see FILES_MAX_UPLOAD_SIZE under Files and batch operations.
  • 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 — proxy addresses trusted to supply forwarded client IPs. Default false. Set an exact IP or CIDR, a comma-separated list, or loopback. Without it, proxied requests use the proxy’s IP. Use true only when every ingress sanitizes forwarded headers; see Trusted proxies and client IP.
  • IP_CUSTOM_HEADER — a header CairnCMS reads the client IP from instead of X-Forwarded-For. Honored only when the immediate peer is a trusted proxy per IP_TRUST_PROXY. Default false (disabled).

Limits below about 200 tokens can prevent GraphiQL or Apollo Studio from loading the schema. Large Insights dashboards may also require a higher limit because the admin app sends all panel queries in one GraphQL document.

Two environment variables bound how many rows a list read returns. Both accept -1 for unlimited or an integer in the per-variable range below, and the largest value either one accepts is 2147483647. A malformed, fractional, negative (other than -1), or out-of-range value stops the server from starting.

  • QUERY_LIMIT_DEFAULT — the page size applied to a list read that omits its own limit. Default 100. Accepts 0 or higher, or -1 to return every matching row by default when QUERY_LIMIT_MAX is also unlimited.
  • QUERY_LIMIT_MAX — the ceiling on how many rows a single list may return. Default -1 (no ceiling). A finite ceiling must be 1 or higher.

With a finite QUERY_LIMIT_MAX, a limit=-1 request resolves to the ceiling. An explicit limit above the ceiling is rejected with 400 INVALID_QUERY rather than clamped. A read that omits limit uses the configured default, or the ceiling when the default is unlimited or larger than it. The same ceiling applies to nested relational limits set through deep and to queries sent in a SEARCH request body.

The ceiling applies to the top-level list and to each nested relational list separately, not to the total number of objects in a response. A response with many parents or relational fields can therefore hold more than QUERY_LIMIT_MAX objects. The ceiling also does not by itself bound the database or memory work behind a relational query, since related records are fetched in batches before the response is assembled. Treat it as a per-list response-cardinality control and a partial mitigation against unbounded reads through the public and authenticated APIs.

Permission list reads through /permissions force an unbounded limit after normal query validation. An accepted limit or page does not truncate the result, while other constraints such as filter, offset, search, sort, and fields stay effective. An explicit limit above a finite QUERY_LIMIT_MAX is still rejected before the read runs. A role-filtered request returns every permission matching its other constraints, so the admin app can render a role’s permission set in full. The server stays the authority on what each role is allowed to do.

Background exports through POST /utils/export are the other exception. They may exceed the ceiling for the top-level rows written to the export file, because the export runs asynchronously and streams to a file in batches. A limit of -1, null, or an omitted limit exports every matching top-level row, and a positive limit above the ceiling is honored rather than rejected. Nested relational lists inside the export keep ordinary query-limit behavior, so a nested _limit above the ceiling is still rejected. See Platform and utilities for the endpoint details.

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 policy limits the admin app’s connect-src to first-party ('self') plus the built-in map origins (OpenStreetMap tiles, OpenMapTiles fonts, and Mapbox), with no external wildcard. Setting CONTENT_SECURITY_POLICY_DIRECTIVES__CONNECT_SRC fully replaces that list, so include 'self' and any map origins you still rely on. This bounds where an app extension can connect from the browser. See Security hardening for the extension egress details.
  • 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_AUTO_PURGE_IGNORE_LIST — comma-separated collections whose writes do not purge the response cache when CACHE_AUTO_PURGE is true. Default directus_activity,directus_presets. Cached responses may remain until CACHE_TTL expires. Remove a collection from the list if its writes must invalidate cached responses.
  • 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.

When MESSENGER_STORE is redis, CairnCMS coordinates scheduled flows and extension hooks across instances. Each occurrence can run on at most one instance. With the default local store, every instance runs its schedules independently. Coordination uses the messenger Redis configuration automatically and has no separate setting.

For a multi-instance deployment:

  • Use a currently supported Redis release with Redis 6.2 protocol support, or a compatible Valkey release.
  • Configure every instance to use the same Redis deployment, MESSENGER_NAMESPACE, and system timezone (TZ).
  • Assign a different MESSENGER_NAMESPACE to each CairnCMS deployment that shares a Redis server. Reusing a namespace can cause one deployment to suppress schedules in another.

If Redis is unavailable or does not support coordination, the API starts but scheduled flows and extension hooks remain disabled. CairnCMS resumes scheduling automatically after Redis recovers. Occurrences missed during the outage are not replayed. Check /server/health for the current messenger and coordination status.

Messenger messages are not queued while Redis is unavailable. Cache invalidations and flow reloads sent during an outage are lost, so instances can remain out of sync until a later update, reload, or restart.

If Redis ACLs restrict publishing, allow PUBLISH on <namespace>:messenger-probe. Without this permission, /server/health reports the messenger as degraded.

Realtime WebSockets are off by default. When enabled, CairnCMS serves the item protocol at /websocket and GraphQL subscriptions at /graphql. The item protocol uses the WEBSOCKETS_REST_* settings. GraphQL uses the WEBSOCKETS_GRAPHQL_* settings.

Both transports deliver permission-checked collection changes. Deployments with multiple API instances need the Redis messenger so changes reach subscribers on every instance. See Realtime for the protocols, authentication modes, subscriptions, and SDK.

  • WEBSOCKETS_ENABLED — master switch for the realtime feature. Default false.
  • WEBSOCKETS_REST_ENABLED — item transport switch. Takes effect only when the feature is enabled. Default true.
  • WEBSOCKETS_REST_PATH — the upgrade path clients connect to. Must be a pure URL path with no query or fragment. Default /websocket.
  • WEBSOCKETS_REST_AUTH — authentication mode: public (anonymous), handshake (authenticate with the first message), or strict (bearer token required at upgrade). Default handshake.
  • WEBSOCKETS_REST_AUTH_TIMEOUT — how long, in seconds, an authentication attempt may take. In strict mode the attempt is at the upgrade, and a timeout rejects the upgrade; in handshake mode a timeout on the first-message attempt closes the connection. A later re-authentication that times out closes a strict or handshake connection and returns a public connection to anonymous access. Accepts a whole number of seconds from 1 to 2147483 (about 24 days). Default 10.
  • WEBSOCKETS_REST_CONN_LIMIT — maximum concurrent connections on the item transport. Accepts a whole number of 1 or greater. Default 1000.
  • WEBSOCKETS_GRAPHQL_ENABLED — GraphQL transport switch, effective only when the feature is enabled. Default true.
  • WEBSOCKETS_GRAPHQL_PATH — the upgrade path GraphQL clients connect to. Must be a pure URL path with no query or fragment, and must differ from the active item-protocol path. An equal path deactivates the GraphQL transport and leaves the item protocol serving. Default /graphql.
  • WEBSOCKETS_GRAPHQL_AUTH — authentication mode: public (anonymous), handshake (authenticate with the connection_init message), or strict (bearer token required at upgrade). Default handshake.
  • WEBSOCKETS_GRAPHQL_AUTH_TIMEOUT — the authentication timeout, in seconds. It bounds each authentication checkpoint independently rather than as one end-to-end deadline. The connection_init frame must be received within it, and the credential check is separately bounded by it (a strict bearer token at the upgrade, a public token supplied in the init message). There is no later in-band re-authentication. Accepts a whole number of seconds from 1 to 2147483 (about 24 days). Default 10.
  • WEBSOCKETS_GRAPHQL_CONN_LIMIT — maximum concurrent connections on the GraphQL transport. Accepts a whole number of 1 or greater. Default 1000.
  • WEBSOCKETS_HEARTBEAT_PERIOD — seconds between server pings; a connection that misses two periods without a pong is closed. Accepts a whole number of seconds from 1 to 1073741 (about 12 days). Default 30.
  • WEBSOCKETS_USER_CONN_LIMIT — maximum concurrent connections per authenticated user. Accepts a whole number of 1 or greater. Default 10.
  • WEBSOCKETS_IP_CONN_LIMIT — maximum concurrent connections per client IP. It gates anonymous and pre-authentication connections. Once a connection authenticates, it counts against WEBSOCKETS_USER_CONN_LIMIT instead. Accepts a whole number of 1 or greater. Default 50.
  • WEBSOCKETS_PROCESS_CONN_LIMIT — maximum concurrent connections per API process. Accepts a whole number of 1 or greater. Default 1000.

An invalid shared setting disables realtime. An invalid item-protocol or GraphQL setting disables only that transport. HTTP remains available.

The shared MAX_PAYLOAD_SIZE bounds inbound WebSocket frames (see Server). Realtime requires it to resolve to a positive whole number of bytes. Outbound WebSocket frames are limited to 1 MiB, including initial subscription results and change notifications. An oversized frame closes the affected connection. The client must reconnect and reread current state.

Realtime is a best-effort notification channel whose latency reflects the deployment’s workload and available capacity. Operators should size infrastructure for expected result sets, query cost, and subscriber load. Applications that require current state should reconcile through API reads rather than depend on notification timing.

  • 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.
  • FILES_MAX_UPLOAD_SIZE — the maximum size of a single uploaded file, applied to both multipart uploads and URL imports. Unset by default (no limit). Accepts a whole-number size such as 10mb, 1gb, or 500kb. This is a per-file cap, distinct from MAX_PAYLOAD_SIZE (the whole request body). A malformed value fails startup rather than silently disabling the cap.
  • FILES_MIME_TYPE_ALLOW_LIST — the content types allowed for upload, applied to both multipart uploads and URL imports. Default */* (all types). Comma-separated glob patterns, for example image/*,application/pdf. The check matches the declared content type, not the file’s actual bytes, so treat it as a guard, not a guarantee.
  • 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, including when NODE_ENV=development. Default false. See the Creating extensions page for the development workflow.
  • 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 ..
  • SECRETS_ENCRYPTION_KEY — key under which inline extension secrets and secret flow-operation options are encrypted at rest. The value must be canonical base64 decoding to at least 32 bytes; generate one with openssl rand -base64 32. Leaving it unset is allowed until the first secret is saved; without it the save fails with a configuration error, and non-secret values are unaffected. Setting it to a malformed value fails startup. Treat as a credential, and keep it distinct from SECRET, which signs tokens and is unrelated. Changing it makes previously stored secret values unreadable: they fail closed rather than decrypting wrongly, and each must be re-entered.
  • CAIRNCMS_EXT_<NAME>_<KEY> — the variable family for extension secrets declared with the config source. The extension’s documentation names the exact variables; the name derives from the package name and setting key, so it is stable and can be provisioned in a secret manager ahead of deployment. See Extension settings for the derivation.

These govern the sandboxed server runtime that a confined extension runs in. It is separate from the Flows Run Script sandbox documented below. The size variables accept a byte count or a size string such as 64MB, and the timeout accepts a millisecond count or a duration string such as 10s. See the Sandbox reference for the runtime model.

  • EXTENSIONS_SANDBOX_OS_HARDENINGauto or required. Default auto. Under auto, the OS hardening layers are applied wherever the host supports them and never block a confined extension from running. Under required, the runtime refuses to start a confined extension on a host that cannot provide the escape-containment core (the network namespace and the Node permission model).
  • EXTENSIONS_SANDBOX_MAX_MEMORY — per-invocation memory cap for the QuickJS guest heap. Default 64MB. Bounded between 16MB and 512MB. When cgroup hardening is available, the whole-process memory limit is derived from this value plus the child’s base RSS and headroom.
  • EXTENSIONS_SANDBOX_TIMEOUT — per-invocation wall-clock timeout for a confined child process. Default 10s. Bounded between 1s and 5m.
  • EXTENSIONS_SANDBOX_MAX_PROCESSES — maximum number of concurrent confined child processes. Defaults to an adaptive value up to 4, lowered on smaller hosts. An explicit value is bounded between 1 and 32 and fails closed if it would overcommit the sandbox memory budget.
  • EXTENSIONS_SANDBOX_MAX_RESULT — maximum serialized size of a handler result. Default 1MB. Bounded between 1KB and 16MB.
  • EXTENSIONS_SANDBOX_MAX_HOST_API_CALL — maximum serialized size of a single host.* call payload. Default 256KB. Bounded between 1KB and 4MB.
  • EXTENSIONS_SANDBOX_MAX_ARTIFACT — maximum size of a built confined artifact the loader will read. Default 8MB. Bounded between 1KB and 32MB.

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.