Rails app
Wachturm
Web analytics without cookies, without banners and without personal data. Self-hosted, one SQLite file, runs on all my sites.
View- Status
- In use on my own sites, not public
- First commit
- 6 February 2026
- initial project plan, tasks, and ralph loop setup
- Stack
- Rails 8, SQLite, Chart.js, MaxMind GeoIP, Kamal
Why I built it
It was the early days, when agents suddenly made it easy to just rebuild software. I wanted my own analytics. Why? Why not.
Before that I ran two self-hosted tools side by side, GoatCounter and Bearlytics. Both did roughly what I wanted, neither did it exactly. I liked Bearlytics, so I took the idea and rebuilt it on my terms: Rails, as close to the defaults as possible, one SQLite file, almost no dependencies.
What it does
- A script of about 15 lines on your site sends the path and referrer to Wachturm and gets a 1x1 pixel back.
- It counts page views, visitors per day, referrers, countries, devices, browsers and language.
- No cookies, no local storage, no IP addresses in the database.
- One dashboard per site, and you can exclude your own IPs.
Technical decisions
Count visitors without recognizing them. A hash is built from IP, user agent, date and a secret salt, and the IP itself is never stored. Because the date is part of the hash, it changes every day. You see how many distinct visitors came today. You can’t track who was here yesterday. That’s not a flaw, that’s the point.
One extra gem. Besides Rails there’s exactly one dependency, the GeoIP library for countries. Rate limiting comes from Rails 8 itself. Fewer dependencies means less that can break in two years.
Boring deploys. Wachturm runs in my homelab. In September 2026 I moved it to a deliberately simple Kamal setup that I now use for all my Rails apps.
From the sessions
I built Wachturm on February 6, 2026, in about 90 minutes. From “let’s take a look” to live, with 152 passing tests and real traffic on my blog and the workshop site.
The app itself was only half of it. In the same session I reviewed every file, fully tore down GoatCounter and Bearlytics including DNS records and backup scripts, added Wachturm to the nightly backup and set up automatic deployments for the whole homelab. A year earlier that would have been a week of work. Here it was a long lunch break, because a skill already existed for every step.
Then in September, the reverse check: AskTNT wasn’t receiving any page views. I had excluded my own IP and couldn’t test it myself. The cause was the site’s own Content Security Policy, which blocked the tracking pixel. One line to allow it, a test that pins that rule down, and the check after 24 hours was clean.