> For the complete documentation index, see [llms.txt](https://docs.millimetric.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.millimetric.ai/core-concepts/privacy.md).

# Privacy & data retention

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

```http
POST /v1/forget
Authorization: Bearer sk_live_…
Content-Type: application/json

{ "user_id": "user_42" }
```

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:

```js
navigator.doNotTrack === "1"
navigator.globalPrivacyControl === true
```

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`).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.millimetric.ai/core-concepts/privacy.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
