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_TOKENwith Workers deploy permissions (or runwrangler loginfor OAuth)
One-click deploy
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 installSet ClickHouse secrets
wrangler secret put CLICKHOUSE_HOST
wrangler secret put CLICKHOUSE_PASSWORDBuild and deploy
bun run cf:deploy # vite build → wrangler deployOpen 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 OKHow 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.
| Kind | Where to set | Who reads it | Example |
|---|---|---|---|
| Build-time client vars | CI env / shell before bun run build | Browser JS (VITE-inlined) | VITE_AUTH_PROVIDER, VITE_CLERK_PUBLISHABLE_KEY (derived from CHM_AUTH_PROVIDER / CHM_CLERK_PUBLISHABLE_KEY) |
| Runtime Worker vars | wrangler.toml [vars] or wrangler secret put | Worker process only | CLICKHOUSE_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_SECRETRedeploy 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_SECRETSet 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 keySet 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_SECRETWithout 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-conversationsAdd 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=trueRun migrations:
bun run cf:migrate-conversationsCloud-hosted:
[vars]
CONVERSATION_STORE_BACKEND = "agentstate"wrangler secret put AGENTSTATE_API_KEYAlso 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:deployThis 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:previewCloudflare bindings
The app uses these Cloudflare resources (configured in wrangler.toml):
| Binding | Type | Purpose |
|---|---|---|
CHM_CLOUD_D1 | D1 Database | Conversation history (optional) |
AGENT_CONVERSATIONS_DO | Durable Object | Conversation 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.