Building the analytics tool I actually wanted to use

KJ's Counter started because I wanted analytics on KJ's Games without putting Google Analytics back into my life. The requirement sounded fairly small: count useful things without tracking people. It ended up becoming its own privacy-first analytics platform with a client library, serverless collection, automatic aggregation, country-level statistics and a live dashboard.

At a glance

Organisation
KJ's Counter
Period
2026 – Present
Category
Personal
Stack
TypeScript · React · Vite · DigitalOcean Functions · DigitalOcean App Platform · Turso · Cloudflare Workers

The problem

I wanted to answer fairly ordinary questions. Which games are people playing? Which pages get viewed? Are games being started and finished? Which countries are generating activity?

What I didn’t need was a visitor profile, advertising attribution, cookies, session replay or a tag-management system that needed its own afternoon just to configure an event.

The more I thought about it, the more appealing the simple version became: the application says what happened, Counter counts it, and that’s basically the contract.

Starting with the event

The client API is deliberately small. A site can report something like game.started with game: "solitaire", or game.ended with a result and duration. There is no separate step where that event has to be created in an analytics dashboard before Counter understands it.

Event namespaces provide the categories automatically, properties become useful filters and dimensions, and numeric values can become measures. The application reports the fact; Counter works out how to present it.

That was probably the main design rule throughout the project: don’t recreate the configuration work I was trying to get away from in the first place.

Privacy by leaving things out

I didn’t want “privacy-first” to mean collecting loads of information and then promising to be responsible with it. The more useful question was what Counter didn’t need to collect at all.

There are no analytics cookies, persistent visitor IDs, sessions, fingerprints or stored IP addresses. The collector validates incoming events and rejects obvious identifying properties. Location is deliberately coarse: Cloudflare derives a country code at the edge and Counter stores that against the event without keeping the address that produced it.

That means I can answer “how many Solitaire starts came from the UK?” but not “what else did this person do?”, because Counter doesn’t have a person to join those events back to.

Keeping raw data temporary

Recent anonymous events are useful because they allow detailed live views and give the system something to aggregate from, but I didn’t want that event-level history hanging around forever either.

Counter keeps raw events for a limited period and then compacts them into daily statistics. Counts, dimensions and numeric measures survive; the individual raw rows don’t.

The slightly unglamorous part here was making sure compaction was safe. If a job fails halfway through, it shouldn’t lose events. If it runs twice, it shouldn’t count everything twice. That meant transactions, bounded batches, retry behaviour and quite a few tests for scenarios I sincerely hope never become interesting in production.

Making it useful to look at

The dashboard started fairly conventionally and immediately became annoying to use. There were too many dropdowns leading to other views, which meant answering one question involved repeatedly selecting something, going somewhere else and selecting something again.

I changed it into more of a single analytics workspace. Site, time range, category, event, game, result, country and other dimensions can be filtered at the top, with the data underneath updating around them.

Recent data is live by default. The last hour uses minute-level buckets, the last 24 hours uses 15-minute buckets, and the dashboard refreshes automatically while it’s visible. Longer periods fall back to coarser aggregates, because I probably don’t need to know what happened at 14:17 three months ago.

The infrastructure

Counter is intentionally made from fairly small pieces. The dashboard is a static app on DigitalOcean. Collection and queries run through serverless functions. Turso stores the data. A small Cloudflare Worker sits in front of collection so country can be derived from trusted edge information without Counter storing visitor IPs.

Most importantly for a personal project, the whole thing is designed to stay comfortably inside free tiers at the scale I’m using it. I wanted useful infrastructure, not another collection of monthly subscriptions quietly breeding in the background.

What I learned

Counter has been a good example of a project where the constraints actually made the design better. Deciding that I didn’t want to identify visitors removed a surprising amount of complexity. Deciding that events shouldn’t need configuring separately kept the client simple. Deciding that it should run cheaply pushed the infrastructure towards small serverless pieces rather than a permanently running application.

It has also been a useful AI-assisted project. A lot of the implementation has been carried through by Codex, including infrastructure and production verification, but the interesting work has still been deciding what the system should and shouldn’t do, reviewing the choices it makes and tightening things when a technically valid solution doesn’t quite fit the product I want.

Where it is now

Counter is live on KJ’s Games and this site, so it has moved past being a demo built to prove itself. I’m now using the dashboard to understand real activity and letting that tell me what needs improving next.

There are plenty of things I could add, but I’m trying to resist turning it into Analytics Platform: The Enterprise Years. The useful version is still the one where a project can send a few meaningful events and immediately get useful information back.

View KJ’s Counter

  • Analytics
  • Privacy
  • Architecture
  • Deployment
  • AI-assisted implementation
  • QA

More case studies

Browse all
  • A content-driven rebuild of my personal site: a broader home for work, projects, games and notes, with predictable routing, safe staging behaviour and a cleaner path for future updates.

    Personal website · 2025 – 2026 Featured
    Read case study
  • KJ's Games started as a few browser-game experiments and gradually became somewhere I could test the whole process of building and improving a small product: game logic, interfaces, shared systems, deployment, analytics and the slightly awkward bit where real usage proves some of your assumptions wrong.

    Games site · 2026 – Present Featured
    Read case study
  • I've gone from occasionally using AI to having it involved in quite a lot of how I think about and build things. The interesting part hasn't been finding more things for AI to do. It's been working out which bits I can hand over, which bits I shouldn't, and how the whole workflow changes when implementation is no longer the main bottleneck.

    Personal workflow and tooling · 2025 – Present
    Read case study