Back to docs
Guide 03

Configuration

Simmetric Chat has two configuration levels: environment variables validated with Zod at process startup, and a runtime config layer stored in the database that admins can edit from the UI without restarting any service.

4 min read

Simmetric Chat has two configuration levels: environment variables validated with Zod at process startup, and a runtime config layer stored in the database that admins can edit from the UI without restarting any service.

  1. Environment variables (process startup) - validated with Zod in packages/<pkg>/src/config/env.ts; process.exit(1) when invalid.
  2. Runtime config (DB `SystemConfig`) - editable from the admin UI without a restart.

Resolution:

  • ALWAYS_READONLY keys (JWT_SECRET, DATABASE_URL, SERVER_PORT, COLLECTOR_PORT, SERVER_URL, COLLECTOR_URL): ENV > Default, not editable from the UI.
  • All the others: DB > ENV > Default, editable via PUT /api/system/settings.

The three packages read .env via path.resolve(__dirname, "../../.env") (independent of the cwd). The reference document is .env.example at the repo root.

Environment variables - server

Schema: packages/server/src/config/env.ts. Defaults shown; REQ = required.

Core / infra

VariableDefaultReq.Description
NODE_ENVdevelopmentoptdevelopment/production/test
SERVER_PORT3000optServer API port
COLLECTOR_PORT3210optCollector port (for the default URL)
SERVER_URLhttp://localhost:3000optPublic server URL
COLLECTOR_URLhttp://localhost:3210optCollector URL
WIDGET_SERVICE_URLhttp://localhost:3211optWidget service URL (push cache-bust)
WIDGET_API_KEY-optWidget push shared secret; when absent, cache-bust is a no-op (5 min TTL)
DATABASE_URLpostgresql://simmetricchat:[email protected]:5432/simmetricchatoptPostgreSQL URL
JWT_SECRET-REQJWT secret
ENCRYPTION_KEY-optBase64 of 32 bytes for AES-256-GCM (data-at-rest). Unset → fallback scryptSync(JWT_SECRET, salt)
SESSION_EXPIRY86400000 (24h ms)optJWT expiry
ALLOWED_ORIGINShttp://localhost:5173,127.0.0.1:5173,localhost:3000optGlobal CORS allowlist (CSV). /api/internal/widget excluded
LOG_LEVELinfooptdebug/info/warn/error
ALLOW_REGISTRATIONfalseoptSelf-registration toggle
DISABLE_TELEMETRYtrueoptDisables telemetry

Bootstrap admin (seed)

VariableDefaultDescription
SEED_BOOTSTRAP_ADMINtrueAuto-seed the admin on first startup (custom transform: false/0/no/off/"" = disabled)
SEED_ADMIN_USERNAMEadminBootstrap admin username
SEED_ADMIN_PASSWORDadmin123Password (min 8); account carries mustChangePassword=true
SEED_ADMIN_EMAIL[email protected]Bootstrap admin email

LLM

VariableDefaultDescription
LLM_PROVIDERollamaopenai/anthropic/ollama/openrouter
LLM_MODELgemma4:latestDefault model
LLM_TEMPERATURE0.70–2
LLM_MAX_TOKENS4096Completion token cap
LLM_TIMEOUT0Axios timeout ms (0 = none, for slow local LLMs)
LLM_API_KEY / LLM_API_BASE_URL-Generic overrides
OPENAI_API_KEY / OPENAI_MODEL-OpenAI
ANTHROPIC_API_KEY / ANTHROPIC_MODEL-Anthropic
OLLAMA_BASE_URLhttp://ollama:11434Ollama endpoint
OLLAMA_MODEL / OLLAMA_API_KEY-Ollama override
OPENROUTER_API_KEY / OPENROUTER_BASE_URL / OPENROUTER_MODELhttps://openrouter.ai/api / -OpenRouter

Agent watchdog (loop/stall prevention)

VariableDefaultDescription
AGENT_WALLCLOCK_TIMEOUT_MS600000 (10 min)Absolute upper bound for a ReAct request
AGENT_MAX_TOTAL_TOKENS200000Prompt+completion total per request
AGENT_MAX_CONTEXT_BYTES500000 (500 KB)Context array size cap
AGENT_MAX_TOOL_OUTPUT_LENGTH5000Per-skill output truncation
AGENT_MAX_SKILL_EXECUTION_MS60000Per-skill timeout
AGENT_LOOP_DETECTION_WINDOW3Abort after N identical consecutive tool+input
CHAT_MAX_CONCURRENT_PER_USER5Per-user chat concurrency limit

Embedding / Vector DB / OCR / Synthesis

VariableDefaultDescription
EMBEDDING_PROVIDERlocallocal/openai/ollama
EMBEDDING_MODELXenova/all-MiniLM-L6-v2Embedding model
EMBEDDING_API_KEY-Embedding API key
VECTOR_DB_PROVIDERlancedblancedb/qdrant
VECTOR_DB_URL / VECTOR_DB_API_KEY-For remote Qdrant
OCR_MODELglm-ocr:latestVision OCR model (model:version format)
OCR_TIMEOUT600000Vision OCR timeout
SYNTHESIS_LLM_MODELgemma4:latestSynthesis pipeline model
PUPPETEER_EXECUTABLE_PATH-Chromium for archive PDF export
WIKI_EMBED_TIMEOUT_MS1800000 (30 min)Server→collector POST /api/ingest/wiki-pages wait cap for the hourly wiki-consistency reindex; raise for very large pages on slow hosts; 0 disables the cap (ENV-only)

License, push, email, collector

VariableDefaultDescription
LICENSE_KEY-Enterprise license JWT RS256 (vendor-issued; public key embedded in the server source). Absent → Community; the no-.env alternative = the admin-UI paste / PUT /api/license/instance (F-LIC-1)
COLLECTOR_SECRET-REQ - server↔collector shared secret
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT-Web Push; when unset, ephemeral keys are generated in dev
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM-SMTP for password reset + backup failure notifications
LEGACY_PREVIOUS_ENCRYPTION_KEYS-CSV base64 32-byte keys for key rotation
Variables in .env.example but managed as DB settings (not in envSchema): OCR_ENABLED, OCR_PRECHECK_CHARS, STORAGE_PATH (collector), RESET_PASSWORD_STRATEGY.

Environment variables - collector

Schema: packages/collector/src/config/env.ts.

VariableDefaultReq.Description
COLLECTOR_PORT3210optHTTP port
COLLECTOR_URLhttp://localhost:3210optOwn URL
SERVER_URLhttp://localhost:3000optServer URL (callback)
EMBEDDING_PROVIDERlocaloptlocal/openai/ollama
EMBEDDING_MODEL-optEmbedding model
EMBEDDING_API_KEY-optAPI key
VECTOR_DB_PROVIDERlancedboptlancedb/qdrant
VECTOR_DB_URL / VECTOR_DB_API_KEY-optQdrant
OLLAMA_BASE_URLhttp://ollama:11434optOllama
STORAGE_PATH./storageoptFilesystem storage
COLLECTOR_SECRET-REQMust match the server

Environment variables - widget

Schema: packages/widget/src/config/env.ts.

VariableDefaultReq.Description
NODE_ENVdevelopmentoptEnvironment
WIDGET_PORT3211optHTTP port
SERVER_URLhttp://localhost:3000optServer URL
WIDGET_API_KEY-REQWidget↔server shared secret (must match)
LOG_LEVELinfooptLog level
Frontend: no VITE_* variables and no .env file. The frontend reads branding/config
from the server (GET /api/system/settings, GET /api/license/info).

Feature flags & license

Defined in packages/shared/src/constants/license.ts (FEATURE_FLAGS + defaults). Verified in packages/server/src/services/licenseService.ts.

How to move from Community to Enterprise

  1. Set LICENSE_KEY (a vendor-issued RS256 JWT; the public key is embedded in the server source - packages/server/src/services/license-public-key.ts) - or, without touching .env and without a restart, paste it in Settings → Security → License or PUT /api/license/instance (F-LIC-1).
  2. The payload is validated against licensePayloadSchema (tier, iss, sub, iat, exp, features); probe-first verification: a rejected key is never stored.
  3. tier=enterprise → starts from ENTERPRISE_FEATURE_DEFAULTS + the JWT features overrides.
  4. Source precedence (identical at boot and runtime): the INSTANCE_LICENSE slot (AES-256-GCM ciphertext) → verified plugin row → ENV LICENSE_KEY → Community.
  5. Expired/invalid → Community fallback with a warning.

Graceful degradation: getLicenseInfo() checks expiry at runtime; if Enterprise expires while in use, it degrades to Community without a restart. requireFeature(flag) → HTTP 402 when disabled; requireFeatureLimit(flag, model) → 402 when count >= limit (Enterprise = Infinity).

Always-on commodity flags: since the EPA-02 removal the 8 commodity flags no longer exist in FEATURE_FLAGS - webhooks, push_notifications, web search, memory, lead_export, widget_analytics, auto_title, synthesis_rate_limit are always on in Community (commodity routes run without gating). priority_support is not a flag: it is a commercial/SLA contract (EPA-08). 14 flags remain, including widget_credits_editing (widget credits editing, Community false / Enterprise true), custom_agents as a numeric limit (Community 3, Enterprise Infinity), max_skills (Community 3, Enterprise Infinity, F-SKILL-1), max_users (Community 5, Enterprise Infinity - enforced at user creation) and connectors_enabled (Community false, F-CONN-1).

Flag table (14)

FlagTypeCommunityEnterpriseWhat it gates
sso_enabledboolfalsetrueEnterprise SSO
audit_log_immutableboolfalsetrueAudit log immutability
white_labelboolfalsetrueBRANDING_* branding
max_workspacesnum3InfinityWorkspace creation
max_projectsnum3InfinityProject creation
custom_agentsnum3InfinityCustom agents (limit)
widget_enabledboolfalsetrueEmbeddable widget
max_widgetsnum1InfinityWidget creation
backup_enabledboolfalsetrueBackup system
max_backup_destinationsnum1InfinityBackup destinations
max_usersnum5InfinityUsers (per-license seats)
connectors_enabledboolfalsetrueChat connectors (F-CONN-1)
max_skillsnum3InfinityCustom skills (F-SKILL-1)
widget_credits_editingboolfalsetrueWidget credits editing

Runtime config (DB SystemConfig)

Editable via GET/PUT /api/system/settings (bulk) and PUT /api/system/chat-retention (specific, audited). Defaults live in CONFIG_DEFAULTS (packages/shared/src/constants/permissions.ts).

Key groups: LLM (LLM_PROVIDER, LLM_MODEL, LLM_TEMPERATURE, ...), Embedding/Vector, Server ports/URLs, Auth (SESSION_EXPIRY), Branding (BRANDING_APP_NAME, BRANDING_PRIMARY_COLOR, BRANDING_APP_SUBTITLE, BRANDING_APP_ICON_URL - gated by white_label), Feature (DISABLE_TELEMETRY, ALLOW_REGISTRATION, DLP_ENABLED), OCR (OCR_DEFAULT_MODEL, OCR_DEFAULT_MODE, OCR_DEFAULT_CUSTOM_INSTRUCTIONS, OCR_ENABLED, OCR_PRECHECK_CHARS), Synthesis (SYNTHESIS_LLM_PROVIDER_ID, SYNTHESIS_LLM_MODEL), Retention (upload_draft_retention_days, ALLOW_NON_ADMIN_UPLOAD, chat_message_retention_days).

PUT /api/system/settings always returns 200 with { updated: [...], rejected: [...] } (readonly keys are rejected while the others are applied).

Chat message retention (reaper)

Service: packages/server/src/services/chatMessageReaperJob.ts. DB knob: chat_message_retention_days.

  • Pass 1 (soft-delete): tombstones messages of ACTIVE chats beyond retentionDays. No-op when the value is null/""/<=0.
  • Pass 2 (hard-purge): deletes tombstoned rows beyond a 7-day grace period (hardcoded). Always runs, regardless of the retention config (anti-PII leak).
  • Cadence: 24h setInterval, first run at 03:00 local time. isRunning mutex.
  • Only PUT /api/system/chat-retention (audited, confirmDataLoss: true, retentionDays: integer|null, min 1); the bulk settings endpoint rejects this key.

Backup retention

Per-job (the retentionDays field on BackupJob, default 30). backupRetentionService.ts cleans logs/files beyond cutoffDate. It is not global.

Encryption key rotation

packages/server/src/services/encryptionService.ts (AES-256-GCM, 32-byte key, 16-byte IV, salt simmetric-chat-encryption-salt).

  • Current key: ENCRYPTION_KEY (base64 32 bytes) when set; else scryptSync(JWT_SECRET, salt, 32).
  • LEGACY_PREVIOUS_ENCRYPTION_KEYS (CSV base64 32-byte) → decrypt key chain [current, ...previous, scryptLegacy?].
  • encrypt() uses chain[0]. There is no automatic re-encryption job: the procedure is a manual decrypt-with-old → re-encrypt-with-new.

v0.28.1 Provenance

StatementSource
LICENSE_KEY is RS256 (no LICENSE_SECRET - the public key is embedded)docs/CONFIGURATION.md (LICENSE_KEY row, row-time) + docs/ADMIN.md (Enterprise License) - F-LIC-1
ALWAYS_READONLY = 6 infra keys (LICENSE_SECRET no longer readonly - it no longer exists)docs/CONFIGURATION.md:42 (row-time)
Feature flags = 14 (+max_users 5/∞, connectors_enabled false/true, max_skills 3/∞)packages/shared/src/constants/license.ts (row-time: FEATURE_FLAGS ×14 + COMMUNITY/ENTERPRISE defaults) - F-SKILL-1, F-CONN-1
WIKI_EMBED_TIMEOUT_MS 1800000 (ENV-only)docs/CONFIGURATION.md:107 (row-time)

Operational reference - Simmetric Chat v0.28.1 (docs updated 2026-10-02, census SHA `aca5b8ff`).