- How do you count a unique visitor without cookies?
- HMAC-SHA256 of site, IP and user-agent, keyed with a server pepper and a random salt that exists only for that UTC day. We store 16 bytes. The IP is discarded. Tomorrow the salt is deleted, so the same person is a new digest.
- Why not just SHA-256(site + IP + UA + date)?
- That is deterministic. Anyone with edge IP logs and the formula can remake Monday’s hash. A deleted random salt plus a pepper that is not in the database cannot. HMAC is the keyed primitive; concatenation is not. This is the visitor-side moat — recipe on /methodology.
- How does the beacon leave the browser?
- POST JSON via navigator.sendBeacon. The payload is not in the URL, so CDN access logs do not see ?email= or ?token=. The shipped script does not GET. Collect is POST only — GET returns 204 and writes nothing.
- Which query parameters do you keep?
- utm_source, utm_medium, utm_campaign, utm_content, utm_term. The script allowlists them. The server allowlists them again. Everything else is dropped before it leaves the tab.
- Do you store the full referrer?
- No. Origin + path, no query. Source labels (Google, Hacker News, Direct) come from the hostname. Tokens in a referrer query never land on the row.
- Does it count client-side / SPA navigations?
- Yes. pushState, replaceState, popstate. The same path is not counted twice. Campaign tags stick to the first pageview in the tab — inner routes do not inherit them. After the landing, the referrer we send is the previous path on your host, so those hits are Direct, not another HN session.
- How small is the file with all of that?
- Under 1 KB gzipped, measured from /cf.js on this build. HMAC is server-side. POST, the allowlist, SPA hooks, and a prerender skip are in the script. Auto click-listeners are not.
- Do prefetch and bots inflate the count?
- Prerender/prefetch is dropped in the script (document.prerendering) and again if Sec-Purpose says prefetch. Library and crawler user-agents are dropped. Hostname must belong to the site, except on the public demo. Trailing slashes are folded so /pricing and /pricing/ are one path.
- Do you store city-level location?
- The edge already classified the request. We copy country, ISO 3166-2 region (as US-CA so California is not Canada), and a city name. We do not store IP, latitude, longitude, or postal code, and we do not run MaxMind or any other geo-IP database. If the host sent nothing, the fields are empty.
- What stops someone flooding /api/collect?
- Public ingest is not unforgeable. 60 beacons per hashed IP per minute — one flood, not a busy site. The counter lives in Postgres, so every Frankfurt isolate shares it. Over that IP quota we return 204 and write nothing. A popular site is not dropped. Raw IP is not stored on that row.