Today's hashing salt and yesterday's, cached in ETS.
A visitor is identified by a keyed hash of their user agent and IP. The key is a random salt that changes every UTC day and is deleted shortly after, so the identifier is unlinkable across days and cannot be recomputed later even with the raw inputs. That is what makes the scheme pseudonymous while it lives and anonymous once the salt is gone — and it is why pixelex needs no cookie, and therefore no banner.
Rotation without a scheduler
The salts table is keyed by UTC date. Rotation is
INSERT ... ON CONFLICT DO NOTHING for today: the first node to notice the
new day writes a salt, every other node reads it. No cron job to forget, no
leader election, and no window in which two nodes hash the same visitor
differently.
previous is not an optimisation
It is the whole reason this module has state at all. At 00:00 UTC the salt
changes, so every visitor's hash changes, so every open session looks like a
brand new person. Pixelex.Identity computes the id under both salts and
the session lookup tries both — which is the only thing standing between this
design and data that quietly corrupts itself one night at a time. Plausible
does the same; it is the subtlest part of the scheme and the easiest to omit
without noticing, because nothing fails, the numbers just get worse.
Refresh, rotation and cleanup are three different clocks
- refresh (
salt_refresh_ms, 90s) — reload ETS from the store. Only converges nodes after one of them rotated. Does not create anything. - rotation (daily, 00:00 UTC) — implied by the date key, above.
- cleanup (
salt_ttl_hours, 48h) — delete salts older than two rotations, sopreviousis always still there.
:memory mode
With salt_persistence: :memory nothing is written to the database. A
backup can then never be replayed to reconstruct browsing history, which is
the strictest posture available (Ackee's). The cost is that a restart severs
every open session, and separate nodes disagree about who a visitor is.
Summary
Functions
Returns a specification to start this module under a supervisor.
Delete salts older than salt_ttl_hours. Safe to call from a cron.
%{current: binary, previous: binary | nil}.
Force a reload. Tests, and anything that just rotated deliberately.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec cleanup() :: :ok
Delete salts older than salt_ttl_hours. Safe to call from a cron.
%{current: binary, previous: binary | nil}.
Reads straight from ETS — no GenServer call, because this runs on the path of every single event and a serialised lookup there would be the bottleneck of the whole system.
Force a reload. Tests, and anything that just rotated deliberately.