Configuration Reference

This document provides comprehensive configuration information for BetterDB Monitor.

Table of Contents

Multi-Connection Support

BetterDB Monitor supports monitoring multiple Valkey/Redis instances from a single deployment. This enables centralized monitoring of development, staging, and production databases.

How It Works

  1. Connection Registry: All database connections are managed through a central registry
  2. Default Connection: On first startup, a default connection is created from environment variables (DB_HOST, DB_PORT, etc.)
  3. Connection Scoping: All data (metrics, audit logs, webhooks, etc.) is isolated per connection using the X-Connection-Id header
  4. Prometheus Labels: All metrics include a connection label for filtering (e.g., betterdb_memory_used_bytes{connection="localhost:6379"})

Managing Connections

Via API

# List all connections
curl -b cookies.txt -c cookies.txt http://localhost:3001/api/connections

# Add a new connection
curl -b cookies.txt -c cookies.txt -X POST http://localhost:3001/api/connections \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production Redis",
    "host": "prod-redis.example.com",
    "port": 6379,
    "password": "secret"
  }'

# Test a connection before adding
curl -b cookies.txt -c cookies.txt -X POST http://localhost:3001/api/connections/test \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Test",
    "host": "staging-redis.example.com",
    "port": 6379
  }'

# Set a connection as default
curl -b cookies.txt -c cookies.txt -X POST http://localhost:3001/api/connections/{id}/default

# Remove a connection
curl -b cookies.txt -c cookies.txt -X DELETE http://localhost:3001/api/connections/{id}

Via Web UI

Use the connection selector in the top navigation bar to:

  • View all registered connections and their status
  • Switch between connections (data displayed is scoped to selected connection)
  • Add new connections with the “+” button
  • Manage connections (set default, reconnect, delete)

Connection-Scoped Requests

When making API requests, include the X-Connection-Id header to target a specific connection:

# Get metrics for a specific connection
curl -b cookies.txt -c cookies.txt -H "X-Connection-Id: prod-conn-id" http://localhost:3001/api/metrics/info

# Get audit logs for a specific connection
curl -b cookies.txt -c cookies.txt -H "X-Connection-Id: staging-conn-id" http://localhost:3001/api/audit/entries

If no header is provided, the default connection is used.

Webhooks and Connections

Webhooks can be:

  • Global: Fire for events from any connection (created without X-Connection-Id)
  • Connection-scoped: Fire only for events from a specific connection (created with X-Connection-Id)
# Create a webhook that fires for ALL connections
curl -b cookies.txt -c cookies.txt -X POST http://localhost:3001/api/webhooks \
  -H "Content-Type: application/json" \
  -d '{"name": "Global Alert", "url": "https://...", "events": ["instance.down"]}'

# Create a webhook only for production
curl -b cookies.txt -c cookies.txt -X POST http://localhost:3001/api/webhooks \
  -H "X-Connection-Id: prod-conn-id" \
  -H "Content-Type: application/json" \
  -d '{"name": "Prod Alert", "url": "https://...", "events": ["instance.down"]}'

Cloud: Direct Connection Network Requirements

When using BetterDB Cloud, each workspace runs in an isolated container with a restricted outbound network policy. The following port ranges are permitted for direct database connections:

Port range Protocol Use case
443 TCP HTTPS/TLS connections
2000–2999 TCP Managed Redis/Valkey providers that use ports in this range
6000–6999 TCP Standard Redis/Valkey (6379), TLS (6380), Azure Enterprise (6380), etc.

A small number of sensitive infrastructure ports within these ranges are blocked (e.g. 2375–2376, 2379–2380, 6443). If your database runs on a port outside these ranges, use the BetterDB Agent instead — the agent runs in your own network and connects outbound on port 443, so there are no port restrictions on your side.

Tip: Most managed services (Upstash, Redis Cloud, Aiven) use port 6379 with TLS — enable the Use TLS toggle in the connection form and they will work with direct connection.

Data Isolation

All stored data is isolated by connection:

  • Audit trail entries
  • Client analytics snapshots
  • Slowlog/Commandlog entries
  • Anomaly events
  • Key analytics snapshots

This means:

  • Querying /audit/entries with X-Connection-Id: A returns only data from connection A
  • Prometheus metrics are labeled with connection="host:port" for filtering
  • Dashboard displays data for the currently selected connection

Environment Variables

Database Connection

Variable Required Default Description
DB_HOST Yes localhost Valkey/Redis host to monitor
DB_PORT No 6379 Valkey/Redis port
DB_USERNAME No default Valkey/Redis ACL username
DB_PASSWORD No - Valkey/Redis password
DB_TYPE No auto Database type: auto, valkey, or redis

Storage Backend

Variable Required Default Description
STORAGE_TYPE No memory Storage backend: memory, postgres, sqlite, or turso
STORAGE_URL Conditional - PostgreSQL connection URL (required if STORAGE_TYPE=postgres), or libSQL URL (required if STORAGE_TYPE=turso)
STORAGE_AUTH_TOKEN Conditional - Turso auth token (startup requires it when STORAGE_URL uses libsql://, and rejects it when STORAGE_URL uses http://; a hosted https:// endpoint needs it too but is not checked at startup - see below)
STORAGE_SQLITE_FILEPATH No ./data/audit.db SQLite database file path (only for STORAGE_TYPE=sqlite)

Warning: memory is for development and testing only. Users and sessions live in the process, so a restart loses them, the instance returns to the register screen, and the next visitor claims the owner account. Use sqlite or postgres for anything other people can reach.

Note: sqlite writes to a local file through the better-sqlite3 native module, which is stripped from the latest (no-AI) Docker image - use it for local development, or pick postgres, turso, or memory for Docker deployments.

Note: turso reuses the SQLite adapter over the libSQL wire protocol, so it needs no native module and works in every Docker image. It is opt-in: set STORAGE_TYPE=turso plus STORAGE_URL (and STORAGE_AUTH_TOKEN for libsql:// URLs), exactly like the postgres backend.

Note: http:// is for an unauthenticated local sqld only. An auth token on an http:// URL is rejected at startup, because libSQL sends it as given and the token would cross the network in cleartext.

Note: only libsql:// makes the auth token a startup requirement. https:// and http:// are also accepted so a self-hosted or local sqld can run without one, which means a hosted https:// Turso URL with a missing or empty STORAGE_AUTH_TOKEN starts cleanly and fails on the first query instead. If a turso deployment starts and then reports auth errors on every read, check the token before anything else.

Application Settings

Variable Required Default Description
PORT No 3001 Application HTTP port
NODE_ENV No production Node environment (production or development)

Security

Variable Required Default Description
ENCRYPTION_KEY No - Master key for encrypting stored connection passwords and SSH tunnel secrets (min 16 characters)
ENCRYPTION_KEK_SALT No betterdb-kek-salt-v1 Salt used for key derivation (customize for additional security)
WORKSPACE_DISABLED No false Run without user control: no login, no roles, no Team page. Existing installs upgrading with the default see a register screen on first load
AUTH_SECRET No generated Session signing secret, at least 32 characters. Generated once and stored in BETTERDB_DATA_DIR/auth-secret when unset; set it explicitly when running more than one replica. When the data directory is read-only the API keeps booting with an in-memory secret and logs a warning: sessions are then invalidated on every restart
AUTH_PUBLIC_URL No inferred Public origin used for session cookies and CSRF origin checks, e.g. https://monitor.example.com. When unset, the origin check accepts the host the request carries under either scheme; setting it pins the check to this origin. Browser CLI and monitor tail WebSocket upgrades that carry an Origin header are accepted only from this origin, from the host the request carries, or, outside production (NODE_ENV other than production), from the Vite dev server at http://localhost:5173, and answer 403 otherwise. Session cookies are marked Secure only when this starts with https://, so installs served over HTTPS (including behind a TLS-terminating proxy) should set it and keep session cookies off plain HTTP
TRUST_PROXY No false Trust X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto. Set to a comma-separated list of proxy addresses/CIDRs (10.0.0.0/8,127.0.0.1) so client IPs, rate limits and the CSRF origin check see the browser-facing host, or to true when the API is only reachable through the proxy. A hop count (1) is refused with a warning: Fastify’s hop-count matching ignores the connecting address, so anyone who reaches the API directly can spoof the headers. Leave unset when the API is reachable directly
AUTH_BROKER_URL No https://betterdb.com Origin of the BetterDB sign-in page for Google and GitHub. In production (NODE_ENV=production) broker sign-in only turns on once AUTH_PUBLIC_URL is also set, because the callback origin must not come from the request’s Host header
AUTH_BROKER_DISABLED No false Turn off Google and GitHub sign-in, e.g. for air-gapped installs. The buttons are hidden and the broker sign-in routes answer 404
AUTH_BROKER_PUBLIC_KEY No embedded key PEM public key that verifies Google and GitHub sign-ins. Replaces the embedded key. The embedded key ships with the release that turns the feature on; until then, Google and GitHub sign-in stays off unless this is set. Write line breaks as \n to keep the key on one line
AUTH_BROKER_KEY_ID No brk-override Key id for the AUTH_BROKER_PUBLIC_KEY override
ACTIVITY_RETENTION_DAYS No 90 Days to keep activity log rows (API mutations, browser CLI commands, sign-ins and sign-outs). Older rows are deleted once a day and at startup

Roles: The first account to register becomes the workspace owner, who holds the admin role. Admins can change anything. Members can see the team’s names and roles but not email addresses. Members are read-only: every POST, PUT, PATCH, and DELETE request answers 403 except signing in/out and a small set of read-only POST endpoints (vector-search similarity search, profiling, and full-text search, plus the monitor session preflight check, which run a query but do not mutate data). Two reads are admin-only as well: GET /metrics/config and GET /metrics/config/:parameter answer 403 for members because the server configuration they return can carry credentials (for example requirepass or masterauth). The browser CLI and the monitor tail both require a signed-in session — an unauthenticated socket is rejected before the WebSocket handshake completes — and a member’s CLI session is restricted to read-only commands regardless of BETTERDB_UNSAFE_CLI; CONFIG, ACL, DEBUG, and CLIENT are refused for members outright because they expose server configuration and credentials, and SLOWLOG RESET, COMMANDLOG RESET, and LATENCY RESET are refused because they clear diagnostic state. Members can read monitor capture sessions, including the live tail; captured values are redacted only when MONITOR_REDACT_VALUES=true.

Sign in with Google or GitHub: Self-hosted installs can offer Google and GitHub sign-in buttons on the login, register and accept-invite screens, handled by a sign-in page at AUTH_BROKER_URL. The install needs outbound browser access to that origin; nothing else leaves your network. Signing in with Google or GitHub only works for an email that already belongs to a member, has a pending invitation, or arrives while the workspace is empty (in which case that person becomes the owner) — linking a Google or GitHub account to a monitor account is always by matching email. In production, this requires AUTH_PUBLIC_URL to be set: without it, GET /system/workspace reports the buttons as unavailable and the sign-in routes answer 404, because the callback address sent to the broker must come from a configured origin, not from whatever Host header the request happened to carry. The key that verifies these sign-ins ships with the release that turns the feature on. Until then, Google and GitHub sign-in stays off unless you set AUTH_BROKER_PUBLIC_KEY. AUTH_BROKER_DISABLED=true hides the buttons and makes the sign-in routes answer 404.

Invitations: Admins add people from the Team page. Creating an invitation returns a one-time link (<AUTH_PUBLIC_URL>/invite/<token>, or the requesting origin when AUTH_PUBLIC_URL is unset) that the admin copies and sends by any channel; the monitor sends no email. The link is shown once, expires after 7 days, and is stored only as a hash. Opening it shows the invited email and role; the invitee sets a name and password and is signed in as a member or admin according to the invitation. Admins can revoke pending invitations; re-inviting an email whose earlier invitation was revoked, accepted or expired issues a fresh link. The owner can change any other member’s role, remove members (which ends their sessions), and transfer ownership to an admin (a member must first be promoted to admin); the previous owner then remains an admin. Public registration stays closed after the first account.

Activity log: Every POST, PUT, PATCH, and DELETE request made by a signed-in user is recorded with the user, the source IP, the connection (X-Connection-Id), the route, the response status and, where the route has one, the affected object (a connection id, an invitation id, a member id, a bulk-delete job id). Sign-ins, sign-outs and invitation acceptances are recorded as auth.login / auth.logout, and every browser CLI command as cli.command with the command name and argument count (argument values are kept only for read commands, never for AUTH, HELLO, CONFIG, ACL or MIGRATE, and at most the first 16 arguments of 128 characters each). Requests the server refuses with 401 or 403 are not recorded. Admins read the log from the Activity tab of the Team page or from GET /api/workspace/activity (actor, action, from, to, cursor, limit ≤ 100), and rows are kept for ACTIVITY_RETENTION_DAYS.

Password Encryption: When ENCRYPTION_KEY is set, all connection passwords are encrypted at rest using envelope encryption (AES-256-GCM). Each password gets a unique encryption key (DEK) that is itself encrypted with a master key (KEK) derived from your ENCRYPTION_KEY. The same encryption covers SSH tunnel secrets (SSH password, key passphrase, and inline private keys).

  • If not set, passwords are stored in plaintext (a warning is logged at startup)
  • Use a strong, random key (e.g., openssl rand -base64 32)
  • Store the key securely (e.g., in a secrets manager)
  • If you lose the key, encrypted passwords cannot be recovered
  • Optionally set ENCRYPTION_KEK_SALT to a custom value for defense-in-depth (attackers would need both key and salt)

SSH Tunnels

A connection can reach its database through an SSH bastion/jump host (single hop) instead of connecting directly. Configure it per-connection via the web UI (“Connect via SSH tunnel”) or the connection API (sshTunnel field). Authentication is a password or a private key.

Variable Required Default Description
BETTERDB_SSH_KEY_DIR No - Directory that server-side SSH private keys must live in. Enables the “server file path” key source; a connection’s privateKeyPath must resolve inside this directory. Unset disables file-based keys.

Private key sources:

  • Inline (paste key) — the PEM key content is submitted with the connection. It is encrypted at rest only when ENCRYPTION_KEY is set; without it, the key (like connection passwords) is stored in plaintext and a warning is logged at startup. Works in any deployment, including managed/cloud.
  • Server file path — the key already exists on the monitor server’s filesystem. Set BETTERDB_SSH_KEY_DIR to the directory holding allowed keys; the connection’s privateKeyPath is resolved relative to it and rejected if it escapes the directory (no path traversal). Best for self-hosted deployments that mount keys as a secret volume.

Host key verification: pin the SSH server’s SHA256 host-key fingerprint on the connection (hostKeyFingerprint, e.g. SHA256:... from ssh-keyscan -t ed25519 HOST | ssh-keygen -lf -). When set, the tunnel is refused unless the server presents a matching key, which prevents a man-in-the-middle on the bastion path. When left unset the server key is accepted and a warning is logged.

The Valkey/Redis client connects to 127.0.0.1:<local-forwarded-port> through the tunnel. When TLS is enabled, the certificate is still validated against the real database hostname (SNI servername), not localhost.

Host key verification (TOFU): if you leave hostKeyFingerprint unset, the SSH server’s key is trusted on first connect and then pinned automatically — subsequent connects are refused if the key changes. Pin the fingerprint up front (see above) to avoid trusting the first key blind.

Password auth: password and keyboard-interactive (PAM) bastions are both supported; a password is answered to interactive prompts automatically.

Known limitation — cluster/Sentinel: only the configured connection is tunnelled. Cluster/Sentinel monitoring connects to peer nodes at the addresses they advertise, directly rather than through the tunnel, so nodes reachable only via the bastion (e.g. ElastiCache/MemoryDB in a private subnet) will not have per-node views. Use tunnels for single-node/primary monitoring.

Audit Trail

Variable Required Default Description
AUDIT_POLL_INTERVAL_MS No 60000 ACL audit polling interval (milliseconds)

Anomaly Detection

Variable Required Default Description
ANOMALY_DETECTION_ENABLED No true Enable anomaly detection features (Pro tier required)
ANOMALY_POLL_INTERVAL_MS No 1000 Anomaly detection polling interval (milliseconds)
ANOMALY_CACHE_TTL_MS No 3600000 Anomaly detection cache TTL (milliseconds)
ANOMALY_PROMETHEUS_INTERVAL_MS No 30000 Prometheus summary update interval (milliseconds)

Prometheus Metrics

Variable Required Default Description
PROMETHEUS_POLL_INTERVAL_MS No 5000 How often exported metrics are refreshed (milliseconds)
PROMETHEUS_STALENESS_MS No 3 × poll interval Drop a connection’s gauge series after this long without a successful INFO read (ms, ≥ 1000, raised to 3 × poll interval if lower)
METRICS_EXPORT_PROFILE No full vitals exports a fixed set of series per connection (no per-db, per-slot or pattern-labelled series); full exports everything. Applies to the scrape endpoint and the OTLP mirror
METRICS_SLOT_STATS_TOP_N No 100 Export slot stats for the top N slots by key count (0–16384, 0 disables). Ignored under vitals
METRICS_KEY_PATTERNS_TOP_N No 100 Export key-size gauges for the top N key patterns by total memory from the latest key analytics run (Pro; 0–10000, 0 disables). Ignored under vitals
METRICS_KEY_DETAILS_TOP_N No 10 Export per-key size gauges for the top N largest keys (Pro; 0–50, 0 disables only the per-key family — key names become label values). Ignored under vitals
PROMETHEUS_METRICS_ENABLED No true Set to false to disable /api/prometheus/metrics entirely (the OTLP mirror keeps exporting)
PROMETHEUS_METRICS_TOKEN No (none) Bearer token required to scrape the metrics endpoint; required when CLOUD_MODE is set and the endpoint is enabled
PROMETHEUS_METRICS_PUBLIC_TOKEN No (none) Optional second accepted scrape token, meant to be published (e.g. a live demo). Configuring either token switches the endpoint to bearer-required; rotating this one never affects scrapes using the primary

Client Analytics

Variable Required Default Description
CLIENT_ANALYTICS_POLL_INTERVAL_MS No 60000 Client analytics polling interval (milliseconds)
CLIENT_ANALYTICS_WINDOW_MS No 86400000 Window for unique name/user/IP counts, average age and per-name/per-user figures, in the Prometheus gauges and the default stats response (24 hours)
CLIENT_ANALYTICS_ROLLUP_RETENTION_DAYS No 90 Days of 5-minute rollup rows to keep (pruned hourly; the general retention window also applies, shorter wins). Also the furthest back the backfill reaches and the span of the headline peak
CLIENT_ANALYTICS_RAW_RETENTION_DAYS No - Days of raw client snapshots to keep, pruned hourly in chunks; unset = keep forever
CLIENT_ANALYTICS_QUERY_BUDGET_MS No 2000 Client analytics and audit stats storage calls slower than this log one warning per method, then stay quiet for that method for 5 minutes

A background aggregator folds new client snapshots into 5-minute rollups and refreshes the in-memory stats every 15 seconds. Catching up after downtime and backfilling older history both read raw snapshots in 1-hour chunks, at most 4 per connection per run. The cadence and chunking are fixed.

Stats merged across connections (GET /client-analytics/stats without a connection) sum each connection’s newest snapshot batch for the current counts. A connection counts toward current if its newest batch is within max(one bucket, two poll intervals) of the newest batch overall, that is max(5 minutes, 2 × the client analytics poll interval, CLIENT_ANALYTICS_POLL_INTERVAL_MS or the clientAnalyticsPollIntervalMs setting), so a single missed poll never drops a connection; one further behind still counts toward peaks, unique counts and average ages.

Cluster Auto-Registration

Variable Required Default Description
CLUSTER_AUTO_REGISTER_NODES No false Default for whether a cluster connection auto-registers its nodes as child connections; overridable per connection. See Cluster Auto-Registration
SENTINEL_AUTO_REGISTER_NODES No false Default for whether a Sentinel connection auto-registers the primaries and replicas it monitors; overridable per connection. See Sentinel Auto-Registration

Data Retention

Variable Required Default Description
LOCAL_RETENTION_DAYS No - Self-hosted only: days of monitoring history to keep (daily sweep). Seeds the setting when the settings row is first created; unset = keep forever

Self-hosted BetterDB keeps stored monitoring history indefinitely by default, bounded only by your storage backend’s capacity and disk space. The retention window, once set, covers every store: slow log and command log entries, client/latency/memory snapshots, latency histograms, anomaly events and correlated groups, ACL audit entries, key pattern snapshots and hot keys, webhook deliveries, monitor captures (sessions, chunks, triggers, scheduled), AI cache samples, OTel spans, command/latency stats samples, and vector index snapshots.

To keep the database from growing forever, set a retention window from Settings → Data Retention in the UI. On a fresh install the window can also be seeded with LOCAL_RETENTION_DAYS — the env var applies only when the settings row is first created; after that the settings page owns the value, so clearing it there sticks even if the env var stays set. A daily sweep then deletes history older than the window, and the high-volume sample stores (command/latency stats samples, vector index snapshots, AI cache samples, OTel spans) are additionally trimmed to the same window on an hourly cycle. Nothing is deleted while the window is unset.

Client analytics keeps two stores. Raw client snapshots (client_snapshots) follow the retention window above like every other store, and can additionally be capped with CLIENT_ANALYTICS_RAW_RETENTION_DAYS (unset by default, so raw snapshots are kept until the retention window removes them). The 5-minute rollups (client_analytics_rollups, 288 rows per connection per day) are also swept by the retention window like every other store, and are additionally pruned hourly after CLIENT_ANALYTICS_ROLLUP_RETENTION_DAYS (default 90); whichever is shorter wins. The sweep also removes rollups left behind by removed connections. Capping raw snapshots with CLIENT_ANALYTICS_RAW_RETENTION_DAYS therefore does not remove the long-range history served from rollups.

BetterDB Cloud applies the tier-based retention policy (Community 7 days, Pro 90, Enterprise 365) automatically; the local retention setting has no effect there.

Webhooks

Variable Required Default Description
FRONTEND_URL No - Public base URL of the web UI (e.g. https://monitor.example.com). Used to build “View in BetterDB” links in Slack/Discord messages; unset = messages render without the link

License Configuration

Pro/Enterprise features are unlocked with a license, and there are two ways to provide one. Pick based on whether the monitor can reach the internet:

Option 1 — Online license key (default)

For monitors that can reach betterdb.com. Set your key; the monitor validates it online and caches a locally-verified signed token, so your tier keeps working through short outages (up to 7 days) and restarts.

-e BETTERDB_LICENSE_KEY=btdb_xxxxxxxxxxxxxxxx

Option 2 — Offline / air-gapped license token

For monitors with no internet access. Download a signed offline license token from betterdb.com/account/licenses (Pro/Enterprise), and provide it as a mounted file or an inline string. The monitor verifies it locally against embedded public keys and never phones home — telemetry and update checks are automatically disabled.

# as a mounted file (recommended — pairs with a Docker/K8s secret mount)
-e BETTERDB_OFFLINE_LICENSE_FILE=/run/secrets/betterdb-license.jwt
# …or inline as a JWT string
-e BETTERDB_OFFLINE_LICENSE=eyJhbGciOiJSUzI1NiIs...

You can also paste/upload it at runtime: Settings → License → “Air-gapped environment? Activate an offline license.” See Offline & Air-Gapped Licenses for the full flow, verification precedence, and key-rotation runbook.

Variable Required Default Description
BETTERDB_LICENSE_KEY No - Online license key (Option 1); validated against ENTITLEMENT_URL.
BETTERDB_OFFLINE_LICENSE No - Offline license token as a JWT string (Option 2).
BETTERDB_OFFLINE_LICENSE_FILE No - Path to an offline license .jwt (Option 2); falls back to <data-dir>/license-offline.jwt.
BETTERDB_DATA_DIR No /app/data (Docker) / ./data (local) Directory for persisted license state (license.jwt, license-offline.jwt, license-clock.json), written mode 0600.
BETTERDB_TELEMETRY No true Enable anonymous telemetry (set false to disable; force-disabled in air-gapped mode).
ENTITLEMENT_URL No https://www.betterdb.com/api/v1/entitlements Entitlement validation endpoint (Option 1).
LICENSE_ALLOW_UNSIGNED No false Accept unsigned entitlement responses from a legacy server. Use only during migration — an unsigned paid grant is otherwise refused.
LICENSE_CACHE_TTL_MS No 3600000 License cache TTL (milliseconds)
LICENSE_MAX_STALE_MS No 604800000 Maximum stale license age (milliseconds)
LICENSE_TIMEOUT_MS No 10000 License validation timeout (milliseconds)

Persisting license state (Docker/Kubernetes): the signed-token outage grace (Option 1) and UI/runtime-activated offline licenses (Option 2) are written to the data directory — /app/data in the image. If you don’t mount a volume there, that state is re-fetched online, or lost for air-gapped hosts, on every restart/upgrade. The container runs as UID 1001 — a freshly-created volume mounts root-owned, so the monitor logs EACCES … open '/app/data/license.jwt' and can’t persist. Make the volume writable by UID 1001 once:

docker volume create betterdb-data
docker run --rm -v betterdb-data:/d alpine chown 1001:1001 /d   # one-time
docker run -d -v betterdb-data:/app/data -e BETTERDB_OFFLINE_LICENSE_FILE=... betterdb/monitor

Telemetry: BetterDB Monitor collects anonymous usage telemetry to help improve the product. No personally identifiable information is collected. The telemetry includes:

  • Instance ID (deterministic hash derived from DB_HOST, DB_PORT, STORAGE_URL, and license key)
  • Application version
  • Platform and architecture (e.g., linux, x64)
  • Node.js version
  • License tier (community/pro/enterprise)

To disable telemetry, set BETTERDB_TELEMETRY=false in your environment variables.

Version Update Checks

Variable Required Default Description
VERSION_CHECK_INTERVAL_MS No 3600000 Version check interval (milliseconds, default: 1 hour)

BetterDB Monitor automatically checks for new versions and displays an update banner in the web UI when a newer version is available. Version information is obtained from:

  1. Entitlement server (piggybacked on license/telemetry requests) - primary source
  2. GitHub Releases API - fallback when entitlement data is unavailable

Key Analytics (Pro Tier)

Variable Required Default Description
KEY_ANALYTICS_SAMPLE_SIZE No 10000 Number of keys to sample for analytics
KEY_ANALYTICS_SCAN_BATCH_SIZE No 1000 Batch size for key scanning operations
KEY_ANALYTICS_INTERVAL_MS No 300000 Key analytics collection interval (milliseconds)

Per-pattern counts come from the sampled keys:

  • Keys, w/ TTL, Expiring Soon and Stale Keys are scaled from the sample to an estimate for the whole pattern. A key is stale when its idle time (OBJECT IDLETIME) exceeds 24 hours; the count is taken key by key, not derived from the average idle time. Snapshots collected by an agent older than this change show the stale count as N/A; the Stale Keys summary sums the patterns with a known count and shows N/A when none has one. Snapshots stored before this change keep their original values.
  • Hot Keys (in sample) and Cold Keys (in sample) are counts within the sample, not totals: keys whose access frequency is above the pattern average, or below half of it.

Key analytics history follows the standard retention policy (see Data Retention): self-hosted installs keep it until a retention window is configured; BetterDB Cloud prunes it at the tier window (Community 7 days, Pro 90, Enterprise 365).

Note: Key analytics features require a Pro tier license.

AI Features (Experimental)

Variable Required Default Description
AI_ENABLED No false Enable AI-powered features (chatbot, RAG)
OLLAMA_BASE_URL No http://localhost:11434 Ollama API endpoint for LLM inference
OLLAMA_KEEP_ALIVE No 24h Keep-alive duration for Ollama models
AI_USE_LLM_CLASSIFICATION No false Use LLM for anomaly classification
LANCEDB_PATH No ./data/lancedb Path to LanceDB vector database
VALKEY_DOCS_PATH No ./data/valkey-docs Path to indexed Valkey documentation

Note: AI features are experimental and require explicit opt-in. You must have Ollama running locally or accessible at the configured URL.

Docker Usage

Building the Image

pnpm docker:build

For multi-arch builds (AMD64 + ARM64):

docker buildx create --name mybuilder --use --bootstrap
pnpm docker:build:multiarch

Running the Container

Basic Setup (Memory Storage)

docker run -d \
  --name betterdb-monitor \
  -p 3001:3001 \
  -e DB_HOST=your-valkey-host \
  -e DB_PORT=6379 \
  -e DB_PASSWORD=your-password \
  -e BETTERDB_LICENSE_KEY=your-license-key \
  -e STORAGE_TYPE=memory \
  betterdb/monitor

Note: STORAGE_TYPE=memory loses users and sessions on every restart, so the instance returns to the register screen and the next visitor claims the owner account. Use sqlite or postgres once other people can reach it.

PostgreSQL Storage

docker run -d \
  --name betterdb-monitor \
  -p 3001:3001 \
  -e DB_HOST=your-valkey-host \
  -e DB_PORT=6379 \
  -e DB_PASSWORD=your-password \
  -e BETTERDB_LICENSE_KEY=your-license-key \
  -e STORAGE_TYPE=postgres \
  -e STORAGE_URL=postgresql://user:pass@postgres-host:5432/dbname \
  betterdb/monitor

Turso Storage

docker run -d \
  --name betterdb-monitor \
  -p 3001:3001 \
  -e DB_HOST=your-valkey-host \
  -e DB_PORT=6379 \
  -e DB_PASSWORD=your-password \
  -e BETTERDB_LICENSE_KEY=your-license-key \
  -e STORAGE_TYPE=turso \
  -e STORAGE_URL=libsql://your-db-your-org.turso.io \
  -e STORAGE_AUTH_TOKEN=your-turso-auth-token \
  betterdb/monitor

Custom Port

You can run the application on any port by setting the PORT environment variable:

docker run -d \
  --name betterdb-monitor \
  -p 8080:8080 \
  -e PORT=8080 \
  -e DB_HOST=your-valkey-host \
  -e DB_PORT=6379 \
  -e DB_PASSWORD=your-password \
  -e BETTERDB_LICENSE_KEY=your-license-key \
  -e STORAGE_TYPE=memory \
  betterdb/monitor

Note: STORAGE_TYPE=memory loses users and sessions on every restart, so the instance returns to the register screen and the next visitor claims the owner account. Use sqlite or postgres once other people can reach it.

Important: When not using --network host, the -p flag port mapping must match the PORT environment variable (e.g., -p 8080:8080 -e PORT=8080).

Host Network Mode

If your Valkey and PostgreSQL are running on the same host:

docker run -d \
  --name betterdb-monitor \
  --network host \
  -e DB_HOST=localhost \
  -e DB_PORT=6380 \
  -e DB_PASSWORD=devpassword \
  -e BETTERDB_LICENSE_KEY=your-license-key \
  -e STORAGE_TYPE=postgres \
  -e STORAGE_URL=postgresql://dev:devpass@localhost:5432/postgres \
  betterdb/monitor

Note: With --network host, no -p flag is needed. The application uses the PORT environment variable directly (default: 3001).

Air-Gapped / Offline License

For hosts with no internet access, mount the offline license token and a writable data volume (see License Configuration for the details):

# one-time: make the data volume writable by the container's UID (1001)
docker volume create betterdb-data
docker run --rm -v betterdb-data:/d alpine chown 1001:1001 /d

docker run -d \
  --name betterdb-monitor \
  -p 3001:3001 \
  -e DB_HOST=your-valkey-host \
  -e DB_PORT=6379 \
  -e DB_PASSWORD=your-password \
  -v /path/to/betterdb-license.jwt:/run/secrets/betterdb-license.jwt:ro \
  -e BETTERDB_OFFLINE_LICENSE_FILE=/run/secrets/betterdb-license.jwt \
  -v betterdb-data:/app/data \
  -e STORAGE_TYPE=memory \
  betterdb/monitor

Note: STORAGE_TYPE=memory loses users and sessions on every restart, so the instance returns to the register screen and the next visitor claims the owner account. Use sqlite or postgres once other people can reach it.

No BETTERDB_LICENSE_KEY is set, so the monitor runs fully offline — no outbound requests, telemetry disabled. Verify with curl -b cookies.txt -c cookies.txt http://localhost:3001/api/license/status (source: offline-token, mode: offline, airGapped: true); see Authenticating API Requests for the cookie.

Accessing the Application

Once running, access the web interface at:

  • Web UI: http://localhost:3001 (or your custom port)
  • Health Check: http://localhost:3001/api/health
  • Prometheus Metrics: http://localhost:3001/api/prometheus/metrics

HTTP Endpoints

Endpoint Description
/ Web UI dashboard
/api/health Health check endpoint
/docs Swagger/OpenAPI documentation
/api/prometheus/metrics Prometheus metrics endpoint

All API endpoints are prefixed with /api when accessed through the web server. The Swagger UI is the exception: it is served at /docs, without the prefix.

Authenticating API Requests

With user control enabled (the default on self-hosted installs), every API endpoint below requires a signed-in session; without one the API answers 401. The curl examples throughout the docs assume a session cookie obtained once:

# Sign in and store the session cookie
curl -c cookies.txt -X POST http://localhost:3001/api/auth/sign-in/email \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "password": "your-password"}'

# Send it on every later call; -c saves a renewed session cookie back to the jar
curl -b cookies.txt -c cookies.txt http://localhost:3001/api/connections

These routes stay open without a session: /api/health, /api/version, /api/system/workspace, /api/prometheus/metrics, the Swagger UI at /docs, the /api/auth and /api/invite routes, and reads under /api/mcp. Writes under /api/mcp that apply changes — approving or rejecting cache and memory proposals — need a session too. The Prometheus metrics endpoint has its own gate on top of this: it can be turned off with PROMETHEUS_METRICS_ENABLED=false and guarded with a PROMETHEUS_METRICS_TOKEN bearer credential (see Prometheus Metrics).

Set WORKSPACE_DISABLED=true to run without user control, in which case every endpoint is reachable without signing in.

Sessions are cached in a signed cookie for 30 seconds, so a role change or a removed member takes effect within that window rather than on the next request.

Health

Endpoint Method Description
/health GET Health status of API server, Valkey/Redis, and storage backend

Version

Endpoint Method Description
/version GET Current and latest version info, update availability

Settings

Endpoint Method Description
/settings GET Get current application settings
/settings PUT Update application settings
/settings/reset POST Reset settings to defaults from environment variables

Audit Trail

Endpoint Method Description
/audit/entries GET Get ACL audit log entries with optional filters
/audit/stats GET Get aggregated audit statistics
/audit/failed-auth GET Get failed authentication attempts
/audit/by-user GET Get audit entries for a specific username

Client Analytics

Endpoint Method Description
/client-analytics/snapshots GET Get historical client connection snapshots
/client-analytics/timeseries GET Get aggregated client counts over time
/client-analytics/stats GET Get client analytics statistics
/client-analytics/history GET Get connection history for specific client
/client-analytics/cleanup DELETE Manually trigger cleanup of old data
/client-analytics/command-distribution GET Get command frequency distribution by client
/client-analytics/idle-connections GET Identify connections idle for extended periods
/client-analytics/buffer-anomalies GET Detect clients with unusual buffer sizes
/client-analytics/activity-timeline GET Get activity over time for correlation
/client-analytics/spike-detection GET Automatically detect unusual activity spikes

/client-analytics/stats without startTime/endTime covers the last CLIENT_ANALYTICS_WINDOW_MS (default 24 hours) and is served from the 5-minute rollups. With an explicit range (a missing endTime means now; a missing startTime means one window before endTime), spans up to 6 hours for a single connection are computed exactly from raw snapshots. Longer spans, and any range without a connection header, are served from the rollups and are aligned to 5-minute bucket boundaries: a range edge inside a bucket includes the whole bucket. With an explicit range, peak connections, the unique name/user/IP counts and the per-name/per-user figures all describe the requested range. Without a range, for a single connection, the headline peakConnections/peakTimestamp cover the whole rollup retention (CLIENT_ANALYTICS_ROLLUP_RETENTION_DAYS, default 90 days), while the unique counts, average age and per-name/per-user figures (including their peaks) cover the window.

/client-analytics/history defaults startTime to one window before endTime when it is omitted, and /client-analytics/idle-connections looks at the last window instead of all history. The other analysis endpoints keep their existing 1-hour or 24-hour defaults.

Metrics

Endpoint Method Description
/metrics/info GET Parsed INFO command output
/metrics/slowlog GET Slowlog entries
/metrics/slowlog/length GET Current slowlog length
/metrics/slowlog DELETE Reset slowlog
/metrics/slowlog/patterns GET Aggregated slowlog pattern analysis
/metrics/commandlog GET Commandlog entries (Valkey 8.1+)
/metrics/commandlog/length GET Commandlog length (Valkey 8.1+)
/metrics/commandlog DELETE Reset commandlog (Valkey 8.1+)
/metrics/commandlog/patterns GET Commandlog pattern analysis (Valkey 8.1+)
/metrics/latency/latest GET Latest latency monitoring events
/metrics/latency/history/:eventName GET Latency history for specific event
/metrics/latency/histogram GET Latency histogram for commands
/metrics/latency/doctor GET Automated latency analysis report
/metrics/latency DELETE Reset latency monitoring data
/metrics/memory/stats GET Detailed memory usage statistics
/metrics/memory/doctor GET Automated memory analysis report
/metrics/clients GET List of currently connected clients
/metrics/clients/:id GET Information about specific client
/metrics/clients DELETE Terminate client connections
/metrics/acl/log GET ACL security log entries
/metrics/acl/log DELETE Clear ACL log
/metrics/role GET Replication role and status
/metrics/cluster/info GET Cluster information and status
/metrics/cluster/nodes GET Information about all cluster nodes
/metrics/cluster/slot-stats GET Per-slot statistics (Valkey 8.0+)
/metrics/config GET Configuration values matching pattern (admin only)
/metrics/config/:parameter GET Value of specific config parameter (admin only)
/metrics/dbsize GET Number of keys in current database
/metrics/lastsave GET Unix timestamp of last RDB save

Prometheus

Endpoint Method Description
/api/prometheus/metrics GET Prometheus-formatted metrics for scraping

Returns 401 when PROMETHEUS_METRICS_TOKEN is set and the scrape omits it, and 404 when PROMETHEUS_METRICS_ENABLED=false.

Runtime Settings

The following settings can be modified at runtime via the /settings API endpoint without requiring an application restart:

Setting Default Description
auditPollIntervalMs 60000 ACL audit log polling interval (milliseconds)
clientAnalyticsPollIntervalMs 60000 Client analytics data collection interval (milliseconds)
anomalyPollIntervalMs 1000 Anomaly detection polling interval (milliseconds)
anomalyCacheTtlMs 3600000 Anomaly detection cache TTL (milliseconds)
anomalyPrometheusIntervalMs 30000 Prometheus summary update interval (milliseconds)

Example: Update Settings

curl -b cookies.txt -c cookies.txt -X PUT http://localhost:3001/api/settings \
  -H "Content-Type: application/json" \
  -d '{
    "auditPollIntervalMs": 30000,
    "clientAnalyticsPollIntervalMs": 45000
  }'

Settings are persisted to the storage backend (PostgreSQL or SQLite) and survive restarts. When using STORAGE_TYPE=memory, settings revert to environment variable defaults on restart.

Container Management

View Logs

docker logs -f betterdb-monitor

Stop Container

docker stop betterdb-monitor

Remove Container

docker rm betterdb-monitor

Replace Running Container

Automatically remove existing container and start a new one:

docker rm -f betterdb-monitor 2>/dev/null; docker run -d \
  --name betterdb-monitor \
  -p 3001:3001 \
  -e DB_HOST=your-valkey-host \
  -e DB_PORT=6379 \
  -e DB_PASSWORD=your-password \
  -e BETTERDB_LICENSE_KEY=your-license-key \
  -e STORAGE_TYPE=postgres \
  -e STORAGE_URL=postgresql://user:pass@postgres-host:5432/dbname \
  betterdb/monitor

Inspect Container

# View container details
docker inspect betterdb-monitor

# View container stats
docker stats betterdb-monitor

# View container port mappings
docker port betterdb-monitor

Docker Image Details

  • Base Image: node:25-alpine
  • Size: ~188MB (optimized, no build tools)
  • Platforms: linux/amd64, linux/arm64
  • Contains: Backend API + Frontend static files (served by Fastify)
  • Excluded: SQLite support (use PostgreSQL or Memory storage)

Health Check

The Docker image includes a built-in health check that runs every 30 seconds:

# View health status
docker inspect --format='' betterdb-monitor

The health check validates that the HTTP server is responding on the configured PORT.