Headroom for your Cloudflare flare.
A TanStack Start app, deployed on Cloudflare Workers, that tracks month-to-date usage of your metered Cloudflare products against a custom billing reset day, projects each to end-of-cycle on a simple run-rate, and shows it as a dashboard. A daily cron also snapshots usage into D1 so you can chart how the forecast evolves across the cycle.
It answers: "will any product cross its free tier before the next reset, and how much headroom is left?" — without needing realtime data.
- Framework: TanStack Start (file-based routing, server functions) on Cloudflare Workers, built with Vite +
@cloudflare/vite-plugin. - UI: TailwindCSS v4 + shadcn-style components built on Base UI (
@base-ui/react). Recharts for the detail charts, a dependency-free SVG sparkline on the cards. - State: Zustand for client UI state (basis toggle, group filter, the open metric).
- Data: D1 (SQLite) for daily snapshots, accessed with Drizzle ORM; schema in
src/db/schema.ts, migrations generated by drizzle-kit and applied viawrangler d1 migrations apply. Every Cloudflare API / D1 call happens in a TanStack Start server function (src/server/) — the browser only ever receives serialized results.
- The dashboard route loader calls the
getLiveUsageserver function. - Server-side, it computes the current cycle window from
RESET_DAY, builds one combined GraphQL query (one aliased block per metric in the catalogue) and posts it to the Cloudflare Analytics API. - For each metric it sums month-to-date usage, projects it as
mtd * (days_total / days_elapsed), and computes% of free tier+ estimated overage cost. - The client renders summary KPIs, per-product cards, and a detail dialog.
- A daily cron (
src/worker.ts→scheduled) and the "Collect now" button callcollectAndPersist, writing daily values + a projection snapshot to D1. The detail dialog charts that snapshot history.
src/
worker.ts Custom Worker entry: TanStack fetch handler + cron `scheduled`
router.tsx TanStack Start router
routes/
__root.tsx HTML shell (dark theme)
index.tsx Dashboard route + server-side loader
server/ SERVER-ONLY (imports `cloudflare:workers` env)
collector.ts GraphQL query + projection math + D1 reads/writes (Drizzle)
usage.ts createServerFn: getLiveUsage / collectNow / getMetricTrend
db/
schema.ts Drizzle table definitions (source of truth)
client.ts getDb() — Drizzle bound to env.DB
lib/
metrics.ts METRICS catalogue + cycle math + types (pure, shared)
utils.ts cn()
store/dashboard.ts Zustand UI store
components/
ui/ shadcn/ui components
dashboard/ SummaryGrid, UsageCard, MetricDetail, controls, sparkline
drizzle/ Generated SQL migrations (drizzle-kit)
npm install # .npmrc sets ignore-scripts=true — see note below
# 1. Create the D1 database and paste its id into wrangler.jsonc
wrangler d1 create prod-headflare
# 2. Apply the Drizzle migrations (local + remote).
# Migrations live in ./drizzle and are generated from src/db/schema.ts —
# after editing the schema, run `npm run db:generate` then migrate.
npm run db:migrate:local
npm run db:migrate:remote
# 3. Set RESET_DAY in wrangler.jsonc `vars` (the billing reset day)
# 4. Set per-deployment secrets so the repo stays account-agnostic.
# See docs/cloudflare-api-token.md for a step-by-step walkthrough.
wrangler secret put CF_ACCOUNT_ID # your 32-hex Cloudflare account id
wrangler secret put CF_API_TOKEN # API token, scope: Account Analytics -> Read
# 5. Regenerate binding types after editing wrangler.jsonc
npm run cf-typegenWhy
.npmrcsetsignore-scripts=true: a transitive dep ofminiflare(sharp, for local image-binding emulation we don't use) fails its native build on recent Node. Disabling lifecycle scripts still places every prebuilt binary the app needs (esbuild, workerd, oxide, lightningcss), so install/build just work.
npm run dev # http://localhost:3000
npm run build # vite build (client + Worker SSR bundle)
npm run deploy # build + wrangler deployEverything is config-driven — add a row per dashboard line:
{ key, label, group, node, metric, extraFilter?, included, unit, pricePerUnit? }node/metric— confirm viaintrospection.graphqlbefore trusting.included— the free-tier allowance.pricePerUnit— optional overage $/unit; drives the estimated cost.
Shipped pre-filled: Workers requests, D1 rows read/written, KV read/write/delete/list, DO compute requests. Stubbed (confirm names first): R2, Queues, Vectorize, Workers AI.
- Analytics ≠ billing. Cloudflare states these datasets are not the exact measure used for billing. The projection is a close operational estimate, not the invoice.
- Workers CPU-ms isn't cleanly summable from
workersInvocationsAdaptive, so it's omitted. - 31-day retention on the source datasets — the D1
usage_dailytable is your long-term record. - Rate limit ~300 queries / 5 min. One daily run is negligible.
headflare stores no secret or sensitive user data. Your CF_ACCOUNT_ID and
CF_API_TOKEN live only as Cloudflare Worker secrets (and .dev.vars, which is
gitignored) — never in the database, logs, or any committed file. The API token
is read-only (Account Analytics → Read) and is only ever used server-side; it
never reaches the browser. D1 stores only aggregate usage numbers and projection
snapshots per metric — no credentials, request contents, or personal data. See
SECURITY.md.
PRs welcome — see CONTRIBUTING.md for setup, the metric model, and the pre-PR checks. By participating you agree to the Code of Conduct. CI runs the type-check and build on every PR.
MIT © headflare contributors.