Serverless, low-cost telemetry counter for projects, docs, and deploy workflows. Runs as a Cloudflare Worker on D1, with a Hono API and SVG badges. Free tier, no credit card.
Previously called CFlair-Counter. Same project, same API, same public URL (
https://counter.vkrishna04.me) - only the name changed. Existing badges and integrations keep working unchanged.
Teams need a dead-simple way to track project usage and show view badges across multiple repos without maintaining a custom backend.
Provide a fast, low-cost telemetry API that can be integrated in minutes and run reliably on a serverless platform.
This project implements:
- A Cloudflare Worker API for view tracking and stats.
- D1-backed persistence with a project cap and a daily write budget.
- SVG badge generation with style and color customization.
- Admin endpoints for protected project-level operations.
- CI health checks via Postman/Newman.
You get a production-ready counter service that is:
- Easy to integrate: one
POSTrequest to increment views. - Easy to show: one image URL for a live SVG badge.
- Cost-efficient: minimal infra overhead on Cloudflare.
- Automated: CI verifies endpoint behavior continuously.
git clone https://github.com/Life-Experimentalist/ViewFlare.git
cd ViewFlare
npm install
npm run setupnpm run setup checks your Cloudflare login, creates a D1 database, writes its
id into wrangler.toml, applies the schema, lists the nine settings and offers
to change any of them, prompts for an admin password through
wrangler secret put (it never sees the value itself), deploys, and prints your
URL. Answering nothing to the settings question keeps every shipped value, so
the whole thing is two Enters if you want the defaults. Every step is
idempotent, so re-run it if it stops partway.
Two commands rather than one because Windows PowerShell 5.1 has no &&.
The only prerequisite is a free Cloudflare account. If wrangler is not logged in
yet the script says so and stops; npx wrangler login fixes it.
A custom domain is optional and is the one step the script does not do: the
*.workers.dev URL works immediately. To use your own, once the zone is on the
same Cloudflare account:
npx wrangler deploy --domains counter.example.comThat creates the DNS record too, and later npm run deploy runs keep it
attached. Pass it as a flag rather than committing a routes entry, so a fork
never tries to claim your hostname.
Then day to day:
npm run dev # local instance on http://127.0.0.1:8788
npm run deploy # build, type-check and shipdocs/CLOUDFLARE-SETUP.md has the manual version of every step, and the
migration path if you deployed an older version on Cloudflare Pages.
If you put ViewFlare on a domain with Cloudflare's Bot Fight Mode on, requests
from datacenter IPs can be challenged, and a challenge page arrives at a caller
as HTML where it expected JSON. It is scored on IP reputation and request
signature, so it is intermittent rather than a flat block. That affects CI runners and server-side callers,
not browsers. The *.workers.dev hostname is not on your zone, so it is not
subject to it. Bot Fight Mode is zone-level and outside the Ruleset Engine, so a
WAF skip rule cannot exempt a path from it.
fetch("https://your-domain.com/api/views/my-project", {
method: "POST",
keepalive: true,
}).catch(() => {});- name: Increment docs counter
run: |
curl -fsS -X POST "https://your-domain.com/api/views/docs-build"| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/health |
GET | No | Health check |
/api/stats |
GET | No | Global stats across projects |
/api/views?names=a,b,c |
GET | No | Batch read, up to 50 projects, never increments |
/api/views/:project |
GET | No | Get one project's stats, ?rollup=1 to include everything under it |
/api/views/:project |
POST | No | Increment project views |
/api/views/:project/badge |
GET | No | SVG views badge |
/api/views/:project/history |
GET | No | Daily series from the nightly snapshot, ?series=breakdown for country and referrer |
/api/installs/:project |
GET | No | Aggregated install counts across registries |
/api/installs/:project/badge |
GET | No | SVG installs badge |
/api/installs/:project/shields.json |
GET | No | shields.io endpoint badge |
/api/installs/:project/history |
GET | No | Daily install series with change and per-day rate |
/api/events |
POST | No | Record a named event |
/api/metrics |
GET | No | All-time event rollup, top 100 pairs |
/api/compute/:project?expr= |
GET | No | One number from an expression over views, installs and events |
/api/compute/:project/badge?expr= |
GET | No | SVG badge of that number |
/api/compute/:project/shields.json?expr= |
GET | No | shields.io endpoint badge of that number |
/api/admin/stats |
POST | Password | Admin dashboard stats |
/api/admin/projects |
GET | Password | Admin project listing |
/api/admin/projects/:project |
PUT | Password | Rename or edit a project |
/api/views/:project |
DELETE | Password | Delete a project |
/api/admin/installs/:project |
PUT | Password | Configure install sources |
/api/admin/installs/snapshot |
POST | Password | Record today's snapshot |
INTEGRATION.md has the request and response shapes for the installs, history,
snapshot and compute endpoints.
Admin auth can be sent via:
X-Admin-PasswordheaderAuthorization: Bearer <password>header- JSON body
{ "password": "..." }(for supported POST endpoints)
GET /api/views/:projectName/badge
Query params:
style:flat|flat-square|for-the-badgecolor: named color (blue,brightgreen,orange, etc.) or hex (#00bcd4)label: custom left label (max 24 chars)
Examples:
/api/views/my-project/badge
/api/views/my-project/badge?style=flat-square&color=brightgreen
/api/views/my-project/badge?style=for-the-badge&color=%2300bcd4&label=downloads
GET /api/compute/:project?expr=... evaluates a small arithmetic expression
over the numbers already collected and answers with a single value.
curl "https://your-domain.com/api/compute/my-project?expr=views.total%2Binstalls.total"/api/compute/my-project/badge?expr=round(pct(installs.npm%2Cinstalls.total),1)&label=npm%20share
/api/compute/my-project/shields.json?expr=views.total%2Finstalls.total
- Variables:
views.total,installs.total,installs.<source>,events.<category>,events.<category>.<name>. - Operators
+ - * / %, parentheses, andmin,max,abs,round,floor,ceil,pct. - A
+in a URL decodes to a space, so write it as%2B. - If any input is unavailable the whole metric reports unavailable. It never substitutes a zero.
&rollup=1makes everyviews.*variable sum the whole dotted subtree.
A dot makes a project name a path. acme.api.docs sits under acme.api, which
sits under acme. Nothing about storage changes, the name is still one string
in one column, so every existing name keeps working untouched.
curl -X POST "https://your-domain.com/api/views/acme.api.docs"
curl "https://your-domain.com/api/views/acme?rollup=1"A rollup answer carries members, the per-project breakdown, alongside the sum.
Matching is on whole dotted segments, so acme_other is never counted under
acme. Rollup is off by default on every route that offers it.
INTEGRATION.md Goal 7 has the full contract.
ViewFlare is built so a coding agent can wire it up without being told the API.
Every deployment serves /llms.txt (the short version) and /openapi.yaml (all
of it), so the file you install into your own project is a short pointer rather
than a copy of the docs that goes stale.
Claude Code gets a plugin carrying two skills: viewflare-setup, which deploys
and upgrades your own instance, and viewflare-integration, the smaller one
that wires tracking into a project you already have.
/plugin marketplace add Life-Experimentalist/ViewFlare
/plugin install viewflare-integration@viewflare
Every other harness gets a copy-in rules file from integrations/agents/:
| Harness | Destination in your repo |
|---|---|
| Codex, Cursor, Windsurf, Antigravity | AGENTS.md |
| GitHub Copilot | .github/instructions/viewflare.instructions.md |
| Amazon Kiro | .kiro/steering/viewflare.md |
| Anything else with a fetch tool | no file, point it at /llms.txt |
AGENTS.md usually already exists, so append instead of overwriting:
curl -sL https://raw.githubusercontent.com/Life-Experimentalist/ViewFlare/main/integrations/agents/AGENTS.md >> AGENTS.mdintegrations/agents/README.md has the commands for the other two and explains
the frontmatter each one needs.
| Variable | Required | Default | Description |
|---|---|---|---|
ADMIN_PASSWORD |
Yes (for admin use) | empty | Set with npx wrangler secret put ADMIN_PASSWORD. It is a secret, never a [vars] entry |
ENABLE_ADMIN |
No | true |
Toggle admin APIs |
MAX_PROJECTS |
No | 1000 |
Most projects this instance will create. A new name is refused with a 409 at the cap; existing ones keep counting. 0 turns it off |
DAILY_WRITE_BUDGET |
No | 30000 |
Tracked requests allowed a day before new views get a 503 and a Retry-After. One recorded view is 2 D1 rows, 3 with TRACK_BREAKDOWN, against a free-tier 100,000 rows a day. Reads are never shed. Counts only while TRACK_USAGE is true. 0 turns it off |
RATE_LIMIT_REQUESTS |
No | 60 |
Requests per window |
RATE_LIMIT_WINDOW |
No | 60000 |
Rate-limit window in ms |
TRACK_USAGE |
No | false |
Write a daily row to usage_stats. Off because it costs one extra D1 write per view and duplicates the Cloudflare dashboard |
TRACK_BREAKDOWN |
No | false |
Record country and referring host per day in view_breakdown, read back with history?series=breakdown. Off because it is a second D1 write per view |
DEBUG |
No | false |
Verbose debug logging |
npm run setup # one-time: create the database and deploy
npm run dev # local worker on 127.0.0.1:8788
npm run build # bundle plus type-check
npm run deploy # build and ship
npm run type-check
npm run test:newmannpm run test:newman runs against base_url from the Postman environment,
which is a deployed instance. To point it at a local one, pass
--env-var "base_url=http://127.0.0.1:8788". CI does exactly that.
INTEGRATION.md- integration checklist and automation flow.docs/AI-AGENT-QUICKSTART.md- give this to coding agents.docs/DEVELOPMENT-GUIDE.md- development details.docs/CLOUDFLARE-SETUP.md- deployment, bindings, custom domain, and moving an older Pages deployment to Workers.docs/api/README.md- the OpenAPI spec, and how to generate a client from it.public/openapi.yaml- OpenAPI 3.1 for every endpoint, served athttps://your-domain.com/openapi.yaml.docs/postman-guide.md- Postman/Newman collection usage.docs/BRAND-PROMPTS.md- image-generation prompts for the logo and banner.skills/viewflare-setup/SKILL.mdandskills/viewflare-integration/SKILL.md- the two Claude Code skills, shipped together as the
viewflare-integrationplugin in theviewflaremarketplace. Install with the two commands under Install It Into Your Agent, or copy the directories into~/.claude/skills/.
- the two Claude Code skills, shipped together as the
integrations/agents/README.md- the same guidance as a copy-in rules file for Codex, Cursor, Windsurf, Antigravity, Copilot and Kiro.public/llms.txt- served athttps://your-domain.com/llms.txt, the short version of this API for an agent that lands on the deployed instance.
ADMIN_PASSWORDis a Workers secret, not a variable. It never belongs inwrangler.toml, and a secret cannot be read back once set, only replaced.- Rotate admin secrets if exposed.
- Restrict admin endpoint access at the edge where possible.
Apache-2.0. See LICENSE.md.