Privacy & data retention
Exactly what gets stored, and what doesn't.
Millimetric is designed so that the data you don't intend to collect can't accidentally be collected. The defaults are restrictive; you opt in to anything beyond them.
What we never store
Raw IP addresses. IPs are HMAC'd with a salt that rotates daily; only the 16-byte hex of the hash is kept. The salt makes the hash effectively unique-per-day, so you can dedupe visitors within a day but cannot link them across days.
Cookies set by us. None. Ever. Anonymous IDs are generated client-side by the SDK and stored in
localStorage, only.Fingerprints. No canvas, fonts, WebGL probing, screen-fingerprint hashes. The browser SDK captures viewport, language, and timezone — none of which are persistent identifiers on their own.
Cross-site tracking. Every project is fully isolated. There is no shared identity across projects.
What gets stored on each event
project_id (your project)
timestamp (when the event happened)
event_name (e.g. "signup", "$pageview")
anonymous_id (caller-supplied UUID)
user_id (optional — only if you sent it)
session_id (auto-derived from anonymous_id + 30-min bucket)
source (classifier output — see Attribution)
medium (classifier output)
campaign (utm_campaign, if any)
source_confidence (low|medium|high)
source_rule_id (which rule fired)
referrer (Referer URL — only the originating page, not the chain)
url (the landing URL, including utm_*)
path (path portion of the URL)
country (from geo-IP — country code only, never city)
device_type (mobile | tablet | desktop)
browser (chrome | firefox | safari | edge | opera | "")
os (windows | macos | android | ios | linux | "")
ip_hash (HMAC(ip, IP_SALT + UTC_date) — un-reversible, rotates daily)
properties (JSON, capped at 8 KB)Retention
Every project has a retention_days setting (default 90, configurable per project). The /internal/retention/run job — wired to a Cloudflare Cron Trigger — issues per-project ALTER TABLE events DELETE to enforce it.
Materialised views (daily_rollup, sessions) retain only aggregated state without properties or ip_hash, so historical breakdowns remain available for analysis after raw events expire.
Right-to-be-forgotten
Issues an immediate parameterised ALTER TABLE events DELETE WHERE project_id = ? AND user_id = ? against ClickHouse. Mutations are async — they typically complete within seconds.
Only sk_* (secret) keys can call /v1/forget. pk_* keys are explicitly rejected so a leaked browser key cannot wipe data.
Properties payload limits
JSON-stringified size
8 KB
Cardinality (per key)
enforced upstream by your code — no server-side cap
If properties exceeds 8 KB after JSON.stringify, the value is replaced by {"__truncated":true,"original_bytes":N} rather than being silently truncated. We never partially commit a malformed payload.
DNT and Global Privacy Control
The browser SDK short-circuits the entire pipeline when either is set:
No events queued, no fetches sent, no localStorage writes. You can override with init({ ignoreOptOut: true }) if you have a separate legal basis.
The server endpoints do not auto-apply DNT — that would be the wrong layer (servers shouldn't second-guess their own integrations). DNT enforcement lives at the SDK / client edge.
What admins can see
Admins (any signed-in user from the admin UI) can see their own projects, keys, and event data.
Admins cannot see other users' projects — Supabase RLS restricts
projects.owner_id = auth.uid()and cascades toapi_keys.The Worker uses the publishable Supabase key, so even if its env were compromised, an attacker couldn't read arbitrary tables — only call the two
SECURITY DEFINERRPCs (verify_api_key,list_projects_retention).
Audit trail
api_keys.last_used_atis updated on every successful auth.Every event row stores
source_rule_idso future classifier changes can be back-traced.All read queries via MCP get logged with project + tool + arguments (see
apps/api/src/mcp/server.ts).
Last updated
Was this helpful?