Status: draft, pending legal review (input I10). Not legal advice. Last updated 2026-11-12. This is the explicit erasure statement the RFP asks for. The API contract isdocs/reference/control-plane-api.mdsection 10; the behaviour below is tested ininternal/controlplane/erasure/live_test.goagainst real PostgreSQL, NATS, Umami, the gateway’s visitor code, the replayer and the jobs follow-up.
1. What can be erased
A project owner asks for erasure on the project’s Privacy page in the console (/projects/{tenant_id}/privacy, tested in
tests/e2e-console/specs/privacy.spec.ts) or with
POST /api/tenants/{tenant_id}/erasure. Every member sees the request
history there. The page keeps a typed IP address and user agent in memory
only, never in browser storage, and clears them once the request is answered. Erasure is never blocked by a
suspension or by terms the project has not accepted yet. There are three kinds.
After each kind the jobs task
erasure-followup reads the affected UTC days of
agg.web_daily and agg.web_daily_dim again from Umami (for site_reset it
deletes every aggregate row of the site and reads the last days again), so the
operator view stops counting erased visits. The request shows
reaggregated_at when that is done.
2. Why visitor erasure works only on the day of the visit
The service never stores an IP address. A visitor is known only by a token computed from the address, the user agent, the site and a secret that exists for one UTC day in memory (privacy-strategy.md section 3). On the day of the
visit the gateways can recompute the token from the address and user agent the
owner supplies, and the replayer’s visit record links the token to the Umami
sessions.
After midnight the secret is gone. No one, including the operator, can
recompute the token or tell which stored session belongs to which person. A
request on a later day therefore finds nothing, and the answer says so
(outcome = "nothing_found", with the explanation in message). GDPR Art.
11(1) relieves a controller of keeping or acquiring information only to
identify a data subject, and Art. 11(2) suspends Arts. 15 to 20 when the
controller can show it cannot identify the person, unless the person provides
additional information that enables identification (S10). Whether an address
and user agent supplied by the data subject count as such information when the
secret no longer exists is a question for counsel; technically, no supplied
information can re-link a past day’s sessions.
A data subject who can point to a session (for example the tenant finds it in
the dashboard by time and page) can still have it deleted with kind session.
3. How a request runs
- The control plane checks membership and role, the body and the site.
visitor_today: the control plane sends the address and user agent to the gateways in one NATS request onedge.visitor-token(only the control plane may send it, only gateways may answer,internal/natsx/topology.go). A gateway answers with the token and the UTC day it belongs to. The address and user agent are not kept anywhere after this step: not in a table, a log line, an audit row, an error or a queue.- The control plane notes the last sequence of
WEB_EVENTS, inserts the request intocp.erasure_requests(statusrunning) with an audit row, and answers 202. - The run waits until
WEB_EVENTSholds no event of the site with a sequence at or below the noted one, so an event accepted before the request and still queued (up to 72 hours) is in Umami or in the dead-letter queue before anything is deleted, and is deleted with the rest. If that does not happen withinERASURE_QUEUE_WAIT(default 15 minutes), the run fails without deleting anything and says why. - The run deletes as described in section 1. For
visitor_todayandsessionit removes the visit record first, so a later event of the same visitor starts a new session instead of reusing the deleted one. Avisitor_todayrun first writes the session ids it is about to delete to a progress record (erasing.<site>.<token>in the same memory bucket, same 24 hour limit) and removes it only after every session and dead-letter message is gone. A run that fails part way therefore leaves the sessions findable: the next request of the same day finishes the work and answerserased(TestErasure/a_failed_visitor_run_is_finished_by_the_next_request). - The run records
outcome,message, the counts (sessions_deleted,events_deleted,dlq_messages_deleted) and the affected days. A failed run records what it deleted before the failure. erasure-followupre-aggregates the affected days. It also fails a run whose control plane process stopped (no heartbeat for 2 minutes); such a request must be submitted again, because avisitor_todaytoken existed only in that process. A request submitted again on the same day finds what the failed run left (step 5).
erasure_in_progress.
4. What the records keep
cp.erasure_requests keeps the kind, site, requesting user, times, status,
outcome, message, counts, the UTC day (visitor_today) or the session id
(session), and the affected days. It never holds an IP address, a user
agent or a visitor token. The audit log keeps erasure.request,
erasure.complete and erasure.fail rows with the same restriction. The test
TestErasure/no_residue reads both tables, every message of every NATS stream
and bucket, and every log line of every component for the addresses and user
agents used, and finds none.
5. Limits
- Same-day only for visitor erasure (section 2).
- The link is in memory. The record that links a visitor token to its
Umami sessions lives in the memory bucket
visit_state, which a NATS restart empties; a restart that also emptiesedge_secretschanges the visitor’s token for the rest of the day. A visit before such a restart cannot be located, although it may still be stored. The answer then says that the visit “could not be located”, never that none was recorded, and names the time the link store was emptied when that was today; it points to a site reset as the way to remove everything that may remain (TestErasure/a_visit_whose_link_was_lost_is_not_located). Umami derives the session id from the token with its ownAPP_SECRET, which the control plane deliberately does not hold (it also signs Umami’s login tokens), so the session cannot be computed instead. - Concurrent event. If the visitor’s own browser sends an event at the
moment the run deletes the session, the replayer can store that event with
the old session id after the session row is gone. Such an event belongs to
no session, so Umami’s reports, which join events to sessions, do not show
it.
jobs retain-webdeletes events without a session that are younger than 72 hours on its next run (tested ininternal/jobs/retainweb). - Session kind and the dead-letter queue.
WEB_DLQmessages carry the visitor token, not the session id. Asessionrequest finds them only through the visit record, which exists for 24 hours after the visitor’s last event. Older dead-lettered messages expire with the queue’s 7-day limit. - Probing. The workflow tells an owner whether an address and user agent visited its site today, and deletes that data in the same step. Only owners can use it, and every request is audited.
- Backups. Phase 16 builds backups. Data deleted here stays in backups
until they expire; their retention is stated in
data-residency.mdonce they exist.
6. Tenant exit
When an operator removes a project, the purge deletes its Umami team and websites, its registry entries and, unlessremoval_keep_aggregates is set,
its aggregates (docs/runbooks/tenant-removal.md). A tenant that wants its
web data deleted while it stays enrolled uses site_reset per site.