Skip to content

Configuration

Config in Keryx is statically defined at boot — there's no dynamic config reloading. That said, every config value supports per-environment overrides via environment variables, so you can set things differently in test, development, and production without touching code.

Structure

Config is split into modules:

backend/config/
├── index.ts        # Aggregates everything into one `config` object
├── actions.ts      # Action timeout, fan-out batch size and TTL
├── channels.ts     # Presence TTL and heartbeat interval
├── database.ts     # Database connection string, auto-migrate flag
├── logger.ts       # Log level, timestamps, colors, output format (text/JSON)
├── observability.ts # OpenTelemetry metrics toggle, route, service name
├── process.ts      # Process name, shutdown timeout
├── rateLimit.ts    # Rate limiting windows and thresholds
├── redis.ts        # Redis connection string
├── session.ts      # Session TTL, cookie security flags
├── tasks.ts        # Task queue settings
└── server/
    ├── cli.ts      # CLI error display, quiet mode
    ├── web.ts      # Web server port, CORS, security headers, WS limits
    └── mcp.ts      # MCP server toggle, route, OAuth TTLs, markdown depth limit

Everything rolls up into a single config object:

ts
import { config } from "../config";

config.database.connectionString; // Postgres URL
config.server.web.port; // 8080
config.logger.level; // "info"

Custom Config

You can add your own config modules alongside the built-in ones. Create a new file in your config/ directory, then aggregate it in config/index.ts:

ts
// config/audit.ts
import { loadFromEnvIfSet } from "keryx";

export const configAudit = {
  retentionDays: await loadFromEnvIfSet("AUDIT_RETENTION_DAYS", 30),
  enabled: await loadFromEnvIfSet("AUDIT_ENABLED", true),
};
ts
// config/index.ts
import { configAudit } from "./audit";

export default {
  audit: configAudit,
};

At boot, Keryx deep-merges your config into the framework's config object, so config.audit.retentionDays works at runtime. To get full type safety, augment the KeryxConfig interface:

ts
// config/audit.ts (add at the bottom)
declare module "keryx" {
  interface KeryxConfig {
    audit: typeof configAudit;
  }
}

Now config.audit.retentionDays is fully typed everywhere you import config from "keryx" — no casts needed.

Environment Overrides

The loadFromEnvIfSet() helper is where the magic happens:

ts
import { loadFromEnvIfSet } from "../util/config";

export const configDatabase = {
  connectionString: await loadFromEnvIfSet("DATABASE_URL", "x"),
  autoMigrate: await loadFromEnvIfSet("DATABASE_AUTO_MIGRATE", true),
};

The resolution order is:

  1. DATABASE_URL_TEST (env var with NODE_ENV suffix — checked first)
  2. DATABASE_URL (plain env var)
  3. "x" (the default value)

This means you can set DATABASE_URL_TEST=postgres://localhost/bun-test and it'll automatically be used when NODE_ENV=test, without any conditional logic in your config files.

The helper is also type-aware — it parses "true"/"false" strings into booleans and numeric strings into numbers. So DATABASE_AUTO_MIGRATE=false does what you'd expect.

Reference

Actions

KeyEnv VarDefaultDescription
timeoutACTION_TIMEOUT300000 (5 min)Global action execution timeout in ms (0 = off)
fanOutBatchSizeACTION_FAN_OUT_BATCH_SIZE100Max jobs enqueued per Redis round-trip
fanOutResultTtlACTION_FAN_OUT_RESULT_TTL600 (10 min)TTL in seconds for fan-out result keys in Redis

Database

KeyEnv VarDefault
connectionStringDATABASE_URL"x"
autoMigrateDATABASE_AUTO_MIGRATEtrue

Advanced: Pool Tuning

The database connection pool defaults are suitable for development and most production workloads. If you need to tune pool behavior — for example, to match your database's max_connections limit or to reduce idle resource usage — you can override these settings via environment variables.

KeyEnv VarDefaultDescription
pool.maxDATABASE_POOL_MAX10Maximum number of connections in the pool
pool.minDATABASE_POOL_MIN0Minimum number of idle connections to maintain
pool.idleTimeoutMillisDATABASE_POOL_IDLE_TIMEOUT10000How long (ms) a connection can sit idle before being closed
pool.connectionTimeoutMillisDATABASE_POOL_CONNECT_TIMEOUT0Max time (ms) to wait for a connection from the pool (0 = no timeout)
pool.allowExitOnIdleDATABASE_POOL_EXIT_ON_IDLEfalseAllow the Node.js process to exit while idle connections remain open

All defaults match pg.Pool's built-in defaults, so existing deployments are unaffected. A common production override:

bash
DATABASE_POOL_MAX=25
DATABASE_POOL_MIN=5
DATABASE_POOL_IDLE_TIMEOUT=30000
DATABASE_POOL_CONNECT_TIMEOUT=5000

Logger

KeyEnv VarDefaultDescription
levelLOG_LEVEL"info"Minimum log level (trace, debug, info, warn, error, fatal)
includeTimestampsLOG_INCLUDE_TIMESTAMPStruePrepend ISO-8601 timestamp to each log line
colorizeLOG_COLORIZEtrueApply ANSI color codes (text format only)
formatLOG_FORMAT"text"Output format: "text" for human-readable, "json" for structured NDJSON
maxParamLengthLOG_MAX_PARAM_LENGTH100Max length of individual param values in action logs before truncation (0 = no limit)

In JSON mode, each log line is a single JSON object with timestamp, level, message, and pid fields. Action and task logs include additional structured fields like action, duration, status, method, url, correlationId, queue, and jobClass — making them easy to parse with log aggregation systems (ELK, Datadog, CloudWatch, Loki, etc.).

bash
# Enable JSON logging in production
LOG_FORMAT=json bun run start

Example JSON output:

json
{
  "timestamp": "2025-01-15T10:30:00.000Z",
  "level": "info",
  "message": "action: status",
  "pid": 12345,
  "action": "status",
  "connectionType": "web",
  "status": "OK",
  "duration": 12,
  "method": "GET",
  "url": "/api/status"
}

Redis

KeyEnv VarDefault
connectionStringREDIS_URL"redis://localhost:6379/0"

Session

KeyEnv VarDefaultDescription
ttlSESSION_TTL86400 (1 day in seconds)Session lifetime
cookieNameSESSION_COOKIE_NAME"__session"Cookie name
cookieHttpOnlySESSION_COOKIE_HTTP_ONLYtruePrevent JavaScript access
cookieSecureSESSION_COOKIE_SECUREfalseHTTPS-only cookies
cookieSameSiteSESSION_COOKIE_SAME_SITE"Strict"CSRF protection (Strict, Lax, None)

Process

KeyEnv VarDefault
namePROCESS_NAME"server"
shutdownTimeoutPROCESS_SHUTDOWN_TIMEOUT30000 (30s)

Web Server

KeyEnv VarDefault
enabledWEB_SERVER_ENABLEDtrue
portWEB_SERVER_PORT8080
hostWEB_SERVER_HOST"localhost"
applicationUrlAPPLICATION_URL"http://localhost:8080"
apiRouteWEB_SERVER_API_ROUTE"/api"
allowedOriginsWEB_SERVER_ALLOWED_ORIGINS"*"
allowedMethodsWEB_SERVER_ALLOWED_METHODS"HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS"
allowedHeadersWEB_SERVER_ALLOWED_HEADERS"Content-Type"
includeStackInErrorsWEB_SERVER_INCLUDE_STACK_IN_ERRORStrue (dev) / false (prod)

Static Files

KeyEnv VarDefaultDescription
staticFiles.enabledWEB_SERVER_STATIC_ENABLEDtrueEnable static file serving
staticFiles.directoryWEB_SERVER_STATIC_DIRECTORY"assets"Directory to serve files from
staticFiles.routeWEB_SERVER_STATIC_ROUTE"/"URL route prefix for static files
staticFiles.cacheControlWEB_SERVER_STATIC_CACHE_CONTROL"public, max-age=3600"Cache-Control header value
staticFiles.etagWEB_SERVER_STATIC_ETAGtrueEnable ETag/304 conditional caching

WebSocket

KeyEnv VarDefaultDescription
websocket.maxPayloadSizeWS_MAX_PAYLOAD_SIZE65536 (64 KB)Max message size in bytes
websocket.maxMessagesPerSecondWS_MAX_MESSAGES_PER_SECOND20Per-connection rate limit
websocket.maxSubscriptionsWS_MAX_SUBSCRIPTIONS100Max channel subscriptions per connection
websocket.drainTimeoutWS_DRAIN_TIMEOUT5000 (5 s)Graceful shutdown drain period

Compression

HTTP responses are automatically compressed when the client sends an Accept-Encoding header. Brotli is preferred over gzip. Responses below the threshold or with incompressible content types (images, video, etc.) are served uncompressed.

KeyEnv VarDefaultDescription
compression.enabledWEB_COMPRESSION_ENABLEDtrueEnable HTTP response compression
compression.thresholdWEB_COMPRESSION_THRESHOLD1024Minimum response size in bytes to compress
compression.encodings["br", "gzip"]Encoding preference order (brotli first)

Correlation IDs

KeyEnv VarDefaultDescription
correlationId.headerWEB_CORRELATION_ID_HEADER"X-Request-Id"Header name to read/echo (empty string to disable)
correlationId.trustProxyWEB_CORRELATION_ID_TRUST_PROXYfalseRead and echo the incoming correlation ID header from proxies

See the Security guide for details.

Security Headers

All HTTP responses include these headers. Each is configurable:

HeaderEnv VarDefault
Content-Security-PolicyWEB_SECURITY_CSPdefault-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; font-src 'self' https://cdn.jsdelivr.net data:; img-src 'self' data: blob:; connect-src 'self'; worker-src blob:
X-Content-Type-OptionsWEB_SECURITY_CONTENT_TYPE_OPTIONSnosniff
X-Frame-OptionsWEB_SECURITY_FRAME_OPTIONSDENY
Strict-Transport-SecurityWEB_SECURITY_HSTSmax-age=31536000; includeSubDomains
Referrer-PolicyWEB_SECURITY_REFERRER_POLICYstrict-origin-when-cross-origin

Channels

KeyEnv VarDefaultDescription
presenceTTLPRESENCE_TTL90Presence key TTL in seconds
presenceHeartbeatIntervalPRESENCE_HEARTBEAT_INTERVAL30Heartbeat interval in seconds to refresh presence

Observability

KeyEnv VarDefaultDescription
enabledOTEL_METRICS_ENABLEDfalseEnable OpenTelemetry metrics and /metrics route
metricsRouteOTEL_METRICS_ROUTE"/metrics"URL path for Prometheus scrape endpoint
serviceNameOTEL_SERVICE_NAME""Service name in metric labels

Tasks

KeyEnv VarDefault
enabledTASKS_ENABLEDtrue
timeoutTASK_TIMEOUT5000
taskProcessorsTASK_PROCESSORS1

Rate Limiting

See the Security guide for details on how rate limiting works.

KeyEnv VarDefault
enabledRATE_LIMIT_ENABLEDtrue (disabled in test)
windowMsRATE_LIMIT_WINDOW_MS60000 (1 min)
unauthenticatedLimitRATE_LIMIT_UNAUTH_LIMIT20
authenticatedLimitRATE_LIMIT_AUTH_LIMIT200
keyPrefixRATE_LIMIT_KEY_PREFIX"ratelimit"
oauthRegisterLimitRATE_LIMIT_OAUTH_REGISTER_LIMIT5
oauthRegisterWindowMsRATE_LIMIT_OAUTH_REGISTER_WINDOW_MS3600000 (1 hour)

CLI

KeyEnv VarDefault
includeStackInErrorsCLI_INCLUDE_STACK_IN_ERRORStrue
quietCLI_QUIETfalse

MCP Server

KeyEnv VarDefault
enabledMCP_SERVER_ENABLEDfalse
routeMCP_SERVER_ROUTE"/mcp"
oauthClientTtlMCP_OAUTH_CLIENT_TTL2592000
oauthCodeTtlMCP_OAUTH_CODE_TTL300
markdownDepthLimitMCP_MARKDOWN_DEPTH_LIMIT5

Released under the MIT License.