Skip to content
all writing
3 min readinfluxdbself-hostingprivacy

A cookieless page-view counter in one InfluxDB measurement

The footer view count runs on one InfluxDB measurement, a least-privilege database user, and no cookies. The interesting part is what it deliberately leaves out.

The footer of this site shows a small view counter. It works without cookies, fingerprinting, or a third-party service: one measurement in an InfluxDB instance that was already running for the live cluster status, one small API route, and a client beacon that fires a single POST per page view.

That is the whole system. The interesting part is what it deliberately leaves out.

One measurement, no timestamp

Every view is one point in a measurement called views:

text
views,path=/writing v=1i

The write carries no timestamp, and that is the load-bearing detail. InfluxDB overwrites points that share a measurement, tag set, and timestamp, so a counter that stamps its own writes at second resolution silently merges two hits that arrive in the same second. Leaving the timestamp off lets the database stamp each write at nanosecond resolution, and the collision window disappears.

The lifetime total is one query:

sql
SELECT count("v") FROM "views"

No sessions table, no aggregation jobs, no retention gymnastics. The default retention policy keeps everything, so the number in the footer is a true count since the counter went live.

A user that can only touch one database

The site writes with a dedicated Influx user whose entire grant is one database, the one holding the counter. It cannot read the cluster metrics, create databases, or touch anything else. If the credential ever leaked, the blast radius is vandalising a view counter.

The status widget on the home page follows the same rule with a read-only user pointed at the metrics database. Every credential the site holds is scoped to the one thing it does.

Off by default

When the Influx credentials are absent from the environment, every counter function no-ops: the beacon still fires, the API still answers, and nothing is recorded. Local development gets the honest version of the site without polluting the production count, and a broken counter can never take a page down with it. The live status widget degrades the same way when the database is unreachable. It says "unavailable" instead of inventing data.

Counting pages, not people

This week the counter learned which pages the views land on. The beacon now reports the route it is on, and the server normalises that against a short allow-list derived from the site's own route table: the home page, the project pages, and the writing section.

Anything that does not match lands in a single bucket called other. A scanner probing for /wp-admin cannot mint a new series in the database; it increments other and moves on. The private corners of the site fold into the same bucket on purpose, so they never show up in any future breakdown.

Nothing about the person is recorded. There is no IP address, no user agent, and no session identifier in the measurement. The tag says which page was viewed and nothing else, which is why a per-page breakdown and a views-per-day sparkline can be added later without a privacy conversation.

What it does not do

There are no unique-visitor counts, no sessions, no dwell time, no funnels, and no way to reconstruct anyone's visit. For a portfolio site those numbers would be decoration, and the price of collecting them is holding data about people who only came to read.

A counter that counts pages is enough. It answers the only question I had, which is whether anyone is reading, one page at a time.