No description
Find a file
Ryan Grippeling 81d95e66e1
Some checks failed
[Workflow] On Source Change / Check Worker configuration (push) Successful in 29s
[Workflow] On Source Change / Deploy preview (push) Has been skipped
[Workflow] On Source Change / Deploy production (push) Failing after 3m43s
[Workflow] On Source Change / Deploy Production (push) Failing after 0s
chore(renovate): extend het org-preset op een gepinde versie
Het preset gaat zelf van een gepinde verwijzing uit: default.json draagt een
soak-uitzondering voor preset-updates (die alleen bestaat als er een versie te
bumpen valt) en een pinDigests: false waarvan de beschrijving zegt dat de
verwijzing 'is already pinned to a protected semver release tag'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 08:05:08 +02:00
.forgejo/workflows fix(ci): de deploy-baan bouwde met een script dat hier niet bestaat 2026-09-04 20:17:47 +02:00
.gitignore feat: Counterscale uitrollen vanuit Forgejo in plaats van vanaf een laptop 2026-09-04 16:17:46 +02:00
AGENTS.md docs: agent guide, met CLAUDE.md als symlink 2026-09-04 17:05:02 +02:00
CLAUDE.md docs: agent guide, met CLAUDE.md als symlink 2026-09-04 17:05:02 +02:00
CODEOWNERS feat: Counterscale uitrollen vanuit Forgejo in plaats van vanaf een laptop 2026-09-04 16:17:46 +02:00
package.json feat(deps): update pnpm ( 11.8.0 ➔ 11.11.0 ) [security] 2026-09-18 11:39:40 +00:00
pnpm-lock.yaml feat: Counterscale uitrollen vanuit Forgejo in plaats van vanaf een laptop 2026-09-04 16:17:46 +02:00
pnpm-workspace.yaml feat: Counterscale uitrollen vanuit Forgejo in plaats van vanaf een laptop 2026-09-04 16:17:46 +02:00
README.md docs: de R2-bucket voor de rollups komt uit webgrip/cloudflare 2026-09-05 06:31:53 +02:00
renovate.json chore(renovate): extend het org-preset op een gepinde versie 2026-09-22 08:05:08 +02:00
wrangler.toml fix(deploy): custom_domain, want de hostname had helemaal geen DNS-record 2026-09-04 23:12:54 +02:00

counterscale.webgrip.dev

Counterscale is the self-hosted analytics collector for Webgrip's static sites. This repository deploys it. There is no application code here on purpose: @counterscale/server ships the whole Worker, and what lives here is the configuration that is ours.

What reports into it

Site Reports as How
twente.dev twente-dev @webgrip/edge-analytics from its Worker
webgrip.nl webgrip-nl the same package

Reporting happens server-side, inside the request that serves the page, so there is no script in the page and nothing for a blocker to stop. The reasoning is in twente.dev's ADR 0013.

Why not npx @counterscale/cli

The upstream installer is interactive: it asks questions, writes a wrangler.json and shells out to wrangler deploy after a wrangler login. That puts the Cloudflare token on somebody's laptop and the deployed configuration nowhere.

Forgejo is the sole release authority here (twente.dev ADR 0003), so the config is checked in and the shared cloudflare-deploy.yml does the deploy with the token from the secret store. The installer remains a perfectly good way to try Counterscale; it is not how this one ships.

Retention

Analytics Engine keeps ninety days. Counterscale's nightly cron rolls each day into the counterscale-daily-rollups R2 bucket before that window closes, which is what makes a year of editions comparable rather than a rolling quarter.

That rollup is a cron that can stop without anyone noticing until the data is already gone. It deserves an alert, not trust.

Bindings and secrets

Set in wrangler.toml:

Binding Resource
WEB_COUNTER_AE Analytics Engine dataset metricsDataset
DAILY_ROLLUPS R2 bucket counterscale-daily-rollups
ASSETS Counterscale's own dashboard, out of the installed package

Set as Worker runtime secrets, never in this repo and never by hand:

Secret What it is for Where the value comes from
CF_BEARER_TOKEN Counterscale queries the Analytics Engine SQL API itself OpenBao secret/counterscale/worker, seeded once by a human
CF_ACCOUNT_ID same OpenBao secret/cloudflare/deploy, the account id's single record
CF_JWT_SECRET dashboard session signing generated in-cluster, never typed by anyone
CF_PASSWORD_HASH dashboard login (bcrypt) OpenBao secret/counterscale/worker, seeded once by a human

They reach the Worker through GitOps, not through a laptop. In homelab-cluster, kubernetes/apps/forgejo/forgejo-actions-secrets/app/ holds two ExternalSecrets and an hourly reconciler:

  • counterscale-worker pulls the account id and the two human-seeded values out of OpenBao.
  • counterscale-jwt mints CF_JWT_SECRET from the cluster's password generator and retains it. Regenerating it invalidates every live dashboard session, so it is generate-once by design.
  • counterscale-worker-secrets is a CronJob that proves CF_BEARER_TOKEN can actually answer a query on the Analytics Engine SQL API, and only then PUTs all four onto this Worker over Cloudflare's API. A credential that has gone dead fails that job, on the hour, instead of surfacing as a blank dashboard weeks later. It logs secret names and outcomes, never values.

CF_BEARER_TOKEN is a second Cloudflare token, not the deploy one: it needs Account Analytics:Read and nothing else. The deploy token (CLOUDFLARE_API_TOKEN, Workers Scripts:Edit) and CLOUDFLARE_ACCOUNT_ID live in the Forgejo secret store, are used by the deploy rather than by the Worker, and are what the reconciler uses as its transport.

The one human step is seeding the two values that originate outside the cluster:

bao kv put secret/counterscale/worker \
  CF_BEARER_TOKEN=<cloudflare token with Account Analytics:Read> \
  CF_PASSWORD_HASH=<bcrypt hash of the dashboard password>

Everything after that converges on its own within the hour.

Before the first deploy

  1. The R2 bucket counterscale-daily-rollups is managed in webgrip/cloudflare as cloudflare_r2_bucket.counterscale_daily_rollups; it exists before this Worker's first deploy, and its absence would show in that repo's nightly drift run.
  2. Seed secret/counterscale/worker in OpenBao with the bao kv put above. The other two Worker secrets need nobody: CF_ACCOUNT_ID is already in OpenBao and CF_JWT_SECRET is generated in-cluster. The reconciler publishes all four on its next tick.
  3. Point counterscale.webgrip.dev at Cloudflare so the route can bind.

workers_dev = false with a route that cannot bind is a green deploy and a dead hostname. The deploy workflow probes the apex afterwards for exactly that reason.

Working on it

pnpm install
pnpm run check     # bundles the Worker and prints every binding, touches nothing
pnpm run deploy    # only if you know why you are not letting CI do it
pnpm run tail      # live logs