Build Fathom Analytics

YESreplaces $15/mosaves $180/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort multi-daythe files for this build are in the project pack

CollectionA small script sending an event with no device storageIdentityA rotating daily salted hash, never a persistent identifierAggregationCounts computed and stored, raw events discarded quicklyReportingFigures with their uncertainty statedRuntimeTypeScript with PostgreSQL

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs ingestion, aggregation and reporting.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/analyticsEvents briefly and aggregates long term.
RAW_EVENT_RETENTION_HOURSrequired48Short and stated. Cheap storage is how this becomes a behavioural log.
SALT_ROTATION_HOURSrequired24After rotation the previous salt is destroyed, making old hashes unrecoverable.
THIRD_PARTY_REQUESTSrequirednonePresent as a constant. The exemption conditions are cumulative.
STORE_ON_DEVICErequiredneverPresent as a constant. It is the thing that triggers the consent requirement.
TRACKER_MAX_BYTESrequired2048It runs on someone else's page. Size is a promise to them.

The build, in order

  1. Measuring without storing anything

    A page view is recorded with nothing written to the visitor's device.

    1. Verified in a real browser, not by reading the code.

      Files tracker/collect.tstests/no-storage.test.ts

    2. It is on someone else's page and must not slow it down.

      Files tracker/

    3. A blocked tracker must not break a page.

      Files tracker/collect.ts

    done when · tick each as it passes
    watch 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.
  2. Counting visitors without following them

    A returning visitor within a day counts once, and cannot be linked to yesterday.

    1. Retention makes hashes recomputable, which undoes the whole design.

      Files src/salts.tstests/salt-destruction.test.ts

    2. Fine-grained fingerprinting is identification by another name.

      Files src/visitor.ts

    3. A report for a European site should not roll over at a server's midnight.

      Files src/days.ts

    done when · tick each as it passes
    watch 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.
  3. Aggregating and forgetting

    Raw events disappear quickly and the reports survive.

    1. Cheap storage is how a measurement tool becomes a behavioural log.

      Files src/rollup.ts

    2. To the site owner, in the interface, not in a policy document.

      Files src/settings.ts

    3. A silently failed retention job is months of raw events.

      Files src/retention.ts

    done when · tick each as it passes
    watch 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.
  4. Reporting the uncertainty honestly

    Figures state their limitations rather than implying precision.

    1. Site owners compare tools and conclude one is broken otherwise.

      Files src/report.ts

    2. False precision is a claim you cannot defend.

      Files src/report.ts

    3. Country and device class, not city and model.

      Files src/dimensions.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more analytics.