Skip to content

28 - Environment Configuration ​

Overview ​

Tradeboard uses environment variables for configuration, managed through a .env file with validation at startup. For cloud deployments (Railway/Render), the start.sh script can auto-generate .env from environment variables.

Configuration Files ​

.env                # Active configuration (not in git)
.sample.env         # Reference template with all variables

Environment Variables ​

Version Tracking ​

bash
# Configuration version - compare with .sample.env when updating
ENV_CONFIG_VERSION = '1.0.7'

Startup compares this value against .sample.env and refuses to boot when your .env is older. The values below reproduce .sample.env. Where a variable is optional, the default written in code can differ from the sample; those cases are called out inline.

Core Security (Required) ​

bash
# Application secret key (required, 32+ characters)
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
APP_KEY = 'your_32_character_secret_key_here'

# Security pepper for API key hashing, password hashing, token encryption
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
API_KEY_PEPPER = 'your_32_character_pepper_here'

# Per-install random salt feeding the Fernet KDF in database/auth_db.py
# Auto-generated on first run; never reuse across installs
FERNET_SALT = 'your_random_hex_salt_here'

.sample.env ships all three as named placeholders (TRADEBOARD_PLACEHOLDER_...). On first run utils/env_check.py detects them, generates fresh secrets.token_hex(32) values, writes them back to .env, and re-encrypts stored broker tokens, API keys, and TOTP secrets under the new salt.

Broker Configuration ​

bash
# Broker API credentials
BROKER_API_KEY = 'YOUR_BROKER_API_KEY'
BROKER_API_SECRET = 'YOUR_BROKER_API_SECRET'

# XTS API brokers only (5Paisa XTS, Jainam XTS, etc.)
BROKER_API_KEY_MARKET = 'YOUR_BROKER_MARKET_API_KEY'
BROKER_API_SECRET_MARKET = 'YOUR_BROKER_MARKET_API_SECRET'

# OAuth redirect URL
REDIRECT_URL = 'http://127.0.0.1:5000/<broker>/callback'

# Enabled brokers (comma-separated)
VALID_BROKERS = 'fivepaisa,fivepaisaxts,aliceblue,angel,arrow,compositedge,dhan,dhan_sandbox,definedge,deltaexchange,firstock,flattrade,fyers,groww,hdfcsecurities,hdfcsky,ibulls,iifl,iiflcapital,indmoney,jainamxts,kotak,motilal,mstock,nubra,paytm,pocketful,rmoney,samco,shoonya,tradejini,tradesmart,upstox,wisdom,zebu,zerodha'

Database Configuration ​

bash
# Main database
DATABASE_URL = 'sqlite:///db/tradeboard.db'

# Additional databases
LATENCY_DATABASE_URL = 'sqlite:///db/latency.db'
LOGS_DATABASE_URL = 'sqlite:///db/logs.db'
HEALTH_DATABASE_URL = 'sqlite:///db/health.db'
SANDBOX_DATABASE_URL = 'sqlite:///db/sandbox.db'
HISTORIFY_DATABASE_URL = 'db/historify.duckdb'

Historify uses two different variable names, and they are not aliases of each other. database/historify_db.py and the Historify migrations read HISTORIFY_DATABASE_PATH; .sample.env and blueprints/system_permissions.py use HISTORIFY_DATABASE_URL. Both fall back to the same db/historify.duckdb default, so a default install works either way, but pointing the DuckDB file somewhere else requires setting HISTORIFY_DATABASE_PATH.

app.py creates the SQLite directory from DATABASE_URL before any engine connects, so all SQLite databases are expected to live under the same directory.

Flask Application ​

bash
# Host and port
FLASK_HOST_IP = '127.0.0.1'  # Use 0.0.0.0 for external access
FLASK_PORT = '5000'

# Environment
FLASK_DEBUG = 'False'
FLASK_ENV = 'development'  # or 'production'

# Public URL
HOST_SERVER = 'http://127.0.0.1:5000'

WebSocket Configuration ​

bash
# WebSocket server
WEBSOCKET_HOST = '127.0.0.1'
WEBSOCKET_PORT = '8765'
WEBSOCKET_URL = 'ws://127.0.0.1:8765'

# Unauthenticated client grace window (seconds)
WS_AUTH_GRACE_SECONDS = '15'

# Per-client send buffer cap (pending messages)
WS_MAX_QUEUE = '1024'

# Server-initiated keepalive (seconds)
WS_PING_INTERVAL = '20'
WS_PING_TIMEOUT = '20'

# ZeroMQ message bus
ZMQ_HOST = '127.0.0.1'
ZMQ_PORT = '5555'

ZMQ_HOST must stay on loopback unless every process that publishes is on a trusted network: the proxy SUB socket binds this endpoint and every publisher connects to it, and the tick feed itself is unauthenticated.

Connection Pooling ​

bash
# Maximum symbols per WebSocket connection (default: 1000)
MAX_SYMBOLS_PER_WEBSOCKET = '1000'

# Maximum WebSocket connections per user/broker (default: 3)
# Total capacity = MAX_SYMBOLS_PER_WEBSOCKET × MAX_WEBSOCKET_CONNECTIONS
MAX_WEBSOCKET_CONNECTIONS = '3'

# Enable/disable connection pooling (default: true)
ENABLE_CONNECTION_POOLING = 'true'

Real-Time Order Updates ​

bash
# Always-on adapter per broker session ingesting broker order pushes
ORDER_UPDATES_ENABLED = 'TRUE'

# Fallback REST polling interval (seconds) for brokers without a push feed
ORDER_POLL_INTERVAL = '5'

Broker Connection Keepalive ​

bash
# Keep the pooled broker HTTP connection warm during market hours
BROKER_CONNECTION_KEEPALIVE = 'TRUE'
BROKER_KEEPALIVE_INTERVAL = '20'          # seconds, keep below 30
BROKER_KEEPALIVE_WINDOW = '09:00-23:30'   # IST window

Ngrok Configuration ​

bash
# Enable ngrok tunnel
NGROK_ALLOW = 'FALSE'

Logging Configuration ​

bash
# File logging
LOG_TO_FILE = 'False'
LOG_LEVEL = 'INFO'  # DEBUG, INFO, WARNING, ERROR, CRITICAL
LOG_DIR = 'log'
LOG_FORMAT = '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
LOG_RETENTION = '14'  # Days

# Days to retain request traffic logs in logs.db (purged at startup)
TRAFFIC_LOG_RETENTION_DAYS = '30'

# Color output
LOG_COLORS = 'True'
FORCE_COLOR = '1'

Health Monitoring ​

bash
# Memory thresholds in MB for the background health monitor
HEALTH_MEMORY_WARNING_THRESHOLD = '3000'
HEALTH_MEMORY_CRITICAL_THRESHOLD = '5000'

Python Strategy Logging ​

bash
# Maximum log files per strategy (oldest deleted first)
STRATEGY_LOG_MAX_FILES = '10'

# Maximum total log size per strategy in MB
STRATEGY_LOG_MAX_SIZE_MB = '50'

# Delete strategy logs older than N days
STRATEGY_LOG_RETENTION_DAYS = '7'

Rate Limiting ​

bash
# Login rate limits
LOGIN_RATE_LIMIT_MIN = '5 per minute'
LOGIN_RATE_LIMIT_HOUR = '25 per hour'
RESET_RATE_LIMIT = '15 per hour'

# API rate limits
API_RATE_LIMIT = '50 per second'
ORDER_RATE_LIMIT = '10 per second'
SMART_ORDER_RATE_LIMIT = '10 per second'

# Webhook rate limits
WEBHOOK_RATE_LIMIT = '100 per minute'
STRATEGY_RATE_LIMIT = '200 per minute'

API Configuration ​

bash
# Session expiry time (24-hour format, IST)
SESSION_EXPIRY_TIME = '03:00'

# Disable the daily rollover entirely (crypto brokers, 24/7 markets)
DISABLE_SESSION_EXPIRY = 'false'

# Master contract smart download cutoffs
MASTER_CONTRACT_CUTOFF_TIME = '08:00'         # IST, Indian exchange brokers
CRYPTO_MASTER_CONTRACT_CUTOFF_TIME = '00:00'  # UTC, crypto brokers

CORS Configuration ​

bash
# Enable/disable CORS
# .sample.env ships TRUE; the default in cors.py when the variable is
# absent is FALSE, which falls back to Flask-CORS defaults
CORS_ENABLED = 'TRUE'

# Allowed origins (comma-separated)
CORS_ALLOWED_ORIGINS = 'http://127.0.0.1:5000'

# Allowed HTTP methods
CORS_ALLOWED_METHODS = 'GET,POST,DELETE,PUT,PATCH'

# Allowed headers
CORS_ALLOWED_HEADERS = 'Content-Type,Authorization,X-Requested-With'

# Exposed headers
CORS_EXPOSED_HEADERS = ''

# Allow credentials (cookies, auth headers)
CORS_ALLOW_CREDENTIALS = 'FALSE'

# Preflight cache max age (seconds)
CORS_MAX_AGE = '86400'

Content Security Policy (CSP) ​

bash
# Enable/disable CSP
CSP_ENABLED = 'TRUE'

# Report-only mode (testing)
CSP_REPORT_ONLY = 'FALSE'

# CSP directives
CSP_DEFAULT_SRC = "'self'"
CSP_SCRIPT_SRC = "'self' 'unsafe-inline' https://cdn.socket.io https://static.cloudflareinsights.com"
CSP_STYLE_SRC = "'self' 'unsafe-inline'"
CSP_IMG_SRC = "'self' data:"
CSP_CONNECT_SRC = "'self' wss: ws: https://cdn.socket.io"
CSP_FONT_SRC = "'self'"
CSP_OBJECT_SRC = "'none'"
CSP_MEDIA_SRC = "'self' data: https://*.amazonaws.com https://*.cloudfront.net"
CSP_FRAME_SRC = "'self'"
CSP_FORM_ACTION = "'self'"
CSP_FRAME_ANCESTORS = "'self'"
CSP_BASE_URI = "'self'"
CSP_UPGRADE_INSECURE_REQUESTS = 'FALSE'
CSP_REPORT_URI = ''

# Optional, no sample entry: emitted only when set
CSP_CHILD_SRC = ''
CSP_REPORT_TO = ''

# Additional security headers set by csp.py
REFERRER_POLICY = 'strict-origin-when-cross-origin'
PERMISSIONS_POLICY = 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), screen-wake-lock=(), web-share=()'

Four CSP directives are looser in .sample.env than the fallback defaults in csp.py. When the variable is absent, csp.py uses script-src "'self' https://cdn.socket.io", img-src "'self' data: blob:", connect-src "'self' wss: ws:", and media-src "'self'". X-Frame-Options: DENY, X-Content-Type-Options: nosniff, and X-XSS-Protection: 1; mode=block are hard-coded and not configurable. A route that sets its own CSP header, such as the OAuth consent page, is not overwritten.

CSRF Protection ​

bash
# Enable/disable CSRF protection
CSRF_ENABLED = 'TRUE'

# Token time limit (seconds, empty = no limit)
CSRF_TIME_LIMIT = ''
bash
# Cookie names (customize for multiple instances)
SESSION_COOKIE_NAME = 'session'
CSRF_COOKIE_NAME = 'csrf_token'

When HOST_SERVER starts with https://, app.py marks both cookies Secure and prefixes their names with __Secure-.

Reverse Proxy Trust ​

bash
# Trust CF-Connecting-IP / True-Client-IP / X-Real-IP / X-Forwarded-For
# Only enable when a reverse proxy actually sits in front of gunicorn
TRUST_PROXY_HEADERS = 'FALSE'

With the default FALSE, utils/ip_helper.get_real_ip() returns request.remote_addr and ignores forwarded headers entirely, so IP bans, per-IP rate limits, and the login audit log cannot be spoofed by a client that reaches gunicorn directly.

Remote MCP ​

bash
# Master switch. Off by default; both /mcp and /oauth/* stay unregistered
MCP_HTTP_ENABLED = 'False'

# Required when enabled: canonical HTTPS origin, anchors JWT iss/aud
MCP_PUBLIC_URL = ''

MCP_OAUTH_REQUIRE_APPROVAL = 'False'
MCP_OAUTH_WRITE_SCOPE_ENABLED = 'True'
MCP_HTTP_CORS_ORIGINS = 'https://claude.ai,https://chatgpt.com'

Enabling Remote MCP with FLASK_DEBUG on, or without MCP_PUBLIC_URL, raises at startup. Advanced keys that are commented out in .sample.env and fall back to code defaults include MCP_HTTP_IP_ALLOWLIST, MCP_OAUTH_ACCESS_TTL, MCP_OAUTH_REFRESH_TTL, MCP_OAUTH_CODE_TTL, MCP_RATE_LIMIT_READ, MCP_RATE_LIMIT_WRITE, MCP_OAUTH_KEYS_DIR, and MCP_LOOPBACK_URL.

Debug Override ​

bash
# Allows FLASK_DEBUG=True on a non-loopback FLASK_HOST_IP. Startup refuses
# that combination without it, because the Werkzeug debugger is an RCE primitive
FLASK_DEBUG_ALLOW_EXTERNAL = 'False'

Railway/Cloud Deployment ​

When deploying to Railway or Render, set these environment variables in the platform dashboard:

Required Variables ​

VariableDescription
HOST_SERVERYour app URL (e.g., https://your-app.up.railway.app)
REDIRECT_URLBroker OAuth callback URL
BROKER_API_KEYBroker API key
BROKER_API_SECRETBroker API secret
APP_KEYGenerated secret key
API_KEY_PEPPERGenerated pepper

Auto-Generated by start.sh ​

When HOST_SERVER is set and no .env exists, start.sh automatically generates .env with:

  • All security settings
  • CORS configured for your domain
  • CSP with secure WebSocket URLs
  • Railway's PORT environment variable support

Two caveats about that generated file. It writes ENV_CONFIG_VERSION from the platform environment and falls back to 1.0.4, which is older than the 1.0.7 in .sample.env, so set ENV_CONFIG_VERSION explicitly in the dashboard to avoid the startup version refusal. It also does not write FERNET_SALT; first-run rotation in utils/env_check.py adds it, which requires the .env file to be writable.

Validation ​

Startup Validation ​

app.py calls the validator at the very top of the module, before any other project import, because auth and database modules need the secrets at import time.

python
# First lines of app.py
from utils.env_check import load_and_check_env_variables

load_and_check_env_variables()

load_and_check_env_variables() returns None. It prints a remediation message and calls sys.exit(1) on any failure rather than returning an error list.

Validation Rules ​

VariableValidationEnforced in
ENV_CONFIG_VERSIONNot older than .sample.envutils/env_check.py
APP_KEYPresent, 32+ charactersapp.py create_app()
API_KEY_PEPPERPresent, 32+ charactersdatabase/auth_db.py at import
FLASK_PORT, WEBSOCKET_PORT0-65535utils/env_check.py
FLASK_DEBUGTrue/False/1/0/t/futils/env_check.py
FLASK_ENVdevelopment or productionutils/env_check.py
*_RATE_LIMIT*"X per second|minute|hour|day", semicolon-separated for compound limitsutils/env_check.py
SESSION_EXPIRY_TIMEFormat: HH:MM, 24-hourutils/env_check.py
WEBSOCKET_URLStarts with ws:// or wss://utils/env_check.py
REDIRECT_URLEnds in /<broker>/callback, broker present in VALID_BROKERS, not the shipped defaultutils/env_check.py
BROKER_API_KEY::: field count for 5paisa, Flattrade, and Dhanutils/env_check.py
LOG_LEVELDEBUG/INFO/WARNING/ERROR/CRITICALutils/env_check.py
LOG_RETENTIONPositive integerutils/env_check.py
LOG_TO_FILETrue/Falseutils/env_check.py
LOG_DIR, LOG_FORMATNon-emptyutils/env_check.py

Generating Secrets ​

bash
# Generate a 32-byte secret (64 hex characters) for APP_KEY,
# API_KEY_PEPPER and FERNET_SALT
python -c "import secrets; print(secrets.token_hex(32))"

# Output example:
# a1b2c3d4e5f67890123456789012345678901234567890123456789012345678

Environment Comparison ​

Development ​

bash
FLASK_DEBUG = 'True'
FLASK_ENV = 'development'
LOG_LEVEL = 'DEBUG'
HOST_SERVER = 'http://127.0.0.1:5000'
FLASK_HOST_IP = '127.0.0.1'
CSP_UPGRADE_INSECURE_REQUESTS = 'FALSE'

Production (Local) ​

bash
FLASK_DEBUG = 'False'
FLASK_ENV = 'production'
LOG_LEVEL = 'INFO'
HOST_SERVER = 'https://your-domain.com'
FLASK_HOST_IP = '0.0.0.0'
CSP_UPGRADE_INSECURE_REQUESTS = 'TRUE'

Production (Railway) ​

bash
# Set in Railway dashboard, start.sh generates .env:
HOST_SERVER = 'https://your-app.up.railway.app'
FLASK_HOST_IP = '0.0.0.0'  # Auto-set
FLASK_PORT = '${PORT}'  # Railway's PORT
WEBSOCKET_HOST = '0.0.0.0'  # Auto-set
ZMQ_HOST = '0.0.0.0'  # Auto-set

Security Best Practices ​

File Permissions ​

bash
# Restrict .env access
chmod 600 .env

Never Commit Secrets ​

gitignore
# .gitignore
.env
*.pem
*.key

Version Check ​

Compare ENV_CONFIG_VERSION in your .env with .sample.env after updates. If they differ, copy new variables from the sample.

Key Files Reference ​

FilePurpose
.envActive configuration
.sample.envReference template
start.shAuto-generates .env for cloud
utils/env_check.pyValidation logic and first-run secret rotation
utils/env_config.pyenv_int / env_float readers that fall back instead of crashing
utils/config.pyConfig helpers
cors.py, csp.pyCORS and CSP fallback defaults