Cloudflare Workers
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.
One-click deploy
Section titled “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.
Prerequisites
Section titled “Prerequisites”- Cloudflare account
- Bun installed locally or in CI
CLOUDFLARE_API_TOKENwith Workers deploy permissions (or runwrangler loginfor OAuth)
Quick start
Section titled “Quick start”git clone https://github.com/duyet/clickhouse-monitoring.gitcd clickhouse-monitoring/apps/dashboardbun install
## Set credentials and buildexport CLICKHOUSE_HOST=https://clickhouse.example.com:8443
## Build the Cloudflare Workers bundle and deploybun run cf:deploy # vite build → wrangler deployFor secrets (ClickHouse password, LLM keys, etc.), use wrangler secret put:
wrangler secret put CLICKHOUSE_HOSTwrangler secret put CLICKHOUSE_PASSWORDOpen your Workers URL or set a custom domain in the Cloudflare dashboard.
How configuration works on Cloudflare
Section titled “How configuration works on Cloudflare”There are two kinds of variables:
| 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 |
| 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).
Runtime Worker vars (wrangler.toml [vars] and secrets) are never visible in the browser.
wrangler.toml
Section titled “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_HOSTwrangler secret put CLICKHOUSE_PASSWORDwrangler secret put CLERK_SECRET_KEYwrangler secret put CHM_PROXY_AUTH_SECRETwrangler secret put LLM_API_KEYwrangler secret put CRON_SECRETwrangler secret put CHM_API_KEY_SECRETAfter wrangler secret put, redeploy so the Worker picks up the change: bun run cf:deploy.
Configure
Section titled “Configure”ClickHouse connection
Section titled “ClickHouse connection”wrangler secret put CLICKHOUSE_HOST # https://clickhouse.example.com:8443wrangler secret put CLICKHOUSE_PASSWORD[vars]CLICKHOUSE_USER = "monitoring"CLICKHOUSE_NAME = "prod"Multiple hosts
Section titled “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:8443wrangler secret put CLICKHOUSE_PASSWORD # pass1,pass2[vars]CLICKHOUSE_USER = "monitoring,monitoring"CLICKHOUSE_NAME = "shard-1,shard-2"Query / pool tuning
Section titled “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
Section titled “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
Section titled “Authentication”None (default):
[vars]CHM_AUTH_PROVIDER = "none"API key layer:
wrangler secret put CHM_API_KEY_SECRETClerk:
Set at build time in CI (these are VITE_* client vars):
## In your CI environment / GitHub Actions secrets:VITE_AUTH_PROVIDER=clerkVITE_CLERK_PUBLISHABLE_KEY=pk_live_...Set the server secret at runtime:
wrangler secret put CLERK_SECRET_KEY # sk_live_...[vars]CHM_AUTH_PROVIDER = "clerk"Proxy — Cloudflare Access (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>"Proxy — trusted header:
[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
Section titled “AI agent”Set at runtime via secrets:
wrangler secret put LLM_API_KEYwrangler 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
Section titled “Conversation store”Server-side persistence requires VITE_FEATURE_CONVERSATION_DB=true set at build time (in CI before bun run build), plus Clerk auth. The backend is then selected at runtime.
D1 (Cloudflare-native, recommended):
Create a D1 database:
wrangler d1 create chmonitor-conversationsAdd the binding to wrangler.toml:
[[d1_databases]]binding = "CONVERSATIONS_D1"database_name = "chmonitor-conversations"database_id = "<database-id-from-above>"[vars]CONVERSATION_STORE_BACKEND = "d1"CONVERSATIONS_D1_DATABASE_ID = "<database-id>"In CI, also set VITE_FEATURE_CONVERSATION_DB=true before the build step:
## In your CI environment or GitHub Actions secrets:VITE_FEATURE_CONVERSATION_DB=trueRun migrations:
bun run cf:migrate-conversationsAgentState (cloud-hosted):
[vars]CONVERSATION_STORE_BACKEND = "agentstate"wrangler secret put AGENTSTATE_API_KEYPostgres: also available on Cloudflare Workers via outbound HTTP (DATABASE_URL runtime secret).
Health alerting — Cron Trigger
Section titled “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_SECRETwrangler secret put HEALTH_ALERT_WEBHOOK_URL[vars]HEALTH_ALERT_ENABLED = "true"HEALTH_ALERT_MIN_SEVERITY = "warning"Branding
Section titled “Branding”Set these in CI before running bun run build (they are baked into the JS bundle):
VITE_TITLE_SHORT=MyClusterVITE_LOGO=/logo.pngVITE_MEASUREMENT_ID=G-XXXXXXXXXXVITE_POSTHOG_KEY=phc_...Deploy
Section titled “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 build-time VITE_* vars (e.g. VITE_AUTH_PROVIDER, VITE_CLERK_PUBLISHABLE_KEY).
Preview locally
Section titled “Preview locally”bun run cf:previewCloudflare bindings
Section titled “Cloudflare bindings”The app uses these Cloudflare resources (configured in wrangler.toml):
| Binding | Type | Purpose |
|---|---|---|
CONVERSATIONS_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
Section titled “Upgrading”- Pull the latest code:
git pull. - Update dependencies:
bun install. - Rebuild and deploy:
bun run cf:deploy.
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
Section titled “Troubleshooting”wrangler deploy ships nothing or stale assets: run bun run build (or bun run cf:deploy, which does both) before wrangler deploy — the Vite build must produce the Workers bundle first.
Runtime errors / blank pages: check Workers logs in the Cloudflare dashboard. Verify secrets are set (wrangler secret list) and redeploy after any wrangler secret put.
Build-time client var not taking effect: VITE_* vars must be set in the environment when bun run build runs, not in wrangler.toml [vars].