This document covers all environment variables, admin panel settings, and the settings persistence system.
Overview: Two Configuration Systems
The engine separates infrastructure configuration (env vars) from runtime configuration (admin panel / database):
| Config Type | Where Stored | Examples |
|---|---|---|
| Infrastructure & Secrets | .env file / environment variables | Database URL, JWT secrets, CORS |
| Game & Feature Settings | Database (admin panel) | Game limits, SMTP, payment providers, feature flags |
Game settings, feature flags, SMTP, and payment configuration are managed via the Admin Panel — not via environment variables. This allows operators to change these settings without redeploying.
Environment Variables
The variables in src/config/env-schema.ts are validated at startup using Zod. If validation fails, the server refuses to start with a descriptive error message. A few others are read directly from the environment; they’re listed under Other Variables.
Application
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
NODE_ENV | development | production | test | development | No | Deployment environment. Controls CORS validation, ENCRYPTION_KEY requirement, logging verbosity |
PORT | number | 3000 | No | HTTP port the server listens on |
HOST | string | localhost | No | Hostname shown in the startup log. The server listens on all interfaces regardless |
APP_VERSION | string | 1.0.0 | No | Version reported to OpenTelemetry. The /health version always comes from package.json |
TRUST_PROXY | string | false in production, 1 otherwise | Set in production | Express trust proxy setting: false, true, a hop count such as 1, or a subnet label such as loopback. Set 1 behind exactly one reverse proxy |
DEPLOYMENT_MODE | docker | kubernetes | docker | No | kubernetes disables self-updates in favor of helm upgrade |
LOG_LEVEL | string | debug in development, info otherwise | No | Logging level. Read directly from the environment |
Database
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
DATABASE_URL | string | — | Yes | PostgreSQL connection string. Format: postgresql://user:password@host:5432/dbname?schema=public |
DATABASE_POOL_SIZE | positive integer | 50 in production, 10 otherwise | No | Connection pool size. Ignored if DATABASE_URL contains connection_limit. See Database Pooling |
DATABASE_POOL_TIMEOUT | positive integer | 10 | No | How long (seconds) to wait for an available connection before failing |
Production warning: If DATABASE_URL contains the string password@, the server logs a security warning (default password detected).
Authentication
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
JWT_ACCESS_SECRET | string (≥64 chars) | — | Yes | Secret for signing access tokens. Must not contain a placeholder such as change-me, default or your-secret. Generate: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" |
JWT_REFRESH_SECRET | string (≥64 chars) | — | Yes | Secret for signing refresh tokens. Use a different value from the access secret |
JWT_2FA_TEMP_SECRET | string (≥64 chars) | — | Yes | Secret for signing temporary 2FA tokens issued during step-up auth |
ADMIN_INITIAL_PASSWORD | string (12-128 chars, mixed case, number, special) | — | First empty production/staging bootstrap only | Password assigned to the bootstrap admin account when no admin exists. Required outside explicit development/test bootstrap environments; rejected if weak or if it contains a known default such as password or admin123 |
JWT_ACCESS_EXPIRES_IN | string | 5m | No | Access token lifetime. Uses ms format: 5m, 1h, 1d |
JWT_REFRESH_EXPIRES_IN | string | 1d | No | Refresh token lifetime |
BCRYPT_ROUNDS | integer (4-31) | 12 | No | bcrypt work factor for password hashing. The deployment validator warns below 12 |
Security
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
CSRF_SECRET | string (≥32 chars) | — | Yes | Secret for CSRF token signing. Must not contain a placeholder such as change-me or default. Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" |
ENCRYPTION_KEY | string (exactly 64 hex chars) | — | Yes (production) | AES-256-GCM key for encrypting sensitive data at rest (TOTP secrets, API keys). Required in production. Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" |
ENCRYPTION_PREVIOUS_KEYS | comma-separated 64-char hex (max 3) | — | No | Old keys still accepted for decryption during key rotation. Remove after re-encrypting stored records |
ALLOW_LEGACY_PLAINTEXT_SECRETS | boolean | false | No | Accept stored secrets that aren’t encrypted, for a one-off migration. Authentication failures still fail closed |
RIGGED_DECK_CONTROLS_ENABLED | boolean | false | No | Test-only rigged-deck admin controls. Rejected in production |
RIGGED_DECK_CONTROLS_CONTEXT | disabled | local-test | ci | disabled | No | Must be local-test or ci when the controls are enabled |
CORS
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
CORS_ORIGINS | comma-separated URLs | — | Yes | Allowed origins for cross-origin requests. Example: https://poker.example.com,https://admin.example.com. In production: cannot contain localhost or 127.0.0.1 |
License
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
LICENSE_KEY | string (≥16 chars) | — | Yes (production install) | Your license key. Without it the server starts in setup mode with game services disabled |
LICENSE_DOMAIN | string | — | Yes (production install) | The bare domain players use, for example poker.example.com |
Live Session Migration
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
LIVE_MIGRATION_ENABLED | boolean | false | No | Drain active hands and persist table snapshots before stopping for an upgrade |
LIVE_MIGRATION_DRAIN_WINDOW_MS | positive integer | 90000 | No | How long to let active hands finish |
LIVE_MIGRATION_GRACE_MS | positive integer | 10000 | No | Extra time after the drain window |
Debugging Tools
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
QUERY_MONITOR | boolean | false | No | Enable N+1 query detection. Logs a warning when more than QUERY_MONITOR_THRESHOLD queries are executed per request |
QUERY_MONITOR_THRESHOLD | number | 10 | No | Number of queries per request before triggering a warning |
QUERY_MONITOR_VERBOSE | boolean | false | No | Log every query (very noisy — development only) |
EVENT_TRACING | boolean | false | No | Enable event pipeline tracing for game engine debugging |
Redis
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
REDIS_ENABLED | boolean | false | No | Enable Redis integration. Required for multi-instance deployments (Socket.IO pub/sub adapter) |
REDIS_URL | string | — | No (required if REDIS_ENABLED=true) | Redis connection string. Format: redis://user:password@host:6379 |
When REDIS_ENABLED=true, Developer API HMAC request-ID deduplication uses Redis as shared replay storage and fails closed with HTTP 503 if Redis is unavailable. Per-key Developer API rate limiting also uses Redis when available, but deliberately falls back to process-local counters during Redis outages so availability is preserved without weakening replay protection.
Error Tracking (Sentry)
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
SENTRY_ENABLED | boolean | false | No | Enable Sentry error tracking |
SENTRY_DSN | string | — | No (required if SENTRY_ENABLED=true) | Sentry Data Source Name from your Sentry project settings |
SENTRY_ENVIRONMENT | string | NODE_ENV value | No | Sentry environment tag (e.g., production, staging) |
SENTRY_RELEASE | string | — | No | Release identifier for Sentry (e.g., git commit SHA or version tag) |
SENTRY_TRACES_SAMPLE_RATE | number | 0.1 | No | Fraction of transactions to trace (0.0–1.0). 0.1 = 10% sampling |
Observability
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
METRICS_SCRAPE_TOKEN | string (≥32 chars) | — | No | Enables GET /metrics/prometheus, authenticated with this bearer token. Returns 404 when unset |
OTEL_ENABLED | boolean | false | No | Enable OpenTelemetry tracing |
OTEL_EXPORTER_OTLP_ENDPOINT | string | — | No | OTLP collector endpoint |
OTEL_TRACES_SAMPLER_ARG | string | 0.1 | No | Trace sampling ratio |
RUNTIME_INVARIANTS_ENABLED | boolean | true | No | Runtime hand-completion invariant checks |
LOAD_PROBE_ENABLED | boolean | Off in production, on otherwise | No | Enables GET /metrics/load-probe |
LOAD_PROBE_TOKEN | string (≥32 chars) | — | If the probe is enabled in production | Sent in the X-Load-Probe-Token header |
SMS
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
SMS_ENABLED | boolean | false | No | Enable SMS. Only the mock sender ships, so it can’t deliver real messages |
SMS_PROVIDER | mock | mock | No | Any other value fails validation |
Dead Letter Queue
| Variable | Type | Default | Required | Description |
|---|---|---|---|---|
DLQ_ACTIVE_RETENTION_DAYS | number | 7 | No | How many days to keep active (unresolved) DLQ entries |
DLQ_ARCHIVE_RETENTION_DAYS | number | 90 | No | How many days to keep archived (resolved) DLQ entries |
DLQ_ALERT_THRESHOLD | number | 50 | No | Number of active DLQ entries before triggering an alert |
Other Variables
These are read directly from the environment and aren’t validated at startup.
| Variable | Default | Description |
|---|---|---|
FRONTEND_URL | http://localhost:3000 for Stripe, https://app.example.com for account emails | Public site URL for Stripe return links and account-deletion emails. Set it in production if you use these |
FEATURE_BOTS | — | Set to false to stop bots playing, whatever the Bots toggle says |
SAFE_MODE | — | Set to true to skip loading all plugins |
PAYMENT_PLUGINS | — | Comma-separated payment plugin packages to load, in addition to those in plugins/ |
SANCTIONS_LIST_PATH | — | Path to a maintained sanctions JSON list with source, updatedAt and entries. It’s rejected if older than 48 hours. Without it, sanctions screening is unavailable |
KYC_STORAGE_DIR | storage/kyc-documents under the working directory | Directory for KYC documents |
KYC_STORAGE_URL | encrypted-storage://kyc-documents | Base storage reference for KYC documents |
KYC_ADMIN_EMAIL | kyc-review@poker-engine.local | Address for KYC review notifications |
FRAUD_SWEEP_INTERVAL_MS | 3600000 | Fraud sweep interval, between one minute and one day |
PUSH_NOTIFICATIONS_ENABLED | false | Enable mobile push notifications |
PUSH_PROVIDER | mock | firebase or mock |
FIREBASE_CREDENTIALS | — | Firebase credentials for the firebase provider |
APPLE_TEAM_ID | — | Required for iOS Universal Links |
ANDROID_CERT_SHA256 | — | Used for Android App Links |
POKER_ENGINE_TRUSTED_DOWNLOAD_ORIGINS | — | Used by the deployment scripts: additional exact HTTPS origins for signed release downloads |
docker-compose.prod.yml also reads GHCR_OWNER, POKER_ENGINE_VERSION, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, REDIS_PASSWORD, BACKUP_SCHEDULE and BACKUP_RETENTION_DAYS from .env.prod. It builds DATABASE_URL and REDIS_URL from them.
Generating Secrets
Use these commands to generate cryptographically secure secrets:
# JWT secrets (64 bytes = 128 hex chars, but 64 is minimum)
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
# CSRF secret (32 bytes = 64 hex chars, but 32 chars minimum)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Encryption key (exactly 32 bytes = 64 hex chars)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Environment Validation
Validate your environment without starting the server:
npm run env:validate # Validates all env vars
npm run env:validate:verbose # Also prints a summary of the parsed values (redacts secrets)
npx tsx scripts/validate-env.ts --env-file=.env.prod # Validates another file
The server validates all variables at import time. Any missing or invalid variable causes an immediate startup failure with a clear error message listing all problems.
Admin Panel Settings
The following categories of settings are managed through the Admin Panel UI (stored in the database) and can be changed at runtime without restarting the server.
Game Settings
Under Settings → Game:
- Move time, warning time and time bank
- Timeouts and disconnect handling
- Showdown behaviour
- Rake percentage and cap
Buy-ins, betting limits, blinds and seats are set per table under Tables.
Feature Flags
Feature flags control which features are enabled globally. They are managed under Settings → Features and checked via:
import { isFeatureEnabled } from './services/general-settings.service.js';
const enabled = await isFeatureEnabled('registration'); // true/false
The flag keys are defined by FEATURE_TOGGLE_KEYS in src/services/general-settings.service.ts:
| Flag Key | Label |
|---|---|
cashier | Cashier |
tournaments | Tournaments |
privateGames | Private Games |
chat | Chat |
bonuses | Bonuses |
affiliates | Affiliates |
agents | Agents |
virtualChips | Virtual Chips |
realMoney | Real Money |
registration | Registration |
emailVerification | Email Verification |
twoFactorAuth | Two-Factor Auth |
maintenance | Maintenance Mode |
bots | Bots |
gameHistory | Game History |
fraudAlerts | Fraud Alerts |
kycVerification | KYC Verification |
amlReviews | AML Reviews |
chargebacks | Chargebacks |
coupons | Coupons |
gameScheduler | Game Scheduler |
developerApi | Developer API |
The requireFeatureToggle(feature) middleware in src/api/middleware/auth.ts guards API routes based on feature flags and the license tier. A flag your plan doesn’t include returns FEATURE_TIER_RESTRICTED.
SMTP / Email Settings
- SMTP host, port, username, password (Settings → Server)
- From address and display name
- System emails, enabled or disabled per type (System Emails)
- Email template content (Email Templates)
Payment Providers
Under Settings → Cashier:
- Per-provider credentials (encrypted at rest using
ENCRYPTION_KEY) - Deposits and withdrawals enabled or disabled per provider
Registration Settings
- Account approval method, including email verification and manual admin approval
Server Settings
Under Settings → Server:
- Base URL and CDN URL
- Script paths
- Admin, support and no-reply email addresses
- Timezone
Settings Persistence System
Runtime settings are stored in the Setting model (settings table) as key-value pairs, unique per category and key. There’s no shared accessor: each area has its own service with typed functions, such as general-settings.service.ts, server-settings.service.ts and smtp-config.service.ts:
import { getGeneralSettings, updateGeneralSettings } from './services/general-settings.service.js';
const settings = await getGeneralSettings();
You don’t need to restart after changing settings in the Admin Panel. Some reads go through an in-memory cache (src/cache/settings-cache.ts) with a default TTL of 30 seconds. Updates invalidate the relevant entry.
Live Config Updates
Services can react to changes as they happen through configEventBus in src/events/config-events.ts, which emits events such as feature_toggles_changed and game_settings_changed.
.env.prod.example
A template for production environment variables is provided at .env.prod.example in the repository root and is included in each release tarball. Copy it to .env.prod and fill in your values. install.sh and the compose file read that name:
cp .env.prod.example .env.prod
# Edit .env.prod with your production values
Example .env for Development
NODE_ENV=development
PORT=3000
HOST=localhost
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/poker_engine?schema=public
JWT_ACCESS_SECRET=<generate, at least 64 chars>
JWT_REFRESH_SECRET=<generate, at least 64 chars>
JWT_2FA_TEMP_SECRET=<generate, at least 64 chars>
# ADMIN_INITIAL_PASSWORD is not needed for development seed/bootstrap defaults.
JWT_ACCESS_EXPIRES_IN=5m
JWT_REFRESH_EXPIRES_IN=1d
BCRYPT_ROUNDS=12
CSRF_SECRET=<generate, at least 32 chars>
# ENCRYPTION_KEY not required in development
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
QUERY_MONITOR=false
EVENT_TRACING=false
REDIS_ENABLED=false
SENTRY_ENABLED=false