Configuration

Configuration comes from three places, merged in this order:

  1. The defaults in calm/config.js (listed below).
  2. calm.config.js in your project.
  3. Overrides passed to createApp({ config }), which is what tests use.

Before that, .env is loaded into process.env (with Node's built-in process.loadEnvFile). Keep secrets in .env and the environment; keep calm.config.js for behavior, and commit it.

calm.config.js#

JavaScript
import { defineConfig } from '#calm';

export default defineConfig({
  pagination: { defaultLimit: 20, maxLimit: 100 },
  auth: { sessionTtlDays: 30, resetTokenTtlMinutes: 30 },
  mail: { from: 'My API <no-reply@example.com>' },
});

It can also export a function of the environment and mode:

JavaScript
export default defineConfig(({ env, mode }) => ({
  bodyLimit: mode === 'production' ? '256kb' : '5mb',
  cors: { origins: env.CORS_ORIGINS?.split(',') ?? ['http://localhost:3000'] },
}));

defineConfig returns its argument unchanged; it only helps editors autocomplete. Nested objects are merged key by key, so auth: { register: false } keeps the other auth defaults. Arrays are replaced.

Mode#

The mode is NODE_ENV, and defaults to production when NODE_ENV is not set. calm dev sets development, tests use test.

developmenttestproduction
Devtools at /_calmonoffoff, cannot be enabled
CORS without CORS_ORIGINSany origin (*)any originno origin
APP_URLoptionaloptionalrequired
Log formatprettypretty, silentJSON lines
Strict-Transport-Security headersent
Rate limitsonoffon
500 responses include debugyes

Only development and test relax the rules. Any other value (staging, preview, unset) runs as production: CORS needs an explicit list, APP_URL is required and devtools stay off. The raw value is available as config.nodeEnv.

Validation at boot#

The app refuses to start, with a readable message, when:

  • MONGODB_URI is not set (db.uri);
  • the mode is production and APP_URL is not set.
Invalid configuration:
  - APP_URL must be set in production (used in emails and links)

Every key#

KeyDefaultNotes
app.namename in package.jsonBanner, emails, OpenAPI title
app.versionversion in package.json
app.urlAPP_URL, else http://localhost:<port>Public URL of the API, used in reset links and OpenAPI servers
portPORT, else 5001
hostHOST, else all interfaces
apiPrefix'/api'Where every route is mounted
trustProxyTRUST_PROXY, else falseExpress trust proxy. 'true', 'false', a hop count (1) or a value like 'loopback'
bodyLimit'1mb'Maximum JSON body
jsonLimits{ maxDepth: 20, maxKeys: 10000 }Deepest nesting and most values a JSON body may have; more is a 400 (see Security)
db.uriMONGODB_URIRequired
db.autoIndextrueBuild indexes declared on fields at startup
db.connectRetries5Attempts with exponential backoff (up to 10 s apart)
cors.originsCORS_ORIGINS (comma separated), else [] in production and '*' otherwiseAllowed browser origins
cors.credentialsfalseSend Access-Control-Allow-Credentials (not needed for bearer tokens)
cors.maxAge600Preflight cache, in seconds
cors.exposeHeaders[]Response headers browsers may read, besides X-Request-Id, the rate limit headers, Retry-After and Idempotent-Replayed
pagination.defaultLimit20Page size when ?limit is absent
pagination.maxLimit100Largest ?limit allowed
unknownFields'reject'Unknown body fields are a 422. Any other value ('strip') drops them silently.
rateLimit.enabledtrue (false in test mode)
rateLimit.global{ windowMs: 60000, max: 300 }Every /api route, per IP
rateLimit.auth{ windowMs: 900000, max: 10 }Login, register, password routes, per IP and route
auth.enabledtrueAuth also needs a user resource
auth.registertruefalse closes public sign-up
auth.sessionTtlDays30
auth.resetTokenTtlMinutes30
auth.resetUrl<app.url>/reset-passwordThe page of your frontend that receives ?token=
auth.password{ logN: 15, r: 8, p: 3 }scrypt cost; length rules live on f.password() (8 to 128 characters)
auth.lockout{ maxAttempts: 5, windowMinutes: 15, lockMinutes: 15 }Per account lockout after failed sign-ins; maxAttempts: 0 turns it off
auth.devicesFollowSessionstrueRemove push devices when the session that registered them ends (see Devices follow sessions)
encryption.keysCALM_ENCRYPTION_KEYSKeys for .encrypted() fields. Keep them in the environment, not in this file.
openapi.enabledtrueServe the OpenAPI document
openapi.path'/openapi.json'Relative to apiPrefix
devtools.enabledtrue in developmentForced off in production
log.levelLOG_LEVEL, else info (silent in tests)debug, info, warn, error, silent
log.formatjson in production, else pretty
shutdownTimeoutMs10000How long open connections may finish on shutdown
mail.from<app.name> <no-reply@localhost>Sender for app emails
blocks{}Settings per block, merged over the block's defaults: blocks: { webhooks: { maxEndpointsPerUser: 5 } }. See each block's page.
pluginsnoneExtra plugins (definePlugin(...) objects) besides app/blocks

Read the effective configuration anywhere through ctx.config or app.config.

Environment variables#

VariableUsed for
NODE_ENVdevelopment or test; anything else (or unset) runs as production
MONGODB_URIDatabase connection string (required)
APP_URLPublic URL (required in production)
PORT, HOSTWhere to listen (default 5001, all interfaces)
CORS_ORIGINSComma separated allowed origins, for example https://app.example.com,https://admin.example.com
TRUST_PROXYSet behind a load balancer, usually 1
LOG_LEVELdebug, info, warn, error, silent
CALM_SKIP_ENV_FILE1 to ignore .env
CALM_MAIL_PRINT1 to print email bodies to the console in development
CALM_ENCRYPTION_KEYSKeys for .encrypted() fields: k2:<base64>,k1:<base64>, the first one encrypts. Create with npx calm keygen --write (see Encryption at rest)
MONGODB_TEST_URIServer used by createTestApp and calm audit (default mongodb://127.0.0.1:27017)
CALM_TEST_LOGLog level inside tests, for example info
CALM_DEBUGPrint stack traces for CLI errors

Blocks add their own variables (for example JOBS_WORKER, RESEND_API_KEY); calm add appends them to .env.example and .env. Blocks declare the variables they read, so calm doctor reports required ones that are missing, and the devtools Blocks panel shows which are set (never the values of secret ones).

INI
# .env (never commit it)
MONGODB_URI=mongodb://127.0.0.1:27017/my_api
APP_URL=http://localhost:5001
PORT=5001
# CORS_ORIGINS=https://app.example.com
# TRUST_PROXY=1
# LOG_LEVEL=info