Status: active
Analytics is split into two lanes that share no identifier and no join key. The separation is the load-bearing design decision in the whole subsystem.
ANONYMOUS LANE AUTHENTICATED LANE
analytics.events analytics.user_events
analytics.sessions
visitor_id = HMAC(key, ip:ua) user_id → auth.user (CASCADE)
keyed; key is rotatable stable, identified
no FK, no user reference no visitor_id column, ever
basis: Art 6(1)(f) + LIA basis: Art 6(1)(b) + 6(1)(f)
ePrivacy: contested (see below) ePrivacy: does not engage
consent tier gates collection no consent tier — Art 13 disclosure
retention 60 days retention 180 days, or immediate on erasure
public routes only /account/* only
NOT in collectUserData IS in collectUserData, section `behavior`
Why the wall exists
If a hashed visitor could be walked to a user id, the anonymous lane would stop being anonymous. Concretely, it would:
- become reachable by an Art 15 access request and an Art 17 erasure, which it currently is not and is not built to be;
- invalidate the necessity and balancing analysis in legitimate-interest.md, which is argued on the premise that the lane's subjects are unidentified;
- turn a short-lived pseudonym into a durable profile, which is precisely the characteristic that separates "counting visits" from "tracking people" in reasonable expectations. The visitor hash is bounded by the 60-day retention window and by a rotatable key, not by daily rotation — daily rotation would make "unique visitors this month" unanswerable, which is the question the lane exists to answer.
Adding a join key between these tables is therefore not a refactor — it is a change of
legal position. privacy/report.ts says so in prose; user-events.ts enforces it in
schema; the erasure test in privacy.test.ts pins the consequence.
Which lane claims a request
Decided in one place, analytics/collect-policy.ts, so the two collection paths cannot drift:
| Path | Lane | Note |
|---|---|---|
/, /blog/*, /showcases/* … |
anonymous | Public surfaces |
/account/* |
authenticated | Only when a session exists |
/admin/* |
neither | Operator's own usage — high volume, no insight |
/desk/* |
neither | Authenticated, excluded by decision |
/api/*, /_app/*, *.ico |
neither | Not pages |
| Bots, prefetch, prerender | neither | Not visitors |
A path is eligible for exactly one lane. Both the server hook and the SPA beacon endpoint
import the same predicates — they previously disagreed, and client-side navigations into
/admin and /account leaked into the anonymous lane as a result.
The ePrivacy question
ePrivacy Art 5(3) / TDDDG §25 gates access to terminal equipment, independently of whether GDPR is satisfied. Two elements matter:
The session cookie is unambiguous. It writes to the device and is not strictly necessary,
so it requires consent. Below the analytics tier it is never set, an existing one is
actively deleted, and session grouping falls back to hash(visitorId + UTC day) — the
Plausible/Fathom pattern, which stores nothing on the device.
The visitor hash itself is contested, and we do not claim otherwise. No primary source — EDPB, CNIL, DSK, or a court — resolves this exact pattern: no cookie, no added entropy, daily rotation, aggregate-only output. Two credible readings exist:
- Narrow: IP and User-Agent are unavoidable HTTP transport metadata. Nothing additional is read from or written to the device, so Art 5(3) does not engage and it is purely an Art 6 question.
- Broad: rooted in Art 29 WP Opinion 9/2014 on device fingerprinting and the technology-neutral framing of "access" in EDPB Guidelines 2/2023 — the User-Agent reports terminal configuration, and combining it with an IP for identification purposes is a form of access however it arrives.
We hold the narrow reading, with the mitigations that make it strongest: no added entropy, daily rotation, aggregate-only output, short retention, and a documented LIA. This is a risk-managed position, not legal certainty. Note also that Germany has no CNIL-style administrative exemption for consent-free audience measurement; the DSK/BfDI consent-management-service regime is a mechanism for capturing consent, not for dispensing with it.
The operative consequence is a rule: never add a signal to the visitor hash. Screen size,
timezone, canvas, fonts, hardwareConcurrency — each would convert an arguably-out-of-scope
technique into one that is confirmed in scope under Guidelines 2/2023, and would collapse the
narrow reading we rely on.
Why the authenticated lane needs no consent tier
For a logged-in user the ePrivacy gate does not engage: the auth cookie is already strictly necessary under TDDDG §25(2) Nr.2, so reading it requires no further permission. The processing rests on Art 6(1)(b) — operating the account the user asked for — and Art 6(1)(f) for improving it. This is disclosed under Art 13, not consented to under Art 6(1)(a), and the consent banner correctly says nothing about it.
The hard stop is Art 22: nothing derived from this lane may drive a solely automated decision producing legal or similarly significant effects for the user. Aggregate product analytics — cohorts, retention, funnels, which screens get used — is fine. Automated account restriction, materially consequential personalisation, or scoring is not, and would need explicit consent, contractual necessity, or a Member State law basis.
Related
- legitimate-interest.md — the Art 6(1)(f) assessment
- dpia-screening.md — Art 35 screening
- activation.md — collector wiring
- ../../stack/capabilities/gdpr.md — data-subject rights