Cookieless analytics, and why there's no consent banner
By EngineeringAlmost every analytics tool answers "how many people visited?" the same way: drop an identifier on the device, then count the distinct ones. That identifier is what turns a page view into personal data, and it's why you end up with a consent banner in front of your own product.
We wanted the numbers without the identifier. So Marsad never stores anything on the visitor's device — no cookie, no localStorage entry, no device id.
A visitor id that expires every day
When a page view arrives, the ingest endpoint derives a visitor id by hashing four things together with a secret salt that never leaves our server: the current day, the project id, the request IP, and the user-agent string.
createHmac("sha256", ANALYTICS_SALT)
.update(`${day}|${projectId}|${ip}|${ua}`)
.digest("hex")
.slice(0, 16);The raw IP is used for the hash and then dropped — it is never written to the event. Because the day is part of the input, the same person browsing tomorrow hashes to a completely different id, so ids can't be correlated across days. And because the salt is secret and the output is truncated, the hash can't be walked backwards to a person.
That's the whole mechanism. It's the same approach Plausible and Fathom take, and it has a real consequence worth being upfront about.
What this costs us
A rotating id means unique visitors are a daily figure. We cannot tell you that someone who came on Monday came back on Thursday, because by design we no longer have anything that links the two. Cross-day retention cohorts and multi-session funnels are off the table.
In exchange you get numbers you can publish without a legal review, and a dashboard nobody has to click through a banner to reach. For most teams shipping a product, "how much traffic, from where, to which pages, and did it convert today" is the actual question.
What we collect
- The path — never the query string, so tokens and email addresses in URLs can't leak into analytics.
- The referrer and page title.
- UTM parameters (source, medium, campaign, term, content), grouped into campaigns.
- Coarse buckets derived server-side from the user-agent: device type, browser, OS.
- Country, resolved at the edge. No city, no coordinates.
It's on by default in the browser and needs no extra code — the SDK records a view on load and on every route change. If you'd rather not have it, init({ webAnalytics: false }) turns the whole thing off.
Start watching your apps in minutes
Drop in the SDK and watch your first events arrive. Free plan, no credit card.
Start free