Environment Variables
Every setting is read from the environment, 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.
Nothing has a built-in default. Every variable marked required below must be set explicitly, in every environment. At startup the API server validates the whole configuration, collects every missing or invalid variable, prints one list naming each of them, and exits with status 78. The web server, the image build and the container entrypoint apply the same rule to the variables they read. Optional features are either off (variable unset) or fully configured; a half-configured feature, such as MAIL_TRANSPORT=smtp without SMTP_HOST, is a startup error rather than a silent downgrade. Blank values count as unset.
Invalid configuration (2 problems):
- JWT_SECRET is not set (session token signing secret)
- DB_POOL_MAX must be a whole number (got "twenty")
Refusing to start. Set the variables above and restart.
Required Secrets
| Variable | Min | Role and operational implication |
|---|---|---|
JWT_SECRET | 32 | HMAC-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_KEY | 32 | Master 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. Rotating the key orphans all existing ciphertext (wire prefix enc:); back up and re-encrypt before changing. |
DB_DEVOPS_DB_PASSWORD | 1 | Password for the metadata Postgres role. |
openssl rand -hex 32 # 256-bit secret, hex-encoded
Server Runtime
| Variable | Example | Description |
|---|---|---|
NODE_ENV | production | development, production or test. Controls error-detail leakage in responses and forbids the QA bypass key in production. The container image sets production. |
LOG_LEVEL | info | trace, debug, info, warn, error or fatal. Read by the API server and the web server. |
JWT_EXPIRES_IN | 8h | Session token lifetime: a number followed by s, m, h or d. When it runs out the user signs in again; there is no refresh token. |
LOG_STDOUT_ONLY | true | true emits structured JSON to stdout; false pretty-prints outside production. The container image sets true. |
PORT | 5002 | Express API listen port. Set by the container image. |
NEXT_PORT | 3000 | Next.js listen port inside the container. Set by the container image. |
BACKEND_URL | http://localhost:5002 | URL the web server's /api proxy forwards to. Set by the container image, where both servers share the container. |
CLIENT_URL | https://dbstudio.example.com | Sole CORS allow-list origin. Must match the browser origin exactly, scheme, host, and port, no trailing slash. |
APP_PUBLIC_URL | https://dbstudio.example.com | Externally reachable base URL used to build the links in password-reset and invitation emails. |
MIGRATE_ON_BOOT | true | true makes the container entrypoint apply pending migrations before serving; false when migrations run as a discrete step (init container, CI job) to avoid rolling-update races. |
MIGRATIONS_DIR | /app/migrations | Directory holding the NNN_*.sql migration files. Set by the container image. |
LICENSE_PUBLIC_KEY_PATH | /app/keys/license-public.pem | License verification public key. Set by the container image; startup fails if the file cannot be read. |
Build-Time Variables
These are inlined into the browser bundle when the image or the web app is built, so they cannot be changed at run time. The build fails and names any that are missing.
| Variable | Description |
|---|---|
NEXT_BUILD_MODE | server builds the standalone web server used by the container; export builds static HTML for the desktop app. The container build sets server. |
NEXT_PUBLIC_API_URL | Origin the browser prefixes to /api calls. Must be set; an empty value means same origin, which is what the container build uses. Otherwise an origin such as https://dbstudio.example.com, without /api. |
NEXT_PUBLIC_SUPPORT_EMAIL / NEXT_PUBLIC_SALES_EMAIL | Optional. Contact links on the settings page and the license screens; unset means the link is not rendered. |
Metadata Database
All application state, users, scripts, releases, connections, executions, approvals, audit, licenses, lives in one Postgres database. This is the application'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. Only the DB_DEVOPS_DB_* names are read.
| Variable | Example | Description |
|---|---|---|
DB_DEVOPS_DB_HOST | postgres | Postgres host. In the canonical Compose stack this is the service name postgres. |
DB_DEVOPS_DB_PORT | 5432 | Postgres port. |
DB_DEVOPS_DB_NAME | db_devops | Metadata database name. |
DB_DEVOPS_DB_USER | dbstudio | Metadata role. |
DB_DEVOPS_DB_PASSWORD | See Required Secrets. |
Pool tuning
The metadata pool is pg (node-postgres). Raise DB_POOL_MAX only after confirming your Postgres max_connections headroom, or front the pool with PgBouncer in transaction mode.
| Variable | Example | Description |
|---|---|---|
DB_POOL_MAX | 20 | Max concurrent pooled connections per process. |
DB_IDLE_TIMEOUT_MS | 30000 | Idle connection reap interval. |
DB_CONNECTION_TIMEOUT_MS | 2000 | Time to wait for a free/established connection before erroring, keep low to fail fast under saturation. |
CONNECTION_IDLE_DISCONNECT_MIN | 60 | Minutes of inactivity before a live user database connection is auto-disconnected (audited as AUTO_DISCONNECT_IDLE). 0 disables. 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. |
Rate Limiting and Query Guards
The IP buckets back a 429 on breach. They count each client address: behind a reverse proxy, list the proxy in TRUST_PROXY (and have it send X-Forwarded-For), otherwise every request carries the proxy's address and collapses onto one bucket.
| Variable | Example | Scope |
|---|---|---|
TRUST_PROXY | optional | Addresses or CIDR ranges of the proxies in front of DB Studio (load balancer, ingress, Caddy, nginx), comma-separated; loopback, linklocal and uniquelocal name those ranges. The API always believes the bundled web server on the same host; listing the proxies in front lets audit entries and per-address rate limits use the client's address. Unset: the address of the proxy in front is recorded. An entry that is not an address, a range or one of those names stops the server at startup. |
GENERAL_RATE_LIMIT_MAX | 2000 | Requests per IP per 15 minutes across all API routes, including auth. |
DB_DEVOPS_RATE_LIMIT_MAX | 5000 | Requests per IP per minute on DB DevOps routes, higher cap to absorb bulk script execution. |
QUERY_RATE_LIMIT_PER_MIN | 60 | IDE query executions per user per minute. |
QUERY_MAX_BYTES | 0 | Largest SQL text accepted by the IDE query routes, in bytes; larger requests get a 413. 0 disables the cap. |
QUERY_TIMEOUT_MS | 0 | Wall-clock limit for an IDE query request; slower requests get a 504. 0 disables it. |
LLM Provider Endpoints
AI features (NL-to-SQL, risk audit, chat) select a provider, key, and model per tenant in Settings, those live in the metadata store. These variables set the API endpoint for each provider, which you can point at Azure OpenAI, an on-prem gateway or a compatible proxy, and the model used when a tenant has not chosen one. AWS Bedrock takes its region from the AI provider settings.
| Variable | Example |
|---|---|
OPENAI_BASE_URL | https://api.openai.com/v1 |
OPENAI_DEFAULT_MODEL | gpt-4o |
ANTHROPIC_BASE_URL | https://api.anthropic.com/v1 |
ANTHROPIC_DEFAULT_MODEL | claude-sonnet-4-20250514 |
ANTHROPIC_API_VERSION | 2023-06-01 |
GEMINI_BASE_URL | https://generativelanguage.googleapis.com/v1beta |
GEMINI_DEFAULT_MODEL | gemini-1.5-pro |
Email (optional)
Leave MAIL_TRANSPORT unset to run with no outbound email. Once it is set, the variables for that transport become required, and setting any mail variable without MAIL_TRANSPORT is a startup error. The same transport sends password-reset emails, issue reports and security alert emails.
| Variable | When | Notes |
|---|---|---|
MAIL_TRANSPORT | optional | smtp or ses. |
MAIL_FROM | any transport | Envelope sender, e.g. DB Studio <noreply@example.com>. For SES it must be a verified identity. |
SMTP_HOST / SMTP_PORT | smtp | Relay host and port. |
SMTP_SECURE | smtp | true for implicit TLS (465), false for STARTTLS or plain. |
SMTP_USER / SMTP_PASS | smtp, optional | Relay credentials; SMTP_PASS is required when SMTP_USER is set. |
SES_REGION | ses | AWS region of the SES identity. Credentials come from the standard AWS chain (task or instance role). |
REPORT_ISSUE_EMAIL | optional | Inbox for in-app issue reports; unset hides issue reporting. |
ALERT_EMAIL_TO | optional | Recipient for security alert emails. |
The names SMTP_FROM, REPORT_SES_FROM and REPORT_SES_REGION are no longer read; startup reports them with their replacement.
Other Optional Integrations
| Variable | Behavior |
|---|---|
LICENSE_SERVICE_URL | The address of the Simcha license service, given with your license. Activation registers each key to this setup's installation ID there, so the key cannot be used on another setup, and deactivation releases it. Settings, and the expired-license page, also offer Check for an Updated License, which sends the current key to the service and applies the newest key issued for it (a renewal or more seats). Only the key and the installation ID are sent. Unset: keys must be issued for this setup's installation ID, and no update check is offered; this suits servers without internet access. |
ALERT_WEBHOOK_URL | POSTs every security alert as JSON (Slack, PagerDuty, etc.). |
SUPPORT_EMAIL / SALES_EMAIL / SECURITY_EMAIL | Contact addresses appended to server-rendered messages, such as the refusal when every license seat is taken. Unset means no contact hint is shown. |
AGENT_BYPASS_KEY | Bearer for the X-Agent-Key header used by internal automation to skip rate limits. Unset disables the bypass. |
QA_BYPASS_KEY | Bearer for the X-QA-Bypass-Key test-harness header, which injects a virtual admin. Setting it while NODE_ENV=production is a startup error. |
Licensing
Keys are signed RSA-SHA256; the matching public key ships inside the image at LICENSE_PUBLIC_KEY_PATH, so verification is fully offline. Activate via the UI (Settings, License) or POST /api/license/activate. Every app container on the same database applies a license change at once: the database announces each change and every container reads the license again, so activating, replacing or removing a key on one container takes effect on all of them, however many run. Each container also reads the license again every 60 seconds, and before it refuses a request for want of a license.
Request Path
The browser never talks to Express directly for API calls. Every call traverses browser → Next.js API route (pages/api/*) → Express (:5002), the proxy layer forwards Authorization, X-Request-Id, and the bypass headers to BACKEND_URL. A new Express route is unreachable until its Next.js proxy exists. Live notifications and deployment status use Socket.IO at /socket.io on the same origin, which Next.js forwards to Express, so no extra port is needed. 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 bounds keep the walk cheap on large or busy schemas.
| Variable | Example | Description |
|---|---|---|
BLAST_RADIUS_MAX_NODES | 500 | Object cap per graph. Exceeding it returns a partial, truncation-flagged report instead of unbounded growth. Raise for densely interconnected schemas. |
BLAST_RADIUS_STMT_TIMEOUT_MS | 10000 | Per-statement timeout on the introspection queries, shields busy production catalogs from a slow walk. |
CONSUMER_SAMPLER_INTERVAL_MS | 60000 | Poll interval for the opt-in usage sampler reading each engine's active sessions (see Usage Sampler under the DevOps Manager) on connections with observed-consumer sampling enabled. Must be at least 10000. Off until enabled per connection. |
CONSUMER_STALE_DAYS | 30 | An observed consumer unseen for this many days is greyed in the registry and dropped from blast-radius totals. Manually tagged consumers never expire. Must be at least 1. |
Shadow Execution Tuning
Shadow execution dry-runs a migration against a disposable twin of the target. These bounds control where the twin lives and how a run is capped.
| Variable | Example | Purpose |
|---|---|---|
SHADOW_TWIN_MODE | same-server | same-server builds the twin next to the target: a throwaway database on its server (needs create-database privilege: CREATEDB on PostgreSQL, CREATE on MySQL, CREATE ANY DATABASE on SQL Server), a temporary user on Oracle, a new keyspace on Cassandra, a temporary file for SQLite, a key or index name prefix on Redis and Elasticsearch, and rolled-back transactions on Neo4j; see Shadow Execution. sidecar and ephemeral-docker are reserved and not yet available. Any other value is a startup error. |
SHADOW_SAMPLE_ROWS | 1000 | Max 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_S | 120 | Wall-clock cap per run (at least 5); over-budget runs are aborted and the twin dropped. |
SHADOW_STMT_TIMEOUT_MS | 30000 | Per-statement timeout on the twin (at least 1000). |
SHADOW_LOCK_TIMEOUT_MS | 10000 | Lock-acquisition timeout on the twin (at least 1000), a would-be-blocking migration fails fast. |
SHADOW_MAX_AGE_MIN | 30 | Sweeper 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 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 (including /socket.io) 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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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 APP_PUBLIC_URL=https://dbstudio.example.com. Have the proxy send X-Forwarded-For and list its address or network in TRUST_PROXY (for Docker Compose, the compose network's range, e.g. 172.18.0.0/16), or audit entries record the proxy's address and rate limiting collapses onto a single bucket.
Real-time updates. Live notifications use Socket.IO at /socket.io on the UI port (3000), which forwards it to the API inside the container, so a proxy that sends everything else to :3000 needs no extra route. Allow WebSocket upgrades on that route, as in the Upgrade/Connection headers above. Without them the client falls back to HTTP long-polling, which still works. The connection is authenticated with the user's session and is refused without one.
Example .env
The shipped .env.example lists every variable with a working value. Copy it to .env, replace the secrets, set your URLs, then bring the stack up. The Compose file passes .env to the container and pins the values that describe the container itself (ports, file paths, BACKEND_URL, the metadata database host).
JWT_SECRET=replace_me_with_64_hex_chars
ENCRYPTION_KEY=replace_me_with_64_hex_chars
DB_DEVOPS_DB_NAME=db_devops
DB_DEVOPS_DB_USER=dbstudio
DB_DEVOPS_DB_PASSWORD=replace_me_with_strong_password
LOG_LEVEL=info
MIGRATE_ON_BOOT=true
CLIENT_URL=https://dbstudio.example.com
APP_PUBLIC_URL=https://dbstudio.example.com
# ...plus the pool, rate-limit, LLM, blast-radius and shadow settings
# from .env.example; the app lists any that are missing.