Status: draft, pending legal review (input I10). Not legal advice. Last
updated 2026-11-13. Legal statements cite sources.md (S1 to S11); technical
statements point to the code or test that shows them. A statement that is
neither sourced nor tested does not belong here.
1. Roles
- Each tenant (an enrolled project) is the controller of the data of its website visitors and API clients.
- The service operator (today SCF, after handover the community operator)
is the tenant’s processor. It processes that data only to provide the
service, under the terms in
terms.mdand the processing terms indpa-template.md(GDPR Art. 28, S10). - SCF operators see aggregates only: the global views and the machine API
read
agg.*,api.metrics_hourly,api.metrics_dailyand counters, never raw events, sessions or Umami (plan 4.7 and 4.8; rolecp_operator, tested ininternal/db/boundaries_test.go). - Open question for counsel (plan phase 13): whether SCF’s evaluative use of cross-tenant aggregates is reuse “for the provider’s own account”. CNIL’s self-assessment tool lists that as excluded unless the data was effectively anonymised first (S2). The aggregates are counts per site and day, not rounded; whether they are anonymous in the CNIL sense is not settled here.
2. Data inventory per component
“Memory” means the data exists only in a process’s memory or in a NATS memory-backed store and never reaches a disk. “Queued” means a NATS JetStream file-backed stream. “Stored” means PostgreSQL.
No component stores an IP address. The privacy audit test (
tests/privacy/)
sends real web and API traffic from marker addresses on a dedicated Docker
network and scans both databases, every JetStream stream and KV bucket, every
service log, the Caddy logs and every container volume for them.
3. IP address and visitor token design
- The gateway reads the client address from the one header Caddy sets and keeps it in memory for the request (plan 4.4 step 7).
- Country: the /24 or /48 network is looked up in the local DB-IP Country Lite file. Only the ISO country code leaves the gateway. Region and city are never looked up.
- Visitor token:
vt = hex(HMAC-SHA256(daily_secret, site_id || 0x00 || ip || 0x00 || user_agent))(plan 4.4 step 8). The secret is 32 random bytes per UTC day, created atomically in the memory bucketedge_secretswith a TTL that ends at the next UTC midnight. No copy is written to disk. - The replayer sends Umami the constant address
192.0.2.1(TEST-NET-1) and passesvtas Umami’s client salt, so Umami’s session id depends on the token and never on a real address (plan 4.4 steps 12 and 13). - After midnight nobody can recompute a token of the previous day, so stored sessions cannot be linked back to an address and user agent. The CJEU held in C-413/23 P that pseudonymised data is personal data for whoever can re-identify it (S8). Applying that to a destroyed salt is our inference, recorded in S8 for counsel. During the day the token is computable by the gateways, which is why same-day erasure exists (section 6).
4. Retention per store
jobs retain-web deletes Umami website_event, event_data and the disabled
features’ per-event tables by created_at, then sessions that have no event
left and their session_data and session_link rows. It reads
_prisma_migrations first and refuses to run unless the database holds exactly
the 27 migrations of the pinned Umami 3.5.0 image with their checksums, and
the columns it uses exist (tested in internal/jobs/retainweb/retainweb_test.go).
The role jobs_retention has select and delete on those tables and select on
_prisma_migrations, nothing else (same test). It is not scheduled by the jobs
daemon; deployments run it once a day (phase 15 adds the timer).
Raw retention bounds what a tenant dashboard covers: with the default of 25
months, Umami shows 24 full months.
5. Umami features that are off, and how
Umami 3.5.0 has no global switch for these features (plan section 1, correction 8), so they are disabled by not shipping their client code and by blocking their routes:
The deny list lives in
deploy/caddy/web.Caddyfile and
docs/reference/umami-contract.md.
6. Objection and consent controls for tenants
-
Global Privacy Control. The tracker sends nothing when
navigator.globalPrivacyControlis true, and the gateway drops any event that arrives withSec-GPC: 1(internal/gateway/server.go). -
Opt-out flag. The tracker sends nothing when the browser’s
localStorageholdsumami.disabled. A tenant offers its visitors an opt-out link that sets this key. The tracker never writes browser storage. -
What the tracker reads from browser storage, and when. Exactly this,
from
packages/tracker/src/index.tsandwallets-kit.ts:- On every site: the value of
umami.disabled. In default mode it is read when the page loads and again before every event. In consent mode (data-auto-track="false") nothing is read until the site callsumami.start(), and then before every event. - Only on a site that turns on Wallets Kit capture with the script tag
(
data-wallets-kit="true"), and only once the tracker has started: every 500 ms, and on eachstorageevent raised by another tab of the site, the tracker lists the names of the page’slocalStoragekeys to find the kit’s two keys. It reads the value of@StellarWalletsKit/selectedModuleId(the wallet id, for examplexbull) and checks only whether@StellarWalletsKit/activeAddressexists; it never reads that value or the value of any other key. Nothing it reads leaves the browser except the wallet events ofdocs/onboarding/web/wallets-kit.md: the wallet id, never an address. “Started” means page load in default mode, and only when neither Global Privacy Control nor the opt-out flag is set; in consent mode it means the site’sumami.start()call. From tracker 1.2.0 that call starts the reading only when neither Global Privacy Control, the opt-out flag nor an honoured Do Not Track is set (1.1.0 started it regardless, although it sent no event); tested bynever reads the kit's storage when start() is called for a gpc visitor(packages/tracker/test/wallets-kit.test.ts). - With the npm adapter (
attachWalletsKit) the tracker reads no kit storage: it subscribes to the events of the kit the dapp set up itself. - Nothing else. The challenge and proof stay in page memory.
sources.mdS1), which is why Wallets Kit capture is off unless the tenant turns it on for its own site, and why a tenant that turns it on must include it in its own assessment. - On every site: the value of
-
Do Not Track. With
data-do-not-track="true"the tracker also honours the browser’s Do Not Track setting. -
Deferred start. With
data-auto-track="false"the tracker loads, sends nothing and fetches no challenge until the site callsumami.start(), for example after consent (docs/reference/wire-v1.md). -
Erasure (
erasure.md): one visitor’s visit of the current UTC day, one session at any time during raw retention, or a whole site.
7. Redaction and its limits
Before an event is queued, the gateway (internal/redact,
internal/gateway/sanitize.go):
- reduces the referrer to its scheme and host (
https://news.example/a?b#cbecomeshttps://news.example); a same-origin path becomes empty (SanitizeReferrer, tested ininternal/gateway/sanitize_test.goand end to end intests/gateway); - replaces all ten SEP-23 strkey types (G, M, C, W, L, B, T, X, P, S),
email addresses and federation addresses in the path, kept query values,
referrer host, title and event data; S secret seeds become
{redacted-secret}; - drops every query parameter of the page URL. Only on a site whose owner
turned on UTM collection (the site setting
collect_utm, off by default;docs/reference/control-plane-api.md3.1) does it keeputm_source,utm_medium,utm_campaign,utm_content,utm_termandref(CampaignParams, tested ininternal/gateway/sanitize_test.goand through the registry ininternal/gateway/utm_node_test.go); - drops URL fragments except a
#/hash route, without its own query.
site_reset remain available for the whole raw retention
period.
8. CNIL self-assessment criteria: what the code does
The primary text of CNIL’s July 2025 self-assessment tool, quoted in S2, says that the technical measures it names are recommendations: a provider may use an alternative if it shows the alternative meets the criterion. The conditions of the exemption are those of délibération 2020-091 art. 5 (S2).Decision on rounding to ten: documented limitation
Three options were weighed: a display rule for every tenant, a per-tenant “CNIL mode”, and a documented limitation. The service takes the third, for these reasons:- Where tenants see numbers. Every tenant web report is Umami’s own UI, read API and CSV export, run from the official image unmodified (plan section 1, plan 4.9). The console shows tenants no web counts. Rounding there would change nothing a tenant sees. Rounding in Umami would mean patching the image or rewriting every JSON and CSV answer of dozens of report routes in the web proxy, which the project ruled out by running Umami unmodified.
- Rounding alone would not meet the criterion. The criterion is that
every report is anonymous for any combination of filters, and the next
row asks that no single user’s navigation can be followed. Umami’s
Sessions and realtime pages show the pages of one visitor’s day to a
team-view-onlymember (docs/reference/umami-contract.md, item 10). A rounded overview next to those pages is still not anonymous output, so a “CNIL mode” that only rounded would invite a claim the system does not satisfy. Blocking those pages would also remove the session ids a tenant needs for session erasure (erasure.md). - No false claim. France therefore stays “not as built”
(
jurisdictions.md): tenants with French visitors use consent mode until counsel decides otherwise. No anonymity analysis has been made, and none is claimed.
9. Known gaps
- Exact counts and single-visitor views. Section 8: no rounding, and Umami’s Sessions page shows one visitor’s pages of a day.
- UTM parameters. Off by default since 2026-11-13: the gateway drops
them unless the site’s owner turns on
collect_utm(section 7), which moves the default to CNIL’s recommended measure. A site that turns it on no longer meets that measure; the console says so next to the switch. Sites that existed before the setting start with it off. - Erasure race. See
erasure.md, “Limits”. - NATS file blocks. An acknowledged
WEB_EVENTSmessage is removed from the stream, but its bytes can stay in a JetStream file block until the block is compacted or expires (at most 72 hours). Erasure deletesWEB_DLQmessages with secure delete, which overwrites them. - No production deployment yet. Phases 15 and 16 build the hosting and
backups.
data-residency.mdstates what they must satisfy.