Reference
Environment Variables
Full reference for every chmonitor environment variable, grouped by category with build-time vs runtime notes.
Self-hosters need three vars. Everything else is optional and listed here.
CLICKHOUSE_HOST=http://localhost:8123
CLICKHOUSE_USER=monitoring
CLICKHOUSE_PASSWORD=change-medocker run -d -p 3000:3000 \
-e CLICKHOUSE_HOST='http://localhost:8123' \
-e CLICKHOUSE_USER='monitoring' \
-e CLICKHOUSE_PASSWORD='change-me' \
ghcr.io/chmonitor/chmonitor:X.Y.Zkubectl create secret generic chmonitor-clickhouse \
--from-literal=CLICKHOUSE_HOST='http://localhost:8123' \
--from-literal=CLICKHOUSE_USER='monitoring' \
--from-literal=CLICKHOUSE_PASSWORD='change-me'Copy apps/dashboard/.env.example
for a short template. Polar license checkout belongs on landing / apps/cloud-hooks,
not the standalone monitor.
All variables
Search or filter every variable the dashboard reads. The list is generated from the code, so it stays current. Descriptions for each group follow below.
| Name | In .env.example | Default |
|---|---|---|
AGENTSTATE_AI_ENRICH | — | — |
AGENTSTATE_API_KEY | — | — |
AGENT_API_TOKEN | — | — |
AGENT_DEBUG | — | — |
AGENT_ENABLE_CONTROL_TOOLS | — | — |
AGENT_JSON_RENDER_PATCH_GUARD_DEBUG | — | — |
ANYROUTER_API_BASE | — | — |
ANYROUTER_API_KEY | yes | — |
ANYROUTER_DYNAMIC_MODELS | — | — |
ANYROUTER_METRICS_CANDIDATE_CAP | — | — |
ANYROUTER_OAUTH_CLIENT_ID | — | — |
ANYROUTER_PRESETS | — | — |
ANYROUTER_PRESETS_MAX | — | — |
ANYROUTER_TOP_MODELS_N | — | — |
APP_CATEGORY | — | — |
APP_NAME | — | — |
APP_REFERER | — | — |
APP_SOURCE | — | — |
APP_VERSION | — | — |
CF_PAGES | — | — |
CHM_AGENT_FIRECRAWL_ALLOW_DOMAINS | yes | — |
CHM_AGENT_FIRECRAWL_MCP | yes | — |
CHM_AGENT_INTEGRATION_PROBE_TIMEOUT_MS | — | — |
CHM_AGENT_INTEGRATION_PROBE_TTL_MS | — | — |
CHM_ALLOW_PRIVATE_HOSTS | yes | — |
CHM_ANALYTICS_HOST | — | — |
CHM_ANALYTICS_KEY | — | — |
CHM_API_KEY_SECRET | yes | — |
CHM_AUTH_PROVIDER | yes | — |
CHM_AUTH_REQUIRED_FEATURES | — | — |
CHM_CF_ACCESS_AUD | — | — |
CHM_CF_ACCESS_TEAM_DOMAIN | — | — |
CHM_CLERK_PUBLIC_READ | — | — |
CHM_CLOUD_MODE | yes | — |
CHM_CONFIG_FILE | yes | — |
CHM_DEPLOYMENT_MODE | yes | — |
CHM_DISABLED_FEATURES | — | — |
CHM_EDITION | — | — |
CHM_EVENTS_INGEST_TOKEN | — | — |
CHM_EVENTS_REEMIT_WEBHOOK_URL | — | — |
CHM_FEATURE_CONVERSATION_DB | yes | — |
CHM_FEATURE_PEERDB_AGENT | — | — |
CHM_FEATURE_PEERDB_ENABLED | — | — |
CHM_FEATURE_POSTGRES_SOURCE | yes | — |
CHM_FEATURE_USER_CONNECTIONS_DB | yes | — |
CHM_FEATURE_WEBHOOK_SUBSCRIPTIONS | — | — |
CHM_GUEST_AI_REQUESTS_PER_DAY | — | — |
CHM_HEALTH_SWEEP_ENABLED | — | — |
CHM_LICENSE_KEY | yes | — |
CHM_MCP_PUBLIC | — | — |
CHM_POLAR_SERVER | — | — |
CHM_PROXY_AUTH_HEADER | — | — |
CHM_PROXY_AUTH_SECRET | — | — |
CHM_PROXY_SHARED_SECRET_HEADER | — | — |
CHM_STATE_CLICKHOUSE_DATABASE | — | — |
CHM_STATE_CLICKHOUSE_PASSWORD | — | — |
CHM_STATE_CLICKHOUSE_TABLE_PREFIX | — | — |
CHM_STATE_CLICKHOUSE_URL | — | — |
CHM_STATE_CLICKHOUSE_USER | — | — |
CHM_TABLE_MAX_RESULT_ROWS | yes | — |
CHM_TELEMETRY_ENDPOINT | — | — |
CHM_TRUSTED_ALLOWED_GROUPS | — | — |
CHM_TRUSTED_ALLOW_INSECURE | — | — |
CHM_TRUSTED_AUTH_SECRET | — | — |
CHM_TRUSTED_AVATAR_HEADER | — | — |
CHM_TRUSTED_CUSTOM_HEADERS | — | — |
CHM_TRUSTED_EMAIL_HEADER | — | — |
CHM_TRUSTED_GROUPS_HEADER | — | — |
CHM_TRUSTED_NAME_HEADER | — | — |
CHM_TRUSTED_ROLE_HEADER | — | — |
CHM_TRUSTED_SHARED_SECRET_HEADER | — | — |
CHM_TRUSTED_USER_HEADER | — | — |
CHM_USER_CONNECTIONS_ENCRYPTION_KEY | — | — |
CHM_WEEKLY_REPORT_HOSTS | — | — |
CLERK_API_URL | — | — |
CLERK_OAUTH_ISSUER | — | — |
CLERK_PUBLISHABLE_KEY | — | — |
CLERK_SECRET_KEY | yes | — |
CLERK_WEBHOOK_SECRET | — | — |
CLICKHOUSE_DATABASE | — | — |
CLICKHOUSE_EXCLUDE_USER_DEFAULT | — | — |
CLICKHOUSE_HOST | yes | http://localhost:8123 |
CLICKHOUSE_MANAGEMENT_ENABLED | yes | — |
CLICKHOUSE_MAX_EXECUTION_TIME | — | — |
CLICKHOUSE_NAME | yes | — |
CLICKHOUSE_PASSWORD | yes | — |
CLICKHOUSE_POOL_CLEANUP_INTERVAL | — | — |
CLICKHOUSE_POOL_SIZE | — | — |
CLICKHOUSE_POOL_TIMEOUT | — | — |
CLICKHOUSE_USER | yes | default |
CLOUDFLARE_WORKERS | — | — |
CRON_SECRET | — | — |
DATABASE_URL | — | — |
EVENTS_TABLE_NAME | — | — |
GITHUB_WEBHOOK_SECRET | — | — |
HEALTH_ALERT_COOLDOWN_MINUTES | — | — |
HEALTH_ALERT_DASHBOARD_URL | yes | — |
HEALTH_ALERT_DIGEST_MINUTES | — | — |
HEALTH_ALERT_EMAIL_ENABLED | — | — |
HEALTH_ALERT_EMAIL_FROM | — | — |
HEALTH_ALERT_EMAIL_MIN_SEVERITY | — | — |
HEALTH_ALERT_EMAIL_PROVIDER_URL | — | — |
HEALTH_ALERT_EMAIL_TO | — | — |
HEALTH_ALERT_ENABLED | — | — |
HEALTH_ALERT_HEALTHCHECKS_URL | — | — |
HEALTH_ALERT_INSTANCE_NAME | yes | — |
HEALTH_ALERT_METADATA | yes | — |
HEALTH_ALERT_MIN_SEVERITY | — | — |
HEALTH_ALERT_NTFY_ENABLED | — | — |
HEALTH_ALERT_NTFY_MIN_SEVERITY | — | — |
HEALTH_ALERT_NTFY_TOKEN | — | — |
HEALTH_ALERT_NTFY_URL | — | — |
HEALTH_ALERT_OPSGENIE_API_KEY | — | — |
HEALTH_ALERT_OPSGENIE_ENABLED | — | — |
HEALTH_ALERT_OPSGENIE_MIN_SEVERITY | — | — |
HEALTH_ALERT_OPSGENIE_REGION | — | — |
HEALTH_ALERT_PAGERDUTY_API_KEY | — | — |
HEALTH_ALERT_PAGERDUTY_ENABLED | — | — |
HEALTH_ALERT_PAGERDUTY_MIN_SEVERITY | — | — |
HEALTH_ALERT_PAGERDUTY_ROUTING_KEY | — | — |
HEALTH_ALERT_PUSHOVER_ENABLED | — | — |
HEALTH_ALERT_PUSHOVER_MIN_SEVERITY | — | — |
HEALTH_ALERT_PUSHOVER_TOKEN | — | — |
HEALTH_ALERT_PUSHOVER_USER | — | — |
HEALTH_ALERT_TELEGRAM_BOT_TOKEN | — | — |
HEALTH_ALERT_TELEGRAM_CHAT_ID | — | — |
HEALTH_ALERT_TELEGRAM_ENABLED | — | — |
HEALTH_ALERT_TELEGRAM_MIN_SEVERITY | — | — |
HEALTH_ALERT_TWILIO_ACCOUNT_SID | — | — |
HEALTH_ALERT_TWILIO_AUTH_TOKEN | — | — |
HEALTH_ALERT_TWILIO_FROM | — | — |
HEALTH_ALERT_TWILIO_MIN_SEVERITY | — | — |
HEALTH_ALERT_TWILIO_TO | — | — |
HEALTH_ALERT_WEBHOOK_ENABLED | — | — |
HEALTH_ALERT_WEBHOOK_MIN_SEVERITY | — | — |
HEALTH_ALERT_WEBHOOK_TARGETS | — | — |
HEALTH_ALERT_WEBHOOK_URL | — | — |
HEALTH_HYSTERESIS_BREACHES | — | — |
HEALTH_HYSTERESIS_CLEARS | — | — |
LLM_API_BASE | — | — |
LLM_API_KEY | yes | — |
LLM_EXTRA_MODELS | — | — |
LLM_MODEL | yes | — |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | — | — |
NVIDIA_API_BASE | — | — |
NVIDIA_API_KEY | — | — |
NVIDIA_DYNAMIC_MODELS | — | — |
NVIDIA_TOP_MODELS_N | — | — |
OPENROUTER_API_KEY | — | — |
OPENROUTER_APP_NAME | — | — |
OPENROUTER_DYNAMIC_MODELS | — | — |
OPENROUTER_FREE_FALLBACK_MODEL | — | — |
OPENROUTER_MODELS_API | — | — |
OPENROUTER_REFERER | — | — |
OPENROUTER_TOP_MODELS_N | — | — |
PEERDB_API_URL | — | — |
PEERDB_CACHE_MAX_ENTRIES | — | — |
PEERDB_CACHE_TTL_MS | — | — |
PEERDB_FETCH_TIMEOUT_MS | — | — |
POLAR_ACCESS_TOKEN | — | — |
POLAR_WEBHOOK_SECRET | — | — |
POSTGRES_DATABASE | — | — |
POSTGRES_HOST | — | — |
POSTGRES_NAME | — | — |
POSTGRES_PASSWORD | — | — |
POSTGRES_PORT | — | — |
POSTGRES_PRISMA_URL | — | — |
POSTGRES_SSLMODE | — | — |
POSTGRES_URL | — | — |
POSTGRES_USER | — | — |
RATE_LIMIT_AGENT_GUEST_PER_MIN | — | — |
RATE_LIMIT_AGENT_PER_MIN | — | — |
RATE_LIMIT_API_PER_MIN | — | — |
RATE_LIMIT_BROWSER_CONN_PER_MIN | — | — |
RATE_LIMIT_DEVICE_CODE_PER_MIN | — | — |
RATE_LIMIT_MCP_PER_MIN | — | — |
SLACK_CLIENT_ID | — | — |
SLACK_CLIENT_SECRET | — | — |
SLACK_OAUTH_REDIRECT_URL | — | — |
SLACK_SIGNING_SECRET | — | — |
SLACK_TOKEN_ENCRYPTION_KEY | — | — |
One canonical name per setting
A setting the browser needs (auth provider, cloud mode, feature flags) is inlined
into the client bundle at build time as a VITE_* var. You do not set it
twice. Set the canonical CHM_* name once and vite.config.ts derives the
matching client VITE_*:
| Set this (canonical) | Derives this (client, build-time) |
|---|---|
CHM_AUTH_PROVIDER | VITE_AUTH_PROVIDER |
CHM_CLERK_PUBLISHABLE_KEY | VITE_CLERK_PUBLISHABLE_KEY |
CHM_CLOUD_MODE | VITE_CLOUD_MODE |
CHM_FEATURE_CONVERSATION_DB | VITE_FEATURE_CONVERSATION_DB |
CHM_FEATURE_USER_CONNECTIONS_DB | VITE_FEATURE_USER_CONNECTIONS_DB |
CHM_EDITION | VITE_EDITION |
Precedence per var:
The explicit VITE_* form still works as an override. Legacy NEXT_PUBLIC_*
names are no longer read.
Every boolean flag (CHM_CLOUD_MODE, CHM_CLERK_PUBLIC_READ, CHM_FEATURE_*,
AGENT_ENABLE_CONTROL_TOOLS, …) accepts the same values on the server and in
the build: true / 1 / yes / on and false / 0 / no / off,
case-insensitive. Anything else counts as unset, so the deployment-mode default
applies (an unrecognised value baked into a client VITE_* flag is false).
CHM_CLOUD_MODE also accepts cloud.
A dual-surface setting must still be present at both build time (so vite
inlines its VITE_*) and runtime (so the server reads it) — but it is the same
single name in both places.
Cloud mode is a build-time contract. CHM_CLOUD_MODE /
CHM_DEPLOYMENT_MODE=cloud must be set before the build so VITE_CLOUD_MODE
is inlined into the client bundle. Setting it only at runtime on a prebuilt
OSS image (e.g. the published Docker image) splits the product: the server
enforces cloud behaviour (demo-host guard) while the client still renders OSS
UI. /api/healthz detects and warns on this mismatch and reports a cloudMode
object. The reverse — a cloud build with the runtime var unset — is safe
(fail-closed: both halves degrade to OSS).
Operators set CHM_* (and CLICKHOUSE_* / third-party secrets). Do not also
set VITE_*. The build derives client values. Legacy NEXT_PUBLIC_* names are
no longer read — see Migrate to v0.3.
Build-time vs runtime
There are two categories of environment variables:
| Category | When resolved | How to set | Example |
|---|---|---|---|
| Build-time | When pnpm run build runs | CI / shell before the build | CHM_FEATURE_CONVERSATION_DB, CHM_AUTH_PROVIDER, CHM_CLERK_PUBLISHABLE_KEY |
| Runtime | When the server process starts per request | wrangler.toml [vars], wrangler secret put, Docker -e, k8s ConfigMap/Secret | CLICKHOUSE_HOST, CONVERSATION_STORE_BACKEND |
VITE_* variables are build-time only
Setting a VITE_* var in wrangler.toml [vars] or as a Docker -e flag has no
effect on the running app — the bundle already has the value from when it was
built. Always rebuild and redeploy when changing these variables.
All other vars (unless noted) are runtime — read from process.env when the
Worker or Node process handles a request.
ClickHouse connection
Required for a working dashboard: host, user, password. Name is optional.
| Variable | Required | Default | Purpose |
|---|---|---|---|
CLICKHOUSE_HOST | Yes | — | Comma-separated ClickHouse host URLs, e.g. http://ch-1:8123,http://ch-2:8123. |
CLICKHOUSE_USER | Yes | default | Comma-separated usernames, one per host. |
CLICKHOUSE_PASSWORD | Yes | "" | Comma-separated passwords, one per host. |
CLICKHOUSE_NAME | No | — | Comma-separated display labels shown in the host selector. |
CHM_ALLOW_PRIVATE_HOSTS | No | false | SSRF opt-in. When true, 1, yes, on, or cloud, self-hosted installs may connect to private, loopback, link-local, and Tailscale addresses. Cloud mode forces this off. |
CLICKHOUSE_MANAGEMENT_ENABLED | No | off | Gates CREATE USER, ALTER USER, DROP USER, GRANT, and REVOKE on /api/v1/management. Only the exact string true enables it. 1 stays off. |
CLICKHOUSE_HOST defines the host count. CLICKHOUSE_USER and
CLICKHOUSE_PASSWORD may be either a single value (applied to all hosts) or one
value per host position. CLICKHOUSE_NAME is optional. Position N maps to host
index N.
See Multiple hosts and Custom name.
Query execution
| Variable | Default | Purpose |
|---|---|---|
CLICKHOUSE_MAX_EXECUTION_TIME | 60 | Query timeout in seconds. |
CHM_TABLE_MAX_RESULT_ROWS | 10000 | Server-side row cap for table queries (max_result_rows). 0 disables the cap. A non-numeric value also disables it. |
CLICKHOUSE_DATABASE | system | Database used for app-owned tables (self-tracking events, dashboards). |
EVENTS_TABLE_NAME | system.monitoring_events | Full table name override for self-tracking events. |
Connection pool
| Variable | Default | Purpose |
|---|---|---|
CLICKHOUSE_POOL_SIZE | 10 | Max concurrent ClickHouse clients per host. |
CLICKHOUSE_POOL_TIMEOUT | 300000 | Idle client timeout in milliseconds (5 min). |
CLICKHOUSE_POOL_CLEANUP_INTERVAL | 60000 | Stale-client cleanup interval in milliseconds (1 min). |
Health alerting
The cron sweep (GET /api/cron/health-sweep, Cloudflare Cron Trigger every 5
minutes) runs health checks over all hosts and can post webhook alerts without a
browser tab open.
| Variable | Default | Purpose |
|---|---|---|
CRON_SECRET | — | Required shared secret guarding the cron endpoints /api/cron/health-sweep and /api/cron/retention-prune. Send as Authorization: Bearer <secret> only (?secret= is rejected). When unset/empty the endpoints fail closed and return HTTP 503 (they do not run) — retention-prune is destructive, so it must not be reachable unauthenticated. |
HEALTH_ALERT_ENABLED | on when any alert channel is configured | Optional master switch. Alerts dispatch as soon as a channel is configured (webhook URL, email, PagerDuty, and so on); set false (also 0, no, off) to pause all dispatch. Checks run regardless. Accepts true/1/yes/on and false/0/no/off, case-insensitive. |
HEALTH_ALERT_WEBHOOK_URL | — | Slack- or Discord-compatible webhook URL. Setting it is enough to enable webhook alerts. |
HEALTH_ALERT_MIN_SEVERITY | warning | Minimum severity to alert: warning (warning + critical) or critical (critical only). |
HEALTH_ALERT_WEBHOOK_ENABLED | on | Set false (also 0, no, off) to turn off the webhook channel only, including the HEALTH_ALERT_WEBHOOK_URL webhook. The master HEALTH_ALERT_ENABLED still controls the whole sweep. |
HEALTH_ALERT_WEBHOOK_MIN_SEVERITY | inherits HEALTH_ALERT_MIN_SEVERITY | Severity floor for the webhook channel only: warning or critical. Other values are ignored. |
HEALTH_ALERT_WEBHOOK_TARGETS | — | JSON array of extra webhook targets (up to 32). Each entry has a name, a urlEnv naming the env var that holds the URL, and optional headersEnv, enabled, format (auto, raw, slack, matrix, hookshot), minSeverity and templates. URLs and secret headers are read from the named env vars (headersEnv), never inlined; headers may hold non-secret X-* headers. Invalid JSON is ignored with no targets. |
CHM_HEALTH_CONFIG_DIRECTORY | /etc/chmonitor/health.d | Self-hosted only (needs a filesystem; a no-op on Cloudflare Workers). Directory of declarative alert definitions, one YAML file per concern: alerts.yaml, routing.yaml, channels.yaml, quiet-hours.yaml, maintenance.yaml, digest.yaml. Read once per process, so a changed file needs a restart. Bad files and entries are skipped with a warning, never fatal. Secret fields name an env var (secretEnv, urlEnv, headersEnv); secrets are never written in the files. Definitions merge D1/Postgres > file > env > default. Files, examples and merge rules: Run alerts without a metadata database. |
CHM_HEALTH_SWEEP_ENABLED | on when CRON_SECRET is set | Turns the scheduled health sweep on or off independently of the CRON_SECRET auth gate. A truthy value forces it on, a falsy value turns it off; unset or invalid falls back to on only when CRON_SECRET is configured. |
HEALTH_ALERT_COOLDOWN_MINUTES | 60 | Minutes the sweep waits before re-sending an alert for a condition that stays at the same severity. 0 disables reminders (alert once until it escalates or recovers). Invalid or negative values use the default. |
HEALTH_ALERT_DIGEST_MINUTES | 0 (off) | Digest window in minutes. A positive value buffers non-critical findings and sends them as one message when the window ends; critical findings always send immediately. The in-app digest setting overrides this value. |
HEALTH_ALERT_INSTANCE_NAME | — | Deployment label, e.g. prod-eu. Shown as a [prod-eu] prefix on every alert and digest, so alerts from several deployments sharing one channel can be told apart. Max 64 characters. |
HEALTH_ALERT_DASHBOARD_URL | — | Public base URL of this dashboard, e.g. https://chm.example.com. Alerts link to <url>/health?host=<id>. Only http/https URLs are accepted; a trailing slash is removed. |
HEALTH_ALERT_METADATA | — | Non-secret key/values added to alerts, as key=value,key2=value2 or a JSON object. Max 10 pairs; keys match [A-Za-z0-9_.-]{1,32}; values up to 128 characters with no line breaks. Invalid pairs are dropped. Custom webhook templates can use {{meta.<key>}}. |
HEALTH_HYSTERESIS_BREACHES | 1 | Consecutive sweeps a worsening must persist before it fires. Whole numbers >= 1; anything else uses the default. A per-rule override is HEALTH_HYSTERESIS_<RULE_ID>_BREACHES (rule id uppercased, dashes to underscores). |
HEALTH_HYSTERESIS_CLEARS | 2 | Consecutive ok sweeps a firing condition needs before it recovers. Same parsing as above; per-rule override HEALTH_HYSTERESIS_<RULE_ID>_CLEARS. |
# Alert Slack on warning-or-worse
CRON_SECRET=a-long-random-string
HEALTH_ALERT_WEBHOOK_URL=https://hooks.slack.com/services/T000/B000/XXXX
HEALTH_ALERT_MIN_SEVERITY=warningNo metadata database? Alert definitions (rules, routing, channels, webhook
targets, quiet hours, maintenance windows, digest) can be declared from
CHM_HEALTH_CONFIG_DIRECTORY and the HEALTH_* variables. Alert state,
ACKs and dismissals cannot: without D1 or Postgres they live in memory, reset on
restart, and ACK returns 501. See the
read-only/writable boundary.
Alert channels
Each channel is opt-in and only dispatches when its required variables are all set. Variables marked secret are server-side only. A channel saved from the alert settings UI overrides the matching variable here.
Every channel also takes a per-channel _ENABLED switch (false turns it off)
and a _MIN_SEVERITY floor (warning or critical, defaults to
HEALTH_ALERT_MIN_SEVERITY). The names are: HEALTH_ALERT_WEBHOOK_ENABLED, HEALTH_ALERT_WEBHOOK_MIN_SEVERITY, HEALTH_ALERT_EMAIL_ENABLED, HEALTH_ALERT_EMAIL_MIN_SEVERITY, HEALTH_ALERT_OPSGENIE_ENABLED, HEALTH_ALERT_OPSGENIE_MIN_SEVERITY, HEALTH_ALERT_PAGERDUTY_ENABLED, HEALTH_ALERT_PAGERDUTY_MIN_SEVERITY, HEALTH_ALERT_TELEGRAM_ENABLED, HEALTH_ALERT_TELEGRAM_MIN_SEVERITY, HEALTH_ALERT_NTFY_ENABLED, HEALTH_ALERT_NTFY_MIN_SEVERITY, HEALTH_ALERT_PUSHOVER_ENABLED, HEALTH_ALERT_PUSHOVER_MIN_SEVERITY.
| Variable | Default | Purpose |
|---|---|---|
HEALTH_ALERT_EMAIL_PROVIDER_URL | — | Secret. Email transport: mailgun://KEY@DOMAIN, sendgrid://KEY, smtp://user:pass@host:port or smtps://.... The credentials live in the URL. An unrecognised URL leaves email off. |
HEALTH_ALERT_EMAIL_FROM | — | From address. Required for email. |
HEALTH_ALERT_EMAIL_TO | — | Comma-separated recipients. Required for email. |
HEALTH_ALERT_EMAIL_ENABLED | on once the three above are set | Set false (also 0, no, off) to turn email off without removing the settings. |
HEALTH_ALERT_PAGERDUTY_ENABLED | on once a PagerDuty key is set | Set false (also 0, no, off) to turn PagerDuty off without removing its keys. |
HEALTH_ALERT_PAGERDUTY_MIN_SEVERITY | inherits HEALTH_ALERT_MIN_SEVERITY | Severity floor for PagerDuty only: warning or critical. |
HEALTH_ALERT_PAGERDUTY_ROUTING_KEY | — | Secret. PagerDuty Events API v2 routing key, used as the fallback target when no per-rule or per-host PagerDuty route matches. |
HEALTH_ALERT_PAGERDUTY_API_KEY | — | Secret. PagerDuty account REST API token. Used only to list services for the setup UI's picker; chmonitor never changes anything in PagerDuty with it. |
HEALTH_ALERT_OPSGENIE_API_KEY | — | Secret. Opsgenie API key. Setting it enables Opsgenie alerts. |
HEALTH_ALERT_OPSGENIE_REGION | us | Opsgenie region: us or eu. |
HEALTH_ALERT_TELEGRAM_BOT_TOKEN | — | Secret. Telegram bot token. Telegram needs both this and the chat id. |
HEALTH_ALERT_TELEGRAM_CHAT_ID | — | Telegram chat id to post to. |
HEALTH_ALERT_TELEGRAM_MIN_SEVERITY | inherits HEALTH_ALERT_MIN_SEVERITY | Severity floor for Telegram only: warning or critical. |
HEALTH_ALERT_NTFY_URL | — | Full ntfy topic URL, for example https://ntfy.sh/my-topic (a self-hosted server works). Setting it enables ntfy. |
HEALTH_ALERT_NTFY_TOKEN | — | Secret. Optional Bearer token for a protected ntfy topic. |
HEALTH_ALERT_PUSHOVER_TOKEN | — | Secret. Pushover application API token. Pushover needs both this and the user key. |
HEALTH_ALERT_PUSHOVER_USER | — | Pushover user or group key to notify. |
HEALTH_ALERT_TWILIO_ACCOUNT_SID | — | Twilio account SID. SMS needs the SID, auth token, from number and at least one recipient. |
HEALTH_ALERT_TWILIO_AUTH_TOKEN | — | Secret. Twilio auth token. |
HEALTH_ALERT_TWILIO_FROM | — | Twilio sender number. |
HEALTH_ALERT_TWILIO_TO | — | Comma-separated recipient numbers (E.164). One SMS is sent per number. |
HEALTH_ALERT_TWILIO_MIN_SEVERITY | critical | Severity floor for SMS: warning or critical. SMS costs money per message, so it keeps its own floor; set warning here to also page on warnings. |
HEALTH_ALERT_HEALTHCHECKS_URL | — | healthchecks.io ping URL pinged by the sweep. |
Inbound events
| Variable | Default | Purpose |
|---|---|---|
CHM_EVENTS_INGEST_TOKEN | — | Secret. Bearer token for POST /api/events/ingest (Alertmanager, Datadog and similar webhooks). When unset the endpoint fails closed with 503; a wrong or missing token gets 401. |
CHM_EVENTS_REEMIT_WEBHOOK_URL | — (off) | https:// URL that each ingested event is forwarded to as JSON. Best effort: URLs that fail the SSRF check and failed requests are skipped with a warning, and never break ingest. |
CHM_FEATURE_WEBHOOK_SUBSCRIPTIONS | mode default (true when CHM_DEPLOYMENT_MODE=cloud, else false) | Per-user webhook subscriptions. Also needs CHM_AUTH_PROVIDER=clerk with CLERK_SECRET_KEY and the CHM_CLOUD_D1 binding; without them the feature stays absent. |
Weekly health report
A second cron job (GET /api/cron/weekly-report) builds a proactive weekly
HTML report per opted-in host — a 7-day narrative from the AI insights engine,
adaptive statistical baselines, and the disk-capacity forecaster. Opt-in, never
opt-out: reports are generated only for hosts listed in
CHM_WEEKLY_REPORT_HOSTS.
| Variable | Default | Purpose |
|---|---|---|
CHM_WEEKLY_REPORT_HOSTS | unset (no reports) | Comma-separated host indices to opt in, e.g. 0,2. Uses the same CRON_SECRET guard as health-sweep. |
# Opt host 0 and host 2 into the Monday 08:00 UTC weekly report
CRON_SECRET=a-long-random-string
CHM_WEEKLY_REPORT_HOSTS=0,2Persisted regardless of delivery outcome; best-effort delivered to
HEALTH_ALERT_WEBHOOK_URL (above) when configured. See
Health — Weekly health report
for the report contents and how to view a persisted report.
Prometheus metrics exporter
GET /api/v1/metrics serves system.metrics + system.asynchronous_metrics
for every configured host (labeled host="<id>"), plus a
chmonitor_alerts_firing gauge from the in-memory alert-dedup state, as
Prometheus text exposition format. Cached ~30s; concurrent scrapes share one
query batch.
| Variable | Default | Purpose |
|---|---|---|
CHM_FEATURE_PROMETHEUS_ENABLED | mode (self-hosted→true, cloud→false) | Canonical, server-only (no client bundle exposure needed). Set explicitly to override either default. Never gated on Clerk/billing — disabled returns 404 rather than 403 so cloud never advertises the surface. |
# Scrape this instance from Prometheus
CHM_FEATURE_PROMETHEUS_ENABLED=true# prometheus.yml
scrape_configs:
- job_name: chmonitor
scrape_interval: 30s
metrics_path: /api/v1/metrics
static_configs:
- targets: ['dash.example.com']OTel trace export
Exports chmonitor's OWN query traces — a dashboard-request span per request,
with a nested clickhouse-query span per ClickHouse call carrying
query_id/read_bytes/host attributes when available — to an external OTLP
collector for correlation with the rest of your tracing stack. This is
DISTINCT from the built-in OTel span viewer, which reads spans already
stored in system.opentelemetry_span_log.
| Variable | Default | Purpose |
|---|---|---|
CHM_OTEL_EXPORTER_URL | unset (disabled) | Collector's OTLP/HTTP traces endpoint. Must be an absolute http(s) URL, including the /v1/traces path. Unset or invalid → trace export stays off: zero spans, zero network calls, zero added latency (fail-open). Server-only, never gated on Clerk/billing. |
# Export to a local Jaeger instance (OTLP/HTTP receiver)
CHM_OTEL_EXPORTER_URL=http://jaeger:4318/v1/tracesRuns on Cloudflare Workers: the exporter uses OTLP/HTTP (not gRPC, which
Workers cannot run), and the batch export is flushed inline before each
response returns rather than deferred with waitUntil (Workers request
middleware doesn't expose it here) — so export only adds latency when
explicitly enabled.
Feature permissions
All features are public and enabled by default. Override only what needs different behavior.
| Variable | Default | Purpose |
|---|---|---|
CHM_CONFIG_FILE | — | Path to a TOML (.toml) or YAML config file with a features table. Read once at server start (Node / Docker / K8s; ignored on Cloudflare Workers, which have no filesystem). Env vars below override it. Invalid entries are skipped with a warning. |
CHM_DISABLED_FEATURES | — | Comma-separated feature ids to disable entirely. |
CHM_AUTH_REQUIRED_FEATURES | — | Comma-separated feature ids that require authentication. |
CHM_FEATURE_{ID}_ACCESS | public | Per-feature access: public, guest (alias for public), or authenticated. |
CHM_FEATURE_{ID}_ENABLED | true | Per-feature enabled flag: true or false. |
Replace {ID} with an uppercase feature id: OVERVIEW, AGENT, INSIGHTS,
HEALTH, QUERIES, TABLES, METRICS, DASHBOARD, SECURITY, LOGS,
SETTINGS, BILLING, CLUSTER, OPERATIONS, ACTIONS, MCP, DOCS,
ABOUT, PEERDB.
CHM_FEATURE_AGENT_ACCESS=authenticated
CHM_FEATURE_METRICS_ENABLED=false
CHM_DISABLED_FEATURES=settings,insightsSee Feature permissions for config-file examples and precedence details.
Rate limiting
Per-IP (and, for the agent, per-signed-in-identity) request throttling on the
SQL-executing API surfaces. All four share one implementation
(lib/api/rate-limiter.ts, checkRateLimitDurable): an in-memory token
bucket everywhere, upgraded to a fleet-wide Cloudflare Rate Limiting binding
on Workers deploys (CHM_RATE_LIMIT_API / CHM_RATE_LIMIT_AGENT /
CHM_RATE_LIMIT_MCP / CHM_RATE_LIMIT_BROWSER_CONN in wrangler.toml) so the
limit holds across isolates, not just per-isolate. Requests over the limit get
429 with a Retry-After header. None of these need to be set for the
defaults to apply.
| Variable | Default | Purpose |
|---|---|---|
RATE_LIMIT_API_PER_MIN | 100 | GET data routes (e.g. /api/v1/charts/$name), per IP. |
RATE_LIMIT_AGENT_PER_MIN | 10 | POST /api/v1/agent, per IP then tightened per signed-in identity. |
RATE_LIMIT_AGENT_GUEST_PER_MIN | 5 | Cloud-only: additional per-guest identity bucket on POST /api/v1/agent (agent:guest:guest:<hash>). OSS anonymous stays IP-only. |
CHM_GUEST_AI_REQUESTS_PER_DAY | 3 | Cloud-only daily AI message cap for anonymous visitors. D1 ai_usage_daily per guest:<ip-hash>. OSS is not gated. |
RATE_LIMIT_MCP_PER_MIN | 30 | /api/mcp (POST/GET/DELETE), per IP. Same class of capability as the agent route — arbitrary read-only SQL via the query tool — over the MCP transport. |
RATE_LIMIT_BROWSER_CONN_PER_MIN | 10 | POST /api/v1/browser-connections/{test,sessions}, per IP. Both routes are unauthenticated and dial an attacker-supplied host on every request; each route gets its own bucket so a burst against one doesn't consume the other's budget. |
RATE_LIMIT_DEVICE_CODE_PER_MIN | 10 | Public CLI device-login code minting (/auth/device/code, /auth/token), per IP. Limits write load from unauthenticated callers. |
CHM_TRUST_PROXY_HEADERS | Cloudflare runtime only | When unset, X-Real-IP and X-Forwarded-For are trusted only on the Cloudflare runtime. true, 1, or cloud trust them everywhere. yes is not true here. false and 0 turn trusting off, including on Cloudflare. CF-Connecting-IP is always trusted. |
Authentication
The active server-side auth provider is set by CHM_AUTH_PROVIDER. See
Authentication for the full model.
| Variable | Default | Purpose |
|---|---|---|
CHM_AUTH_PROVIDER | mode default (none; clerk when CHM_DEPLOYMENT_MODE=cloud) | Server auth provider: none, clerk, proxy, or trusted. Canonical — the client VITE_AUTH_PROVIDER is derived from it at build time. Server precedence: runtime CHM_AUTH_PROVIDER → build-time VITE_AUTH_PROVIDER → runtime CHM_DEPLOYMENT_MODE default → build-time deployment-mode default. A provider set at build time is never lowered by a runtime mode. So a prebuilt image run with only CHM_DEPLOYMENT_MODE=cloud uses clerk. |
VITE_AUTH_PROVIDER | (from CHM_AUTH_PROVIDER; empty when unset) | Client-side build-time value. Auto-derived from CHM_AUTH_PROVIDER and baked only when set; when empty the client uses the baked deployment-mode default. Only set explicitly to override. |
CHM_API_KEY_SECRET | — | Shared secret for chm_ API keys. When set, API-key auth is always on alongside the active provider. Issue keys at /api/v1/auth/api-key. Required to enable CLI device login. |
CHM_DEVICE_LOGIN | auto | CLI device login (chm auth login / /device). auto = on in cloud when CHM_API_KEY_SECRET is set; off for self-hosted/OSS (typical internal network — mint a key once or leave the API open). true = force on (self-hosted opt-in; with CHM_AUTH_PROVIDER=none this is device-only approve — no Clerk, trust reachability of /device). false = force off. Persistence uses D1 when bound, otherwise an in-memory store (single-node). |
CHM_DEVICE_LOGIN_SUBJECT | self-hosted | Subject embedded in chm_ tokens minted via device-only approve (auth=none). Ignored when Clerk/proxy supplies the user id. |
CHM_MCP_PUBLIC | false | Open the /api/mcp endpoint with no auth. By default, when neither CHM_API_KEY_SECRET nor CLERK_SECRET_KEY is set the endpoint returns 401; set true only on a trusted private network. A warning is logged on every request while it is open. |
Keep secrets server-side
CLERK_SECRET_KEY, CHM_API_KEY_SECRET, and every trust-gate secret below are
runtime secrets. Never prefix them with VITE_ or NEXT_PUBLIC_, and never
expose them to the browser.
Clerk (CHM_AUTH_PROVIDER=clerk)
| Variable | Default | Purpose |
|---|---|---|
CHM_CLERK_PUBLISHABLE_KEY | — | Clerk publishable key (pk_..., public). Canonical — the client VITE_CLERK_PUBLISHABLE_KEY is derived from it at build time. |
VITE_CLERK_PUBLISHABLE_KEY | (from CHM_CLERK_PUBLISHABLE_KEY) | Client-side build-time value. Auto-derived; only set explicitly to override. Must be present before pnpm run build. |
CLERK_SECRET_KEY | — | Clerk secret key (sk_...). Runtime only, secret. Never expose to the browser. |
CLERK_PUBLISHABLE_KEY | — | Read only by the MCP server's OAuth metadata, as a fallback when NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is unset: the Clerk issuer is derived from the key. The dashboard itself uses CHM_CLERK_PUBLISHABLE_KEY. |
CLERK_OAUTH_ISSUER | derived from the publishable key | MCP OAuth issuer URL. Set it to override the derived value, for example behind a proxied Clerk domain. |
CLERK_API_URL | https://api.clerk.com | Clerk API base used to verify MCP OAuth access tokens. |
CHM_CLERK_PUBLIC_READ | mode default (false; true when CHM_DEPLOYMENT_MODE=cloud) | When on, anonymous visitors can view read-only monitoring content; writes (AI agent, control actions, arbitrary SQL) still require sign-in. Runtime only. See Clerk → public read-only mode. |
Reverse proxy (CHM_AUTH_PROVIDER=proxy)
Trust a reverse proxy that already authenticated the user. Either mechanism below can authenticate a request.
Cloudflare Access JWT:
| Variable | Default | Purpose |
|---|---|---|
CHM_CF_ACCESS_TEAM_DOMAIN | — | Cloudflare Access team URL (https://<team>.cloudflareaccess.com). Enables Cf-Access-Jwt-Assertion JWT verification. |
CHM_CF_ACCESS_AUD | — | Access application AUD tag the JWT must carry. |
Trusted header (bare subject):
| Variable | Default | Purpose |
|---|---|---|
CHM_PROXY_AUTH_SECRET | — | Shared secret. When set, the identity header is honored only if this secret is also present (constant-time compare). Without it, the trusted-header mechanism is disabled. |
CHM_PROXY_SHARED_SECRET_HEADER | X-Chm-Proxy-Secret | Header the proxy sends containing the shared secret. |
CHM_PROXY_AUTH_HEADER | X-Forwarded-User | Header the proxy sends containing the authenticated user identity. |
Trusted-header gate
When CHM_PROXY_AUTH_SECRET is set, the identity header (X-Forwarded-User by
default) is honored only when the request also carries a matching shared-secret
header (constant-time compared). Without CHM_PROXY_AUTH_SECRET, the entire
trusted-header mechanism is disabled and identity headers are ignored.
Trusted reverse proxy (CHM_AUTH_PROVIDER=trusted)
Use when an upstream proxy (oauth2-proxy, Authelia, Traefik ForwardAuth, nginx auth-url) forwards the user's full identity as HTTP headers. Extracts a complete principal — name, email, avatar, groups, custom claims.
Trust gate — configure exactly one:
| Variable | Default | Purpose |
|---|---|---|
CHM_TRUSTED_AUTH_SECRET | — | Shared secret (runtime secret). The proxy must send it in CHM_TRUSTED_SHARED_SECRET_HEADER. Constant-time compared. Recommended. |
CHM_TRUSTED_ALLOW_INSECURE | false | When true, trusts headers with no secret check. Only safe when the service is unreachable except via the proxy (e.g. k8s ClusterIP). |
CHM_TRUSTED_SHARED_SECRET_HEADER | X-Chm-Proxy-Secret | Header name carrying the shared secret. |
Identity headers — all optional; defaults match common proxy conventions:
| Variable | Default | Populates |
|---|---|---|
CHM_TRUSTED_USER_HEADER | X-Forwarded-User | Subject / user id. Falls back to the email header. |
CHM_TRUSTED_EMAIL_HEADER | X-Forwarded-Email | Email address. |
CHM_TRUSTED_NAME_HEADER | X-Forwarded-Preferred-Username | Display name. |
CHM_TRUSTED_AVATAR_HEADER | X-Forwarded-Avatar | Avatar URL. |
CHM_TRUSTED_GROUPS_HEADER | X-Forwarded-Groups | Comma- or space-separated groups. |
CHM_TRUSTED_ROLE_HEADER | X-Forwarded-Role | Single role; merged into the groups list. |
CHM_TRUSTED_CUSTOM_HEADERS | — | Extra claims. Comma-separated field:Header-Name pairs, e.g. team:X-Forwarded-Team. |
Access control:
| Variable | Default | Purpose |
|---|---|---|
CHM_TRUSTED_ALLOWED_GROUPS | — | Comma-separated group names. When set, users whose forwarded groups do not intersect are denied 403. Case-insensitive. |
See Trusted proxy for setup examples (oauth2-proxy + Dex + Traefik, nginx).
AI agent / LLM provider
The agent uses an OpenAI-compatible API. Set LLM_API_KEY to enable it. Keep all
LLM keys server-side — never prefix them with VITE_.
| Variable | Default | Purpose |
|---|---|---|
LLM_API_KEY | — | Provider API key (fallback when no provider-specific key is set). |
LLM_API_BASE | https://openrouter.ai/api/v1 | OpenAI-compatible API base URL. |
LLM_MODEL | openrouter:openrouter/free | Model identifier in provider:modelId form. |
LLM_EXTRA_MODELS | — | Additional model picker entries. Format: provider:modelId[|contextLength][|description], comma-separated. Providers: openrouter, nvidia, anyrouter. |
Provider-specific keys
Provider-specific keys take priority over the generic LLM_API_KEY /
LLM_API_BASE.
| Variable | Purpose |
|---|---|
OPENROUTER_API_KEY | OpenRouter API key. |
OPENROUTER_API_BASE | OpenRouter base URL. |
OPENROUTER_REFERER | HTTP referer sent to OpenRouter (for rankings). |
OPENROUTER_APP_NAME | App name sent to OpenRouter (for rankings). |
OPENROUTER_MODELS_API | OpenRouter models list endpoint (default https://openrouter.ai/api/v1/models). |
NVIDIA_API_KEY | NVIDIA NIM API key. |
NVIDIA_API_BASE | NVIDIA NIM base URL. |
ANYROUTER_API_KEY | AnyRouter API key. |
ANYROUTER_API_BASE | AnyRouter base URL. |
ANYROUTER_PRESETS / ANYROUTER_PRESETS_MAX | Surface workspace presets in the model picker (on by default, max 8). |
ANYROUTER_OAUTH_CLIENT_ID | Pre-registered OAuth client for "Sign in with AnyRouter" (optional). |
OPENROUTER_DYNAMIC_MODELS / OPENROUTER_TOP_MODELS_N | Live OpenRouter catalog enrichment (on by default, top 12). |
OPENROUTER_FREE_FALLBACK_MODEL | Concrete model that openrouter/free resolves to (default google/gemma-4-31b-it:free). Override it if the default is delisted upstream. |
NVIDIA_DYNAMIC_MODELS / NVIDIA_TOP_MODELS_N | Live NVIDIA NIM catalog enrichment (on by default, top 12). |
ANYROUTER_DYNAMIC_MODELS / ANYROUTER_TOP_MODELS_N / ANYROUTER_METRICS_CANDIDATE_CAP | Live AnyRouter catalog enrichment (on by default, needs the AnyRouter key; set the first to false, 0, no or off to disable). Top 8 (max 32) and metrics candidate cap 24 (max 64). |
Agent behavior
| Variable | Default | Purpose |
|---|---|---|
AGENT_API_TOKEN | — | Shared Bearer token for the agent API (POST /api/v1/agent). Accepted when CHM_FEATURE_AGENT_ACCESS=authenticated. |
AGENT_ENABLE_CONTROL_TOOLS | on for self-hosted, off in cloud | Gives the agent kill_query, optimize_table, and kill_mutation. Unset: on in self-hosted when auth is none or the caller is signed in, off for anonymous Clerk visitors and in cloud mode. false turns them off everywhere; true turns them on, but anonymous cloud visitors never get them. The actions feature permission still applies. |
CHM_AGENT_FIRECRAWL_MCP | true | Connect Firecrawl's hosted MCP (no API key) on every agent request. Set false to disable. |
CHM_AGENT_FIRECRAWL_ALLOW_DOMAINS | (empty) | Comma-separated hostnames Firecrawl may scrape/crawl/map. Empty / unset = unrestricted. Apex tokens also match subdomains; *.example.com is subdomain-only (not the apex). Search-only calls (no URL) stay allowed. |
AGENT_DEBUG | false | Verbose agent request and usage logging (message keys, resolved user ids, usage). Accepts 1, true, yes, on. Off unless set explicitly, whatever NODE_ENV is. |
AGENT_JSON_RENDER_PATCH_GUARD_DEBUG | — | Set 1 to log each rendered-spec chunk the patch guard drops. Ignored when NODE_ENV=production. |
Attribution headers
Sent to the LLM gateway so usage is attributed to this app. All are optional.
| Variable | Default | Purpose |
|---|---|---|
APP_REFERER | OPENROUTER_REFERER, else https://chmonitor.dev | HTTP-Referer header. |
APP_NAME | OPENROUTER_APP_NAME, else chmonitor | App title header. |
APP_SOURCE | chmonitor | App identifier AnyRouter groups usage by. |
APP_CATEGORY | programming-app | Marketplace category header. |
APP_VERSION | 0.2.0 | App version sent with the attribution metadata. On Cloudflare Workers it also overrides the ui version returned by GET /api/version. |
See AI agent — configuration for full details.
Conversation store
Agent conversations default to browser localStorage. Enable server-side persistence with two steps:
Enable the feature (build-time)
Set CHM_FEATURE_CONVERSATION_DB=true (the client VITE_FEATURE_CONVERSATION_DB
is derived from it at build time — must be present before pnpm run build). Also
requires Clerk auth (CHM_AUTH_PROVIDER=clerk).
Choose a backend (runtime)
Configure a backend at runtime via CONVERSATION_STORE_BACKEND.
| Variable | Default | Purpose |
|---|---|---|
CHM_FEATURE_CONVERSATION_DB | false | Canonical. Set true to enable server-side conversation persistence. Requires Clerk. Drives the build-time VITE_FEATURE_CONVERSATION_DB. |
VITE_FEATURE_CONVERSATION_DB | (from CHM_FEATURE_CONVERSATION_DB) | Client-side build-time value. Auto-derived; only set explicitly to override. Must be present before pnpm run build. |
CONVERSATION_STORE_BACKEND | (auto) | Runtime. Backend: agentstate, d1, postgres, or memory. When unset, the server auto-selects the first available backend (AgentState if key present → D1 binding → Postgres via DATABASE_URL → Memory). |
AgentState backend:
| Variable | Default | Purpose |
|---|---|---|
AGENTSTATE_API_KEY | — | AgentState API key. Required when CONVERSATION_STORE_BACKEND=agentstate. |
AGENTSTATE_BASE_URL | https://agentstate.app/api | AgentState API base URL. |
AGENTSTATE_AI_ENRICH | false (cloud production: true) | Set true to enable AI titles and follow-up suggestions. |
Cloudflare D1 backend:
| Variable | Default | Purpose |
|---|---|---|
CHM_CLOUD_D1_DATABASE_ID | — | Cloudflare D1 database UUID for the CHM_CLOUD_D1 binding. |
AGENT_CHM_CLOUD_D1_DATABASE_ID | — | Alias for CHM_CLOUD_D1_DATABASE_ID. |
PostgreSQL backend:
| Variable | Purpose |
|---|---|
DATABASE_URL | PostgreSQL connection string. |
POSTGRES_URL | Alternative PostgreSQL connection string. |
POSTGRES_PRISMA_URL | Prisma-specific PostgreSQL connection string. |
The same URL also backs the alert settings stores (routing, channels, custom
rules, webhook targets, acks, quiet hours, maintenance windows, digests,
alert state and history) when no D1 binding is present. Their tables are created
on first use (CREATE TABLE IF NOT EXISTS); there is no migration step.
See Conversation history — backends for setup and fallback behavior.
ClickHouse state backend (self-hosted):
Persists dashboards and per-user connections in your own ClickHouse instead of
Cloudflare D1 or Postgres. Opt-in; tables are created lazily
(CREATE TABLE IF NOT EXISTS, ReplacingMergeTree). Resolution order per
store: D1 binding → ClickHouse state env → Postgres env → local/memory.
Alert settings have no ClickHouse store: they use D1, then Postgres, and are
unavailable on a ClickHouse-only state backend.
| Variable | Default | Purpose |
|---|---|---|
CHM_STATE_CLICKHOUSE_URL | — | ClickHouse HTTP(S) URL for state storage. Setting it enables the backend. |
CHM_STATE_CLICKHOUSE_USER | default | ClickHouse user (needs CREATE/INSERT/SELECT/DELETE on the state database). |
CHM_STATE_CLICKHOUSE_PASSWORD | (empty) | ClickHouse password (secret). |
CHM_STATE_CLICKHOUSE_DATABASE | chmonitor | Database holding the state tables. |
CHM_STATE_CLICKHOUSE_TABLE_PREFIX | chm_state_ | Prefix for the state tables (chm_state_dashboards, chm_state_user_connections). |
See Self-hosted state backends for setup.
User connections (server storage)
Let signed-in users save personal ClickHouse hosts to the server. All four of these must be present together, or the server returns "User connections database storage is not enabled".
| Variable | Default | Purpose |
|---|---|---|
CHM_FEATURE_USER_CONNECTIONS_DB | false | Canonical. Set true to enable. Drives the build-time VITE_FEATURE_USER_CONNECTIONS_DB. |
VITE_FEATURE_USER_CONNECTIONS_DB | (from CHM_FEATURE_USER_CONNECTIONS_DB) | Client-side build-time value. Auto-derived; only set explicitly to override. |
CHM_AUTH_PROVIDER | none | Must be clerk — connections are keyed by the Clerk userId. |
CHM_USER_CONNECTIONS_ENCRYPTION_KEY | — | 32-byte base64 key (secret) encrypting saved credentials at rest (AES-256-GCM). |
Plus a database backend: the CHM_CLOUD_D1 binding on Cloudflare, or
DATABASE_URL / POSTGRES_URL on Docker / Kubernetes.
See User connections for the full setup.
AI insights store
The AI Insights panel on /overview persists its findings through a pluggable
store. It is additive opt-in and defaults to ClickHouse; the D1, Postgres, and
AgentState backends reuse the conversation-store env above.
| Variable | Default | Purpose |
|---|---|---|
INSIGHTS_STORE_BACKEND | auto | Backend: auto, clickhouse, d1, postgres, agentstate, or memory. auto and clickhouse both use the ClickHouse monitoring_findings table. auto never silently follows other env; a selected backend missing its prerequisite falls back to ClickHouse. |
INSIGHTS_D1 | — | Optional dedicated D1 binding for the d1 backend. Falls back to CHM_CLOUD_D1 when unset. |
postgres reuses DATABASE_URL; agentstate reuses AGENTSTATE_API_KEY (+
optional AGENTSTATE_BASE_URL).
See AI agent — AI Insights persistence
for setup and fallback behavior. GET /api/v1/insights/backend reports the active
backend.
PeerDB monitoring
Optional. Set PEERDB_API_URL to enable the PeerDB section (Mirrors and Peers) in
the sidebar.
| Variable | Default | Purpose |
|---|---|---|
PEERDB_API_URL | — | PeerDB REST API base URL. For the PeerDB UI (NextAuth), include /api suffix — e.g. https://peerdb.example.com/api. For the raw flow-api, use the bare origin — e.g. http://localhost:8113. |
PEERDB_PASSWORD | — | PeerDB password. Sent as HTTP Basic with an empty username. Server-side only. |
PEERDB_CACHE_TTL_MS | 10000 | Server-side response cache TTL in milliseconds. Set 0 to disable. |
PEERDB_CACHE_MAX_ENTRIES | 500 | Max cached PeerDB responses before oldest are evicted. |
PEERDB_FETCH_TIMEOUT_MS | 10000 | Upstream PeerDB request timeout in milliseconds. |
PEERDB_SWEEP_CONCURRENCY | 8 | Max in-flight PeerDB reads per alert-sweep tick. |
PEERDB_SWEEP_BUDGET_MS | 60000 | Wall-clock budget for one alert-sweep collection in milliseconds. |
PEERDB_SWEEP_MAX_MIRRORS | unset (no limit) | Optional cap on mirrors read per alert-sweep tick. Unset means every listed mirror is read; any truncation is recorded as a partial result. |
CHM_FEATURE_PEERDB_ENABLED | unset (enabled) | Set false to disable PeerDB entirely: hides the section, blocks the proxy, and skips the PeerDB alert cycle in the health sweep. |
CHM_FEATURE_PEERDB_AGENT | unset (auto) | The AI agent's read-only PeerDB tools. Unset: on when PEERDB_API_URL is set and PeerDB answers a quick probe. true: on whenever PEERDB_API_URL is set. false: off. Always off for anonymous cloud visitors; signed-in cloud users and requests on a user's own connection need an explicit true. CHM_FEATURE_PEERDB_ENABLED=false also turns it off. |
CHM_AGENT_INTEGRATION_PROBE_TIMEOUT_MS | 1500 | How long the agent waits for the PeerDB / Postgres reachability probe before treating the integration as unreachable for that conversation. Can lower the default, never raise it. The probe keeps running in the background and fills the cache for the next conversation. |
CHM_AGENT_INTEGRATION_PROBE_TTL_MS | 60000 | How long a successful probe result is reused per server instance. Failed results are reused for at most 15 seconds. |
chmonitor proxies only a read-only allowlist of PeerDB endpoints; mutating
operations (create/drop/pause, alert config) are rejected with 403.
# PeerDB UI behind NextAuth
PEERDB_API_URL=https://peerdb.example.com/api
PEERDB_PASSWORD=your-peerdb-ui-password
# Raw flow-api (no auth)
PEERDB_API_URL=http://localhost:8113Postgres source
Optional read-only Postgres monitoring source, off by default. Each list is
comma-separated and aligned by position with POSTGRES_HOST, so a single value
for POSTGRES_USER or POSTGRES_PASSWORD applies to every host.
| Variable | Default | Purpose |
|---|---|---|
CHM_FEATURE_POSTGRES_SOURCE | unset | Turn on the Postgres source engine and its API routes (true; the UI and routes stay off when unset). For the agent's read-only cross-source tools it is three-state: unset turns them on when a POSTGRES_HOST is set and answers SELECT 1 within the probe budget; true turns them on whenever a host is set; false turns them off. Always off for anonymous cloud visitors; signed-in cloud users and requests on a user's own connection need an explicit true. |
POSTGRES_HOST | — | Host list, each host or host:port. Defines which sources exist; with none set the list is empty. |
POSTGRES_PORT | 5432 | Port list. A port inside a POSTGRES_HOST entry wins; a missing entry falls back to the first value. |
POSTGRES_USER | postgres | User list. |
POSTGRES_PASSWORD | — | Secret. Password list. |
POSTGRES_DATABASE | postgres | Database list. A missing entry falls back to the first value. |
POSTGRES_SSLMODE | require | libpq sslmode list: disable, require, verify-ca, verify-full. prefer and allow behave like require. require encrypts without verifying the certificate. |
POSTGRES_NAME | — | Display name list. |
Slack app
Optional native Slack app (OAuth install, /chmonitor slash command, Home tab,
ACK buttons). With any of the first three variables missing the feature is off
and its routes return "not configured". Set the secrets as Worker secrets or in
.env.local, never in committed files.
| Variable | Default | Purpose |
|---|---|---|
SLACK_CLIENT_ID | — | Slack app OAuth client id (public). |
SLACK_CLIENT_SECRET | — | Secret. OAuth client secret, used to exchange the install code for a token. |
SLACK_SIGNING_SECRET | — | Secret. Verifies the signature of every inbound Slack request. Also the key source for token encryption when no dedicated key is set. |
SLACK_OAUTH_REDIRECT_URL | request origin + /api/v1/slack/oauth | OAuth redirect URI. Set it to match the URL registered in the Slack app exactly. |
SLACK_TOKEN_ENCRYPTION_KEY | derived from SLACK_SIGNING_SECRET | Secret. Optional dedicated 32-byte base64 key for encrypting stored bot tokens, so it can rotate separately from the signing secret. Any other length is rejected. |
GitHub deployment webhook
| Variable | Default | Purpose |
|---|---|---|
GITHUB_WEBHOOK_SECRET | — | Secret. Signing secret for inbound GitHub deployment and deployment_status webhooks. One global secret. When unset the route returns 501 and ingests nothing; a missing or wrong signature gets 401. |
Analytics & branding
Set the canonical CHM_* names. Client VITE_* is derived at build time.
| Variable | Default | Purpose |
|---|---|---|
CHM_ANALYTICS_KEY | — | PostHog project key for opt-in Product Analytics. Empty = disabled. |
CHM_ANALYTICS_HOST | https://us.i.posthog.com | PostHog ingest host. |
Build-time client override (optional):
| Variable | Default | Purpose |
|---|---|---|
VITE_AUTOCOMPLETE_LIMIT | — | Max rows the SQL autocomplete loader requests. Set at build time. |
CLI client
Env vars for the standalone chm / chmonitor CLI
(crate chmonitor). These are read by the binary and scripts/install.sh,
not by the dashboard server. Flags override env; env overrides
./chm.toml / ~/.config/chm/config.toml. Default API base is
https://dash.chmonitor.dev.
chm auth login discovers how to authenticate by probing public
GET /api/v1/auth/cli (method: none | api_key | device) — there is no
auth_mode in config. See API keys — device login.
| Variable | Default | Purpose |
|---|---|---|
CHM_BASE_URL | https://dash.chmonitor.dev | Dashboard API origin for hosts / chart / table / tui / auth / … |
CHM_CHANNEL | stable | Release channel for install.sh and chm update: stable or beta. Also --channel / config channel. chm update --beta installs from beta and writes channel = "beta" to the user config (--stable writes stable). |
CHM_API_KEY | — | chm_ API key (also --api-key). Used when discovery returns api_key, or alongside device login. |
CHM_TOKEN | — | Bearer token from device login (also --token). Usually stored in the OS keyring. |
CHM_HOST_ID | 0 | Host index for dashboard API calls (?host=). |
CHM_CONFIG | — | Path to a config TOML (overrides the default search paths). |
CHM_NO_UPDATE_CHECK | — | Set to 1 to silence the post-command “update available” hint. |
CHM_TELEMETRY | on | Same as dashboard: off / 0 / false / no disables CLI telemetry. |
CHM_TELEMETRY_ENDPOINT | https://telemetry.chmonitor.dev/v1/ping | Empty string = hard kill-switch. |
DO_NOT_TRACK | — | Cross-tool hard override (1) disables CLI telemetry. |
Installer (scripts/install.sh)
| Variable | Default | Purpose |
|---|---|---|
CHM_CHANNEL | stable | stable skips prereleases; beta prefers chm-v*-beta.N. |
CHM_VERSION | latest for channel | Pin a release tag (chm-vX.Y.Z, vX.Y.Z, or X.Y.Z). |
CHM_INSTALL_DIR | $HOME/.local/bin | Install directory (never uses sudo). |
Server-side device login gates (CHM_DEVICE_LOGIN, CHM_API_KEY_SECRET, …)
live under Authentication.
Product telemetry
Anonymous product telemetry is on by default. See Product telemetry.
| Variable | Default | Purpose |
|---|---|---|
CHM_TELEMETRY | on | Set to off (also 0 / false / no) to disable. DO_NOT_TRACK=1 is a hard override. |
CHM_TELEMETRY_ENDPOINT | https://telemetry.chmonitor.dev/v1/ping | Collection endpoint. Empty string = hard kill-switch (no network). |
CHM_LICENSE_KEY | — | Optional. Polar checkout id from a commercial license receipt (the same id lookup accepts). Included on the daily instance ping when telemetry is on. Does not unlock features or fail startup. Do not put a billing email here. |
Polar licenses (landing / cloud-hooks)
Self-host license checkout is not configured on the dashboard example.
Set Polar product ids and secrets on apps/cloud-hooks (and the hosted
.env.production catalog). See apps/cloud-hooks/.env.example.
Operators who already bought a license set CHM_LICENSE_KEY on the
dashboard process (see Product telemetry above) — not on cloud-hooks.
| Variable | Purpose |
|---|---|
CHM_POLAR_SERVER | sandbox or production. |
CHM_POLAR_LICENSE_* | License SKU product ids (team/unlimited × yearly/lifetime). |
POLAR_ACCESS_TOKEN | Org access token (secret). |
POLAR_WEBHOOK_SECRET | Webhook signing secret. |
Ops notifications worker (cloud-hooks)
apps/cloud-hooks is the Cloudflare Worker for Polar license checkout, Clerk
lifecycle hooks, health-probe digests, and exception/issue-watch notifications.
Bindings (CHM_CLOUD_D1, CHM_TELEMETRY_DB, CHM_HOOKS_KV) are declared in
apps/cloud-hooks/wrangler.toml, not here. See apps/cloud-hooks/.env.example.
| Variable | Purpose |
|---|---|
POLAR_WEBHOOK_SECRET | Polar webhook HMAC signing secret. |
POLAR_ACCESS_TOKEN | Polar org API token for checkout/re-key. |
CLERK_SECRET_KEY | Clerk backend API key (when Clerk hooks are enabled). |
CLERK_WEBHOOK_SECRET | Clerk webhook signing secret (whsec_…). |
TELEGRAM_BOT_TOKEN | Telegram bot token for ops digests/alerts. |
TELEGRAM_CHAT_ID | Destination chat/channel id for Telegram alerts. |
GITHUB_TOKEN | PAT with issues:write (fallback when GitHub App creds unset). |
GH_APP_ID | GitHub App id for exception/issue filing (preferred over PAT). |
GH_APP_PRIVATE_KEY | GitHub App PKCS#8 private key PEM. |
GH_APP_INSTALLATION_ID | Optional fixed installation id (else resolved from repo). |
CF_OBSERVABILITY_API_TOKEN | Cloudflare API token with Workers Observability read. |
CHM_POLAR_SERVER | sandbox or production Polar API host. |
CHM_LICENSE_SUCCESS_ORIGIN | Post-checkout redirect origin (default https://chmonitor.dev). |
CF_ACCOUNT_ID | Cloudflare account id for observability queries. |
GITHUB_REPOSITORY | owner/repo for filed issues (default chmonitor/chmonitor). |
CHM_EXCEPTION_ISSUE_LABELS | Comma-separated labels for exception issues. |
CHM_EXCEPTION_MAX_ISSUES_PER_RUN | Max new exception issues per scan (default 5). |
CHM_EXCEPTION_SCRIPTS | Comma-separated Worker script names to scan. |
CHM_ISSUE_WATCH_EXCLUDE_LABELS | Labels whose new issues the watch stays quiet about. |
CHM_ISSUE_WATCH_MAX_PER_RUN | Max new issues announced per ops sweep (default 10). |
CHM_POLAR_LICENSE_TEAM_YEARLY | Polar product id — team yearly license. |
CHM_POLAR_LICENSE_TEAM_LIFETIME | Polar product id — team lifetime license. |
CHM_POLAR_LICENSE_UNLIMITED_YEARLY | Polar product id — unlimited yearly license. |
CHM_POLAR_LICENSE_UNLIMITED_LIFETIME | Polar product id — unlimited lifetime license. |
Runtime & build
| Variable | Default | Purpose |
|---|---|---|
NODE_ENV | development | Runtime environment: development, production, test. |
ENABLE_CLOUDFLARE | false | Enable Cloudflare-specific build configuration. |
CLOUDFLARE_WORKERS | — | Set to 1 when running on Cloudflare Workers. |
CF_PAGES | — | Set automatically by Cloudflare Pages at runtime. |
MINIFLARE | — | Set to 1 when running locally with Miniflare. |
DOCS_CONTENT_ROOT | — | Override docs content source directory. |
Build-time only
Injected at build time for the About page.
| Variable | Purpose |
|---|---|
VITE_GIT_SHA | Current commit SHA. |
VITE_GIT_REF | Current git branch or tag. |
VITE_BUILD_TIMESTAMP | ISO build timestamp. |
VITE_CI | Set to true in CI environments. |
Query config source
| Variable | Default | Purpose |
|---|---|---|
CHM_CONFIG_SOURCE | ts | Query-config resolution path: ts (hand-written TypeScript configs, current default everywhere) or declarative (opt-in data-only catalog, validated against the same schema and kept at parity — see Catalog contributing). Any value other than the literal string declarative resolves to ts. A malformed declarative entry logs an error and falls back to the ts config rather than crashing. |
CHM_CONFIG_DIRECTORY | /etc/chmonitor/queries.d | Self-hosted only. Directory scanned once at startup for *.yaml local query-config overrides — no PR or fork needed. Each file must satisfy the same declarative schema as the catalog; an invalid file (bad YAML, schema violation, or duplicate name) is skipped with a warning log, never crashes boot. Checked first, ahead of CHM_CONFIG_SOURCE — a local file overrides a same-named built-in ts or declarative config regardless of that setting. Requires a real filesystem (Docker/K8s; not available on the Cloudflare Worker deploy). Mount example in the repo's docker-compose.yml. |
CHM_PACK_REGISTRY_URL | — | Comma-separated community query-pack URLs (HTTP(S) or file://), consulted when CHM_CONFIG_SOURCE=declarative. Each pack is a single YAML manifest ({ name, version, queries: [...] }) fetched once at startup (no hot-reload). HTTP(S) fetches are SSRF-guarded; file:// reads a local, operator-chosen path. Precedence (highest to lowest): the self-hosted local queries.d override > packs > the built-in declarative catalog > ts configs. Within packs, the last URL wins on a name collision (logged). An unreachable or malformed pack — or a single bad query inside an otherwise-valid pack — is skipped with a warning; the built-in catalog always still serves. |
Legacy / migration
| Variable | Notes |
|---|---|
NEXT_PUBLIC_AUTH_PROVIDER | Removed. Set CHM_AUTH_PROVIDER (renamed to VITE_AUTH_PROVIDER in v0.3; the old name is no longer read). |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | Removed. Set CHM_CLERK_PUBLISHABLE_KEY (the old name is no longer read). |
NEXT_PUBLIC_FEATURE_CONVERSATION_DB | Removed. Set CHM_FEATURE_CONVERSATION_DB (the old name is no longer read). |
NEXT_PUBLIC_AUTOCOMPLETE_LIMIT | Removed. Set VITE_AUTOCOMPLETE_LIMIT (the old name is no longer read). |
NEXT_PUBLIC_RUNNING_QUERIES_REFRESH_MS | Removed. Set VITE_RUNNING_QUERIES_REFRESH_MS (the old name is no longer read). |
CLICKHOUSE_EXCLUDE_USER_DEFAULT | Comma-separated usernames excluded from history-queries by default. |
See Migrate to v0.3 for the full rename list.
Related
Configuration
How the three sources layer and where to set each category.
Settings
In-app settings and their server-side counterparts.
Connection presets
Create a least-privilege read-only ClickHouse user.
Feature permissions
Config-file and env-override details.
Authentication
Choose and configure an auth provider.
Product Analytics
Opt-in, off-by-default PostHog funnel analytics.
Migrate to v0.3
Full rename list from the Next.js era.
Catalog contributing
The declarative query-config catalog CHM_CONFIG_SOURCE opts into.