Environment Variables

Every knob is read from process.env at boot, no config files, no vendor SDKs. The same surface applies whether you run Compose, a Helm chart, an ECS task definition, or a bare node process. Inject values from whatever secret store you already operate (Secrets Manager, Key Vault, Vault, sealed-secrets); the app doesn't care where they came from.

Validation is fail-closed under NODE_ENV=production: the process exits non-zero at startup when any required secret is missing or below its minimum length. In development the same checks downgrade to a warning and continue, so local runs never block, and a misconfigured production deploy never boots silently degraded.

Required Secrets

VariableMinRole and operational implication
JWT_SECRET32HMAC-SHA256 signing key for session tokens. Anyone holding it can mint a valid token for any user; treat it as a crown-jewel secret. Rotation is your global session kill switch, every previously issued token fails signature verification on its next request.
ENCRYPTION_KEY32Master input to PBKDF2 (100k iterations, SHA-512, per-record random salt) that derives the AES-256-GCM keys wrapping stored DB passwords, SSH credentials, MFA secrets, and sensitive settings. If unset, the encryption service degrades to a no-op and persists those secrets in plaintext behind a single warning log, never run any non-test environment without it. Rotating the key orphans all existing ciphertext (wire prefix enc:); back up and re-encrypt before changing.
DB_DEVOPS_DB_PASSWORD1Password for the metadata Postgres role. Legacy alias DB_PASSWORD is honored.
openssl rand -hex 32   # 256-bit secret, hex-encoded

Server Runtime

VariableDefaultDescription
NODE_ENVdevelopmentGates the fail-closed validator, log verbosity, error-detail leakage in responses, Next.js optimizations, and the non-prod bypass headers. Set to production for anything real.
PORT5002Express API listen port.
NEXT_PORT3000Next.js standalone server listen port inside the container.
CLIENT_URLhttp://localhost:3000Sole CORS allow-list origin. Must match the browser origin exactly, scheme, host, and port, no trailing slash.
NEXT_PUBLIC_API_URLhttp://localhost:3000/apiBackend base URL as seen from the browser. Behind a reverse proxy set this same-origin (e.g. https://host/api) to eliminate CORS. Inlined into the client bundle at build time.
JWT_EXPIRES_IN8hToken TTL (30m, 8h, 1d, 7d). No refresh flow, expiry forces re-auth by design.
MIGRATE_ON_BOOTtrueEntrypoint applies pending migrations before serving. Set false when migrations run as a discrete step (init container, CI job) to avoid rolling-update races.
LOG_STDOUT_ONLYtrue (container)Force structured JSON to stdout only, no file writes. Required under any log-collecting orchestrator.

Metadata Database

All application state, users, scripts, releases, connections, executions, approvals, audit, licenses, lives in one Postgres database. This is Simcha's own metadata store, distinct from the customer databases the IDE connects to. The migration runner needs CREATE TABLE on this database at first boot.

VariableDefaultDescription
DB_DEVOPS_DB_HOSTlocalhostPostgres host. In the canonical Compose stack this is the service name postgres.
DB_DEVOPS_DB_PORT5432Postgres port.
DB_DEVOPS_DB_NAMEdb_devopsMetadata database name.
DB_DEVOPS_DB_USERsimchaMetadata role.
DB_DEVOPS_DB_PASSWORDrequiredSee Required Secrets.

Legacy DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD are accepted as fallbacks for the DB_DEVOPS_* names.

Pool tuning

The metadata pool is pg (node-postgres). Defaults are conservative; raise DB_POOL_MAX only after confirming your Postgres max_connections headroom, or front the pool with PgBouncer in transaction mode.

VariableDefaultDescription
DB_POOL_MAX20Max concurrent pooled connections per process.
DB_IDLE_TIMEOUT_MS30000Idle connection reap interval.
CONNECTION_IDLE_DISCONNECT_MIN60Minutes of inactivity before a live user database connection is auto-disconnected (audited as AUTO_DISCONNECT_IDLE). Set 0 to disable. Connection state is per-user: a connection shows as connected only for the users who opened it, and it closes for real once its last holder disconnects, logs out, or idles past this window.
DB_CONNECTION_TIMEOUT_MS2000Time to wait for a free/established connection before erroring, keep low to fail fast under saturation.

Rate Limiting

Two independent buckets, keyed per client IP, back a 429 on breach. Size them to your reverse-proxy trust boundary; if the proxy doesn't forward the real client IP, every request collapses onto one bucket.

VariableDefaultWindowScope
GENERAL_RATE_LIMIT_MAX200015 minAll API routes, including auth.
DB_DEVOPS_RATE_LIMIT_MAX50001 minDB DevOps routes, higher cap to absorb bulk script execution.

LLM Provider Overrides

AI features (NL-to-SQL, risk audit, chat) select a provider, key, and model per tenant in Settings, those live in the metadata store, not in env. The env overrides below exist to retarget base URLs at Azure OpenAI, Bedrock, or on-prem gateways, and to pin model ids independent of tenant settings.

VariableDefault
OPENAI_BASE_URLhttps://api.openai.com/v1
OPENAI_DEFAULT_MODELgpt-4o
ANTHROPIC_BASE_URLhttps://api.anthropic.com/v1
ANTHROPIC_DEFAULT_MODELclaude-sonnet-4-20250514
ANTHROPIC_API_VERSION2023-06-01
GEMINI_BASE_URLhttps://generativelanguage.googleapis.com/v1beta
GEMINI_DEFAULT_MODELgemini-1.5-pro

Security Alert Email (SMTP)

Security events route through the alert service. With no SMTP host configured, alerts stay in-app only. There is no literal fallback for the envelope sender, SMTP_FROM is mandatory once SMTP_HOST is set.

VariableDefaultNotes
SMTP_HOST, Enables outbound alert email when set.
SMTP_PORT587
SMTP_SECUREfalseSet true for implicit TLS (465).
SMTP_USER / SMTP_PASS, Relay credentials.
SMTP_FROM, Required when SMTP_HOST is set.

Tenant Branding

Support/sales contact links are tenant-owned. Left empty (the default), the corresponding UI affordances are simply not rendered, no Simcha addresses leak into a self-hosted deployment. The NEXT_PUBLIC_* pair is inlined into the client bundle at build time; the unprefixed pair is used in server-rendered messages.

VariableSurface
SUPPORT_EMAIL / SALES_EMAILServer-rendered messages.
NEXT_PUBLIC_SUPPORT_EMAILLogin / help footer.
NEXT_PUBLIC_SALES_EMAILLicense-expired banner.

Non-Prod Bypass Keys

Both headers inject a virtual admin and skip auth/RBAC entirely, they exist for test harnesses and internal automation. Both are hard-gated to NODE_ENV !== 'production': enabling them against a production build is a critical vulnerability, so guarantee your prod env actually sets NODE_ENV=production.

VariableHeaderBehavior
AGENT_BYPASS_KEYX-Agent-KeyNo default, absent value disables the bypass outright.
QA_BYPASS_KEYX-QA-Bypass-KeyDev-only, ignored when NODE_ENV=production.

Licensing

Keys are signed RSA-SHA256; the matching public key ships inside the image, so verification is fully offline and no runtime license variable is required. Activate via the UI (Settings, License) or POST /api/license/activate. License state is cached in-process with a 60-second TTL, direct mutation of the license table won't take effect until the cache expires or an activate/deactivate call forces a refresh. LICENSE_SECRET is only consulted on builds using the legacy HMAC signing path; standard builds ignore it.

Request Path

The browser never talks to Express directly. Every call traverses browser → Next.js API route (pages/api/*) → Express (:5002), the proxy layer forwards Authorization, X-Request-Id, and the bypass headers, and pins the backend target via BACKEND_URL (default http://localhost:5002). A new Express route is unreachable until its Next.js proxy exists. Health surface: GET /api/health (returns STARTING until the metadata DB is reachable) and GET /api/health/migrations (applied-migration inventory, consumed by deploy validators).

Blast Radius Tuning

Blast radius, the downstream-impact analysis surfaced before approval, walks the target's dependency graph via read-only catalog queries. These optional bounds keep the walk cheap on large or busy schemas; defaults suit most databases.

VariableDefaultDescription
BLAST_RADIUS_MAX_NODES500Object cap per graph. Exceeding it returns a partial, truncation-flagged report instead of unbounded growth. Raise for densely interconnected schemas.
BLAST_RADIUS_STMT_TIMEOUT_MS10000Per-statement timeout on the introspection queries, shields busy production catalogs from a slow walk.
CONSUMER_SAMPLER_INTERVAL_MS60000Poll interval for the opt-in usage sampler reading active sessions (pg_stat_activity) on connections with observed-consumer sampling enabled. Floored at 10000ms so misconfiguration can't hammer the DB. Off until enabled per connection (Enterprise).
CONSUMER_STALE_DAYS30An observed consumer unseen for this many days is greyed in the registry and dropped from blast-radius totals. Manually tagged consumers never expire.

Shadow Execution Tuning

Shadow execution dry-runs a migration against a disposable twin of the target. These optional bounds control where the twin lives and how a run is capped; defaults need no infrastructure beyond the target connection.

VariableDefaultPurpose
SHADOW_TWIN_MODEsame-serversame-server spins a throwaway database on the target's own server (needs create-database privilege, CREATEDB on Postgres, CREATE on MySQL). sidecar points at a dedicated scratch database, preferred for regulated tenants. An unknown value fails loud, no silent fallback.
SHADOW_SAMPLE_ROWS1000Max rows copied per table when a run opts into sampled data; 0 disables. Sampled rows live only in the twin and are dropped with it, nothing is persisted.
SHADOW_RUN_TIMEOUT_S120Wall-clock cap per run; over-budget runs are aborted and the twin dropped.
SHADOW_STMT_TIMEOUT_MS30000Per-statement timeout on the twin.
SHADOW_LOCK_TIMEOUT_MS10000Lock-acquisition timeout on the twin, a would-be-blocking migration fails fast.
SHADOW_MAX_AGE_MIN30Sweeper force-drops any twin older than this, guarantees no orphaned twin survives.

Reverse Proxy and CORS

The single most common misconfiguration is a CLIENT_URL that doesn't byte-match the browser origin, symptom is silently failed API calls (dashboard counts stuck at zero, "CORS policy" console errors). In production, front the container with a proxy (nginx/Traefik/Caddy or a cloud LB), terminate TLS there, route /api/* to :5002 and everything else to :3000, and serve UI and API same-origin to sidestep CORS entirely.

server {
    listen 443 ssl http2;
    server_name dbstudio.example.com;

    ssl_certificate     /etc/ssl/certs/dbstudio.crt;
    ssl_certificate_key /etc/ssl/private/dbstudio.key;

    location /api/ {
        proxy_pass http://localhost:5002;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

With that topology set CLIENT_URL=https://dbstudio.example.com and NEXT_PUBLIC_API_URL=https://dbstudio.example.com/api. Ensure the proxy forwards the real client IP or rate limiting collapses onto a single bucket.

Example .env

# --- REQUIRED ---
JWT_SECRET=replace_me_with_64_hex_chars
ENCRYPTION_KEY=replace_me_with_64_hex_chars
DB_PASSWORD=replace_me_with_strong_password

# --- METADATA DB ---
DB_USER=simcha
DB_NAME=db_devops

# --- RUNTIME ---
NODE_ENV=production
CLIENT_URL=https://dbstudio.example.com
NEXT_PUBLIC_API_URL=https://dbstudio.example.com/api
JWT_EXPIRES_IN=8h
MIGRATE_ON_BOOT=false

# Pin the image in production:
# SIMCHA_IMAGE=simchasolutions/db-studio:1.0.1

The shipped .env.example mirrors this template. Copy to .env, populate real secrets, then bring the stack up.