- 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.
- Is Watch just a ping?
- No. A simple ping asks whether a host answers. Watch sends an HTTP GET from Frankfurt every window, confirms a fail or a slow first byte from Dublin, and uses London only after a fail. It checks DNS, the status you expect, redirects that leave your hostname, optional page phrases, first byte and total load time, the leaf certificate, domain expiry, a daily sitemap of the first 40 same-host URLs, visitor 404 and 500 rates, an optional TCP port, and heartbeats for cron jobs. Slow mail waits for two confirmed windows. Down mail waits for two windows where Frankfurt and Dublin both failed. It does not run JavaScript or click through a checkout.