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 variablesEnvironment Variables
Version Tracking
# 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)
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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 windowNgrok Configuration
# Enable ngrok tunnel
NGROK_ALLOW = 'FALSE'Logging Configuration
# 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
# Memory thresholds in MB for the background health monitor
HEALTH_MEMORY_WARNING_THRESHOLD = '3000'
HEALTH_MEMORY_CRITICAL_THRESHOLD = '5000'Python Strategy Logging
# 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
# 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
# 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 brokersCORS Configuration
# 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)
# 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
# Enable/disable CSRF protection
CSRF_ENABLED = 'TRUE'
# Token time limit (seconds, empty = no limit)
CSRF_TIME_LIMIT = ''Cookie Configuration
# 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
# 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
# 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
# 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
| Variable | Description |
|---|---|
HOST_SERVER | Your app URL (e.g., https://your-app.up.railway.app) |
REDIRECT_URL | Broker OAuth callback URL |
BROKER_API_KEY | Broker API key |
BROKER_API_SECRET | Broker API secret |
APP_KEY | Generated secret key |
API_KEY_PEPPER | Generated 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
PORTenvironment 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.
# 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
| Variable | Validation | Enforced in |
|---|---|---|
ENV_CONFIG_VERSION | Not older than .sample.env | utils/env_check.py |
APP_KEY | Present, 32+ characters | app.py create_app() |
API_KEY_PEPPER | Present, 32+ characters | database/auth_db.py at import |
FLASK_PORT, WEBSOCKET_PORT | 0-65535 | utils/env_check.py |
FLASK_DEBUG | True/False/1/0/t/f | utils/env_check.py |
FLASK_ENV | development or production | utils/env_check.py |
*_RATE_LIMIT* | "X per second|minute|hour|day", semicolon-separated for compound limits | utils/env_check.py |
SESSION_EXPIRY_TIME | Format: HH:MM, 24-hour | utils/env_check.py |
WEBSOCKET_URL | Starts with ws:// or wss:// | utils/env_check.py |
REDIRECT_URL | Ends in /<broker>/callback, broker present in VALID_BROKERS, not the shipped default | utils/env_check.py |
BROKER_API_KEY | ::: field count for 5paisa, Flattrade, and Dhan | utils/env_check.py |
LOG_LEVEL | DEBUG/INFO/WARNING/ERROR/CRITICAL | utils/env_check.py |
LOG_RETENTION | Positive integer | utils/env_check.py |
LOG_TO_FILE | True/False | utils/env_check.py |
LOG_DIR, LOG_FORMAT | Non-empty | utils/env_check.py |
Generating Secrets
# 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:
# a1b2c3d4e5f67890123456789012345678901234567890123456789012345678Environment Comparison
Development
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)
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)
# 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-setSecurity Best Practices
File Permissions
# Restrict .env access
chmod 600 .envNever Commit Secrets
# .gitignore
.env
*.pem
*.keyVersion 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
| File | Purpose |
|---|---|
.env | Active configuration |
.sample.env | Reference template |
start.sh | Auto-generates .env for cloud |
utils/env_check.py | Validation logic and first-run secret rotation |
utils/env_config.py | env_int / env_float readers that fall back instead of crashing |
utils/config.py | Config helpers |
cors.py, csp.py | CORS and CSP fallback defaults |
