chmonitor
Deployment

Cloudflare Workers

Deploy chmonitor to Cloudflare Workers for globally distributed, serverless hosting with D1 and Cron Trigger support.

Deploy chmonitor to Cloudflare Workers — best for globally cached, serverless hosting with no servers to manage.

The dashboard app (apps/dashboard) uses the @cloudflare/vite-plugin to build a native Cloudflare Workers bundle — no OpenNext adapter required. The deploy script is just bun run build && wrangler deploy.

Prerequisites

What you need

  • Cloudflare account
  • Bun installed locally or in CI
  • CLOUDFLARE_API_TOKEN with Workers deploy permissions (or run wrangler login for OAuth)

One-click deploy

Deploy to Cloudflare

This deploys the apps/dashboard worker (TanStack Start). You will be prompted to connect your Cloudflare account and configure variables. See the environment variable reference below and the dashboard README for the full variable list.

Setup

Clone and install

git clone https://github.com/chmonitor/chmonitor.git
cd clickhouse-monitoring/apps/dashboard
bun install

Set ClickHouse secrets

wrangler secret put CLICKHOUSE_HOST
wrangler secret put CLICKHOUSE_PASSWORD

Build and deploy

bun run cf:deploy   # vite build → wrangler deploy

Open your Worker URL

Open your Workers URL or set a custom domain in the Cloudflare dashboard.

Verify

Open your Workers URL. To confirm from the shell, hit the readiness endpoint:

curl -sf https://<your-worker-url>/api/healthz && echo OK

How configuration works on Cloudflare

Build-time vs runtime vars

There are two kinds of variables — mixing them up is the most common source of bugs.

KindWhere to setWho reads itExample
Build-time client varsCI env / shell before bun run buildBrowser JS (VITE-inlined)VITE_AUTH_PROVIDER, VITE_CLERK_PUBLISHABLE_KEY (derived from CHM_AUTH_PROVIDER / CHM_CLERK_PUBLISHABLE_KEY)
Runtime Worker varswrangler.toml [vars] or wrangler secret putWorker process onlyCLICKHOUSE_HOST, LLM_API_KEY, CLERK_SECRET_KEY

VITE_* vars are baked into the JS bundle at build time. Setting them in wrangler.toml [vars] has no effect — they must be in the environment when bun run build runs (e.g. CI secrets).

One canonical name

You usually don't set VITE_* directly. Set the canonical CHM_* name (e.g. CHM_AUTH_PROVIDER) in the build environment and vite.config.ts derives the matching VITE_*. A dual-surface setting must be present at build time (so the VITE_* inlines) and as a runtime [var] (so the server reads CHM_*) — same name, both places. See One canonical name per setting.

Runtime Worker vars (wrangler.toml [vars] and secrets) are never visible in the browser.

wrangler.toml

Non-secret runtime vars go in [vars]:

name = "chmonitor"
main = "@tanstack/react-start/server-entry"
compatibility_date = "2026-05-28"
compatibility_flags = ["nodejs_compat", "nodejs_compat_populate_process_env"]

[observability]
enabled = true

[vars]
CLICKHOUSE_USER = "monitoring"
CLICKHOUSE_NAME = "prod"
CLICKHOUSE_MAX_EXECUTION_TIME = "30"
CLICKHOUSE_TZ = "UTC"
CLICKHOUSE_POOL_SIZE = "10"
CLOUDFLARE_WORKERS = "1"
HEALTH_ALERT_ENABLED = "true"
HEALTH_ALERT_MIN_SEVERITY = "warning"
CONVERSATION_STORE_BACKEND = "d1"
CHM_AUTH_PROVIDER = "none"
LLM_API_BASE = "https://openrouter.ai/api/v1"
LLM_MODEL = "openrouter/free"
AGENT_ENABLE_CONTROL_TOOLS = "false"

Secrets go via wrangler secret put (never in wrangler.toml):

wrangler secret put CLICKHOUSE_HOST
wrangler secret put CLICKHOUSE_PASSWORD
wrangler secret put CLERK_SECRET_KEY
wrangler secret put CHM_PROXY_AUTH_SECRET
wrangler secret put LLM_API_KEY
wrangler secret put CRON_SECRET
wrangler secret put CHM_API_KEY_SECRET

Redeploy after secret changes

After wrangler secret put, redeploy so the Worker picks up the change: bun run cf:deploy.

Configure

ClickHouse connection

wrangler secret put CLICKHOUSE_HOST    # https://clickhouse.example.com:8443
wrangler secret put CLICKHOUSE_PASSWORD
[vars]
CLICKHOUSE_USER = "monitoring"
CLICKHOUSE_NAME = "prod"

Multiple hosts

CLICKHOUSE_HOST defines the host count. CLICKHOUSE_USER and CLICKHOUSE_PASSWORD may be a single value (applied to all hosts) or one value per host position. CLICKHOUSE_NAME is optional:

wrangler secret put CLICKHOUSE_HOST     # https://ch1:8443,https://ch2:8443
wrangler secret put CLICKHOUSE_PASSWORD # pass1,pass2
[vars]
CLICKHOUSE_USER = "monitoring,monitoring"
CLICKHOUSE_NAME = "shard-1,shard-2"

Query / pool tuning

[vars]
CLICKHOUSE_MAX_EXECUTION_TIME = "30"
CLICKHOUSE_TZ = "UTC"
CLICKHOUSE_DATABASE = "system"
CLICKHOUSE_POOL_SIZE = "10"
CLICKHOUSE_POOL_TIMEOUT = "300000"
CLICKHOUSE_POOL_CLEANUP_INTERVAL = "60000"

Feature permissions

Via wrangler.toml vars:

[vars]
CHM_DISABLED_FEATURES = "peerdb,actions"
CHM_AUTH_REQUIRED_FEATURES = "agent,settings,mcp"
CHM_FEATURE_AGENT_ACCESS = "authenticated"
CHM_FEATURE_SETTINGS_ENABLED = "false"

Via a mounted config file: not directly supported in Workers — use env vars instead.

Feature ids: overview, agent, insights, health, queries, tables, metrics, dashboard, security, logs, settings, cluster, operations, actions, mcp, docs, about.

Authentication

[vars]
CHM_AUTH_PROVIDER = "none"
wrangler secret put CHM_API_KEY_SECRET

Set the canonical names at build time in CI (the client VITE_AUTH_PROVIDER / VITE_CLERK_PUBLISHABLE_KEY derive from them):

CHM_AUTH_PROVIDER=clerk
CHM_CLERK_PUBLISHABLE_KEY=pk_live_...   # public publishable key

Set the server secret at runtime, plus CHM_AUTH_PROVIDER as a runtime [var] so the Worker reads it too:

wrangler secret put CLERK_SECRET_KEY   # sk_live_...
[vars]
CHM_AUTH_PROVIDER = "clerk"

Native option. Put chmonitor behind a Cloudflare Access application. The Worker verifies Cf-Access-Jwt-Assertion JWT from the Access JWKS.

[vars]
CHM_AUTH_PROVIDER = "proxy"
CHM_CF_ACCESS_TEAM_DOMAIN = "https://yourteam.cloudflareaccess.com"
CHM_CF_ACCESS_AUD = "<audience-tag>"
[vars]
CHM_AUTH_PROVIDER = "proxy"
CHM_PROXY_AUTH_HEADER = "X-Forwarded-User"
CHM_PROXY_SHARED_SECRET_HEADER = "X-Chm-Proxy-Secret"
wrangler secret put CHM_PROXY_AUTH_SECRET

Without CHM_PROXY_AUTH_SECRET, trusted-header auth is disabled.

AI agent

Set at runtime via secrets:

wrangler secret put LLM_API_KEY
wrangler secret put AGENT_API_TOKEN
[vars]
LLM_API_BASE = "https://openrouter.ai/api/v1"
LLM_MODEL = "openrouter/free"
AGENT_ENABLE_CONTROL_TOOLS = "false"

Never put LLM_API_KEY in a VITE_* var or [vars] — use wrangler secret put.

Conversation store

Server-side persistence requires CHM_FEATURE_CONVERSATION_DB=true set at build time (in CI before bun run build; the client VITE_FEATURE_CONVERSATION_DB derives from it), plus Clerk auth. The backend is then selected at runtime.

Cloudflare-native. Create a D1 database:

wrangler d1 create chmonitor-conversations

Add the binding to wrangler.toml:

[[d1_databases]]
binding = "CHM_CLOUD_D1"
database_name = "chmonitor-conversations"
database_id = "<database-id-from-above>"
[vars]
CONVERSATION_STORE_BACKEND = "d1"
CHM_CLOUD_D1_DATABASE_ID = "<database-id>"

In CI, also set CHM_FEATURE_CONVERSATION_DB=true before the build step (the client VITE_FEATURE_CONVERSATION_DB derives from it):

# In your CI environment or GitHub Actions secrets:
CHM_FEATURE_CONVERSATION_DB=true

Run migrations:

bun run cf:migrate-conversations

Cloud-hosted:

[vars]
CONVERSATION_STORE_BACKEND = "agentstate"
wrangler secret put AGENTSTATE_API_KEY

Also available on Cloudflare Workers via outbound HTTP (DATABASE_URL runtime secret).

Health alerting — Cron Trigger

Add a Cron Trigger in wrangler.toml:

[triggers]
crons = ["*/5 * * * *"]

The Worker calls GET /api/cron/health-sweep every 5 minutes. Protect the endpoint:

wrangler secret put CRON_SECRET
wrangler secret put HEALTH_ALERT_WEBHOOK_URL
[vars]
HEALTH_ALERT_ENABLED = "true"
HEALTH_ALERT_MIN_SEVERITY = "warning"

Branding

Set these in CI before running bun run build (they are baked into the JS bundle):

VITE_TITLE_SHORT=MyCluster
VITE_LOGO=/logo.png
VITE_MEASUREMENT_ID=G-XXXXXXXXXX
VITE_POSTHOG_KEY=phc_...

Deploy

bun run cf:deploy

This runs: vite build (produces the Cloudflare Workers bundle) → wrangler deploy.

CI (GitHub Actions): push to main triggers .github/workflows/cloudflare.yml. Set CLOUDFLARE_API_TOKEN as a repository secret, plus CLICKHOUSE_*, and any canonical build-time vars (e.g. CHM_AUTH_PROVIDER, CHM_CLERK_PUBLISHABLE_KEY — the client VITE_* derive from them).

This repo's hosted deploy uses .env.production

The chmonitor monorepo's own wrangler.toml declares no [vars]. Its non-secret hosted config lives in committed apps/dashboard/.env.production (+ .env.preview for PR previews), which feeds both the client build and the Worker runtime vars (injected by scripts/patch-wrangler-env.ts). If you fork this repo, edit .env.production rather than re-adding a [vars] block. When deploying your own standalone Worker from scratch, the [vars] examples above are the simplest path.

Preview locally

bun run cf:preview

Cloudflare bindings

The app uses these Cloudflare resources (configured in wrangler.toml):

BindingTypePurpose
CHM_CLOUD_D1D1 DatabaseConversation history (optional)
AGENT_CONVERSATIONS_DODurable ObjectConversation history via Durable Objects (optional)

The TanStack Start build via @cloudflare/vite-plugin does not require KV, R2, or cache-tag bindings. Only conversation-store bindings need to be added if you enable server-side persistence.

Upgrading

Worker secrets persist across deploys; you only need to re-run wrangler secret put when a value changes.

For breaking changes between major versions, see Migrating to v0.3.

Troubleshooting

On this page