Build Fathom Analytics
YESreplaces $15/mosaves $180/yrback to the verdict
A web analytics service whose measurement approach is dictated by the conditions of an exemption: no storage on the device, no cross-site identity, no third-party sharing, and honest reporting of the uncertainty that those constraints introduce.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs ingestion, aggregation and reporting.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree locally
Why High-volume event ingestion and scheduled aggregation with reliable deletion.
Get it Docker or your package manager. open ↗
Verify
psql connects and a scheduled deletion runs correctly - decidefree to read; advice worth having if this is commercial
Why The exemption conditions become architecture in this build, and which apply to you depends on where your visitors are and which regulator you are answering to.
Get it Read the audience measurement guidance for the jurisdictions your visitors are in and write down the conditions you are relying on. open ↗
Verify
A written note lists the conditions and each maps to a constraint in the build - have readyfree
Why Blocking rates, referrer quality and the difference between your figures and another tool's only appear against real visitors.
Get it Your own site, running both this and an existing tool for a fortnight so the figures can be compared.
Verify
Two weeks of parallel measurement exist for comparison - installfree
Why The central claim is that nothing is written to the device, and that is verified by looking rather than by reading the code.
Get it Any browser with developer tools, plus a blocker installed to test the blocked case.
Verify
Storage is confirmed empty after browsing several pages
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- sites(id, hostname, timezone, created_at, retention_days) - events(id, site_id, at, path, referrer_host, country, device_class, visitor_hash) · deleted after a short window - visitor_hash · derived from a daily rotating salt, so it cannot link across days and is not reversible - daily_rollups(site_id, date, path, views, visitors, referrer_host, country, device_class) - salts(date, value, rotated_at, destroyed_at) · destroyed after the window, which is what makes the hash unrecoverable - goals(id, site_id, name, path_pattern, created_at) and goal_rollups(goal_id, date, count) - The daily salt is destroyed after rotation, which means yesterday's hashes cannot be recomputed even by you, and that is the property the whole model rests on.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://localhost:5432/analytics | Events briefly and aggregates long term. |
RAW_EVENT_RETENTION_HOURS | required | 48 | Short and stated. Cheap storage is how this becomes a behavioural log. |
SALT_ROTATION_HOURS | required | 24 | After rotation the previous salt is destroyed, making old hashes unrecoverable. |
THIRD_PARTY_REQUESTS | required | none | Present as a constant. The exemption conditions are cumulative. |
STORE_ON_DEVICE | required | never | Present as a constant. It is the thing that triggers the consent requirement. |
TRACKER_MAX_BYTES | required | 2048 | It runs on someone else's page. Size is a promise to them. |
The build, in order
Measuring without storing anything
A page view is recorded with nothing written to the visitor's device.
Verified in a real browser, not by reading the code.
Files
tracker/collect.tstests/no-storage.test.tsIt is on someone else's page and must not slow it down.
Files
tracker/A blocked tracker must not break a page.
Files
tracker/collect.ts
done when · tick each as it passeswatch out- A library that writes a single item to browser storage for deduplication reintroduces the consent trigger, and the whole reason for building this disappears for the sake of a small counting improvement. Assert in a real browser that nothing is written, rather than assuming your code does not.
Counting visitors without following them
A returning visitor within a day counts once, and cannot be linked to yesterday.
Retention makes hashes recomputable, which undoes the whole design.
Files
src/salts.tstests/salt-destruction.test.tsFine-grained fingerprinting is identification by another name.
Files
src/visitor.tsA report for a European site should not roll over at a server's midnight.
Files
src/days.ts
done when · tick each as it passeswatch out- Keeping the salt after rotation means the hashes remain recomputable and therefore linkable, which quietly turns a design that avoided identifiers into one that has them and simply does not use them yet. Destroy the salt, and verify it is gone rather than merely unused.
Aggregating and forgetting
Raw events disappear quickly and the reports survive.
Cheap storage is how a measurement tool becomes a behavioural log.
Files
src/rollup.tsTo the site owner, in the interface, not in a policy document.
Files
src/settings.tsA silently failed retention job is months of raw events.
Files
src/retention.ts
done when · tick each as it passeswatch out- Keeping raw events indefinitely because storage is cheap turns a measurement tool into a behavioural log, which is exactly the category the exemption does not cover, and nobody notices because the interface only shows aggregates. Delete on schedule and verify the deletion runs rather than trusting it.
Reporting the uncertainty honestly
Figures state their limitations rather than implying precision.
Site owners compare tools and conclude one is broken otherwise.
Files
src/report.tsFalse precision is a claim you cannot defend.
Files
src/report.tsCountry and device class, not city and model.
Files
src/dimensions.ts
done when · tick each as it passeswatch out- Presenting a visitor count as an exact figure implies a precision that a cookieless daily hash cannot provide, and site owners then compare it against another tool and conclude one is broken. State the counting method beside the number so the difference is explicable rather than alarming.
Goals, exports and staying inside the exemptionproduct builder
Useful features are added without breaking the conditions the design depends on.
A per-visitor path through a site is a behavioural record.
Files
src/goals.tsThe conditions are cumulative. One integration removes all of them.
Files
tests/no-third-party.test.tsSo the reason for each constraint survives the person who wrote it.
Files
docs/exemption-conditions.md
done when · tick each as it passeswatch out- Adding one integration that sends data to a third party removes the entire exemption, because the conditions are cumulative rather than a checklist to score well on. Assert the absence of third-party requests in the suite, so an integration cannot be added without the test failing.
That is the whole plan for Fathom Analytics. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Individual-level funnels, cohorts and attribution.
- Legal advice about your jurisdiction.
- Cross-site or cross-device measurement.
- Feature parity with tools that identify people.
- managed infrastructure
- privacy/compliance messaging
- email reports
- uptime
- support
- billing
- Uptime and page speed alongside traffic, which site owners want and which needs no identity
- An estimate of blocked traffic, which makes the comparison against other tools explicable
- Public dashboards, which are a natural fit when nothing personal is stored
Need the files? The project pack on the verdict page hands your agent the whole brief · more analytics.