For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

Limit
Value

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 to api_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 DEFINER RPCs (verify_api_key, list_projects_retention).

Audit trail

  • api_keys.last_used_at is updated on every successful auth.

  • Every event row stores source_rule_id so 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?