Configuration

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 TypeWhere StoredExamples
Infrastructure & Secrets.env file / environment variablesDatabase URL, JWT secrets, CORS
Game & Feature SettingsDatabase (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

VariableTypeDefaultRequiredDescription
NODE_ENVdevelopment | production | testdevelopmentNoDeployment environment. Controls CORS validation, ENCRYPTION_KEY requirement, logging verbosity
PORTnumber3000NoHTTP port the server listens on
HOSTstringlocalhostNoHostname shown in the startup log. The server listens on all interfaces regardless
APP_VERSIONstring1.0.0NoVersion reported to OpenTelemetry. The /health version always comes from package.json
TRUST_PROXYstringfalse in production, 1 otherwiseSet in productionExpress 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_MODEdocker | kubernetesdockerNokubernetes disables self-updates in favor of helm upgrade
LOG_LEVELstringdebug in development, info otherwiseNoLogging level. Read directly from the environment

Database

VariableTypeDefaultRequiredDescription
DATABASE_URLstring—YesPostgreSQL connection string. Format: postgresql://user:password@host:5432/dbname?schema=public
DATABASE_POOL_SIZEpositive integer50 in production, 10 otherwiseNoConnection pool size. Ignored if DATABASE_URL contains connection_limit. See Database Pooling
DATABASE_POOL_TIMEOUTpositive integer10NoHow 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

VariableTypeDefaultRequiredDescription
JWT_ACCESS_SECRETstring (≥64 chars)—YesSecret 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_SECRETstring (≥64 chars)—YesSecret for signing refresh tokens. Use a different value from the access secret
JWT_2FA_TEMP_SECRETstring (≥64 chars)—YesSecret for signing temporary 2FA tokens issued during step-up auth
ADMIN_INITIAL_PASSWORDstring (12-128 chars, mixed case, number, special)—First empty production/staging bootstrap onlyPassword 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_INstring5mNoAccess token lifetime. Uses ms format: 5m, 1h, 1d
JWT_REFRESH_EXPIRES_INstring1dNoRefresh token lifetime
BCRYPT_ROUNDSinteger (4-31)12Nobcrypt work factor for password hashing. The deployment validator warns below 12

Security

VariableTypeDefaultRequiredDescription
CSRF_SECRETstring (≥32 chars)—YesSecret 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_KEYstring (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_KEYScomma-separated 64-char hex (max 3)—NoOld keys still accepted for decryption during key rotation. Remove after re-encrypting stored records
ALLOW_LEGACY_PLAINTEXT_SECRETSbooleanfalseNoAccept stored secrets that aren’t encrypted, for a one-off migration. Authentication failures still fail closed
RIGGED_DECK_CONTROLS_ENABLEDbooleanfalseNoTest-only rigged-deck admin controls. Rejected in production
RIGGED_DECK_CONTROLS_CONTEXTdisabled | local-test | cidisabledNoMust be local-test or ci when the controls are enabled

CORS

VariableTypeDefaultRequiredDescription
CORS_ORIGINScomma-separated URLs—YesAllowed 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

VariableTypeDefaultRequiredDescription
LICENSE_KEYstring (≥16 chars)—Yes (production install)Your license key. Without it the server starts in setup mode with game services disabled
LICENSE_DOMAINstring—Yes (production install)The bare domain players use, for example poker.example.com

Live Session Migration

VariableTypeDefaultRequiredDescription
LIVE_MIGRATION_ENABLEDbooleanfalseNoDrain active hands and persist table snapshots before stopping for an upgrade
LIVE_MIGRATION_DRAIN_WINDOW_MSpositive integer90000NoHow long to let active hands finish
LIVE_MIGRATION_GRACE_MSpositive integer10000NoExtra time after the drain window

Debugging Tools

VariableTypeDefaultRequiredDescription
QUERY_MONITORbooleanfalseNoEnable N+1 query detection. Logs a warning when more than QUERY_MONITOR_THRESHOLD queries are executed per request
QUERY_MONITOR_THRESHOLDnumber10NoNumber of queries per request before triggering a warning
QUERY_MONITOR_VERBOSEbooleanfalseNoLog every query (very noisy — development only)
EVENT_TRACINGbooleanfalseNoEnable event pipeline tracing for game engine debugging

Redis

VariableTypeDefaultRequiredDescription
REDIS_ENABLEDbooleanfalseNoEnable Redis integration. Required for multi-instance deployments (Socket.IO pub/sub adapter)
REDIS_URLstring—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)

VariableTypeDefaultRequiredDescription
SENTRY_ENABLEDbooleanfalseNoEnable Sentry error tracking
SENTRY_DSNstring—No (required if SENTRY_ENABLED=true)Sentry Data Source Name from your Sentry project settings
SENTRY_ENVIRONMENTstringNODE_ENV valueNoSentry environment tag (e.g., production, staging)
SENTRY_RELEASEstring—NoRelease identifier for Sentry (e.g., git commit SHA or version tag)
SENTRY_TRACES_SAMPLE_RATEnumber0.1NoFraction of transactions to trace (0.0–1.0). 0.1 = 10% sampling

Observability

VariableTypeDefaultRequiredDescription
METRICS_SCRAPE_TOKENstring (≥32 chars)—NoEnables GET /metrics/prometheus, authenticated with this bearer token. Returns 404 when unset
OTEL_ENABLEDbooleanfalseNoEnable OpenTelemetry tracing
OTEL_EXPORTER_OTLP_ENDPOINTstring—NoOTLP collector endpoint
OTEL_TRACES_SAMPLER_ARGstring0.1NoTrace sampling ratio
RUNTIME_INVARIANTS_ENABLEDbooleantrueNoRuntime hand-completion invariant checks
LOAD_PROBE_ENABLEDbooleanOff in production, on otherwiseNoEnables GET /metrics/load-probe
LOAD_PROBE_TOKENstring (≥32 chars)—If the probe is enabled in productionSent in the X-Load-Probe-Token header

SMS

VariableTypeDefaultRequiredDescription
SMS_ENABLEDbooleanfalseNoEnable SMS. Only the mock sender ships, so it can’t deliver real messages
SMS_PROVIDERmockmockNoAny other value fails validation

Dead Letter Queue

VariableTypeDefaultRequiredDescription
DLQ_ACTIVE_RETENTION_DAYSnumber7NoHow many days to keep active (unresolved) DLQ entries
DLQ_ARCHIVE_RETENTION_DAYSnumber90NoHow many days to keep archived (resolved) DLQ entries
DLQ_ALERT_THRESHOLDnumber50NoNumber of active DLQ entries before triggering an alert

Other Variables

These are read directly from the environment and aren’t validated at startup.

VariableDefaultDescription
FRONTEND_URLhttp://localhost:3000 for Stripe, https://app.example.com for account emailsPublic 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_DIRstorage/kyc-documents under the working directoryDirectory for KYC documents
KYC_STORAGE_URLencrypted-storage://kyc-documentsBase storage reference for KYC documents
KYC_ADMIN_EMAILkyc-review@poker-engine.localAddress for KYC review notifications
FRAUD_SWEEP_INTERVAL_MS3600000Fraud sweep interval, between one minute and one day
PUSH_NOTIFICATIONS_ENABLEDfalseEnable mobile push notifications
PUSH_PROVIDERmockfirebase 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 KeyLabel
cashierCashier
tournamentsTournaments
privateGamesPrivate Games
chatChat
bonusesBonuses
affiliatesAffiliates
agentsAgents
virtualChipsVirtual Chips
realMoneyReal Money
registrationRegistration
emailVerificationEmail Verification
twoFactorAuthTwo-Factor Auth
maintenanceMaintenance Mode
botsBots
gameHistoryGame History
fraudAlertsFraud Alerts
kycVerificationKYC Verification
amlReviewsAML Reviews
chargebacksChargebacks
couponsCoupons
gameSchedulerGame Scheduler
developerApiDeveloper 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
Try the live demo Pricing

Have questions about Poker Engine? We reply fast.