Data deletion
The normative description of what Tessera deletes, when, and what remains — for both project purge (deleting a whole project) and entity-level deletion (a data-subject request against one entity). This page is lint-enforced: its required sections are part of the platform's CI gates.
Scope of deletion
Project purge. DELETE /projects/{project_id} (organization owner
only) marks the project deleting, immediately revokes all of its API keys,
and schedules an asynchronous purge. After the grace window, the purge
worker physically deletes every accessible project-prefixed record across
every store:
- Object storage — every live and noncurrent object generation under
projects/{project_id}/in the data, artifacts, and exports buckets, with one retained exception described below. Deleting a generation in a bucket protected by Cloud Storage soft delete moves it into an offline, provider-retained state. The provider does not permit applications to read, modify, or permanently delete those generations before their fixed hard-delete deadline. - Failed-ingest messages held for operator triage — every stored version
of the messages the dead-letter writer could not park as items that name
only this project, usage records included, counted with the data bucket;
their soft-deleted copies count as provider-retained like every other
deleted object. A message that names this project beside another one,
names no project but contains this project's id, or cannot be read, may
be another project's, so it is not deleted: the purge stays
in_progressuntil the 30-day dead-letter retention removes it (a message that cannot be read cannot be removed by that retention either: it holds the purge until an operator restores access or removes it, and raises an operator alert). A usage record that names this project beside another one never holds the purge and survives it: usage records are kept until the one-time metering backfill has run, then follow the same retention. - Entity ledger and state — every Bigtable row with the
{project_id}#key prefix, across both environments and every deployment slot. - Control database — every row carrying the
project_idin every tenant table, deleted child-first along the live foreign-key graph. - Usage metering — the project's rows in the BigQuery metering dataset and its usage rollup/dedup rows.
Completion requires a total-zero verification. Once all
application-addressable records are gone, a purge with Cloud Storage
soft-deleted generations remains in_progress with
phase="awaiting_gcs_hard_delete". The extended lifecycle
(GET /projects/{project_id}/purge/lifecycle) exposes fresh
provider_retained counts and the latest immutable
provider_retained_until deadline. The original
GET /projects/{project_id}/purge response remains compatibility-frozen for
existing API clients. Before the provider deadline worker passes are cheap no-ops;
at or after it, the worker freshly re-enumerates every store. Only a pass
counting zero total GCS generations and zero records everywhere marks the
purge completed. With the mandatory seven-day restore grace and the current
seven-day Cloud Storage soft-delete policy, terminal completion can therefore
take up to about fourteen days from the deletion request: longer when a
failed-ingest message that may be another project's holds the purge (below),
at most until that message's 30-day dead-letter retention removes it (one
that cannot be read: until an operator acts). An
organization can be deactivated only once each of its projects reached
deleted, so a held purge also delays its organization's deactivation.
The final total-zero result and per-store deletion counts are recorded on both
purge read surfaces and on the project.purge_completed audit event.
Entity-level deletion. POST /entities/{entity_type}/{entity_id}:delete
(organization admin or an admin-scoped key) queues a data-subject
deletion that applies to both environments of the project. Because it
also erases the live environment's data, a request acting in the test
environment (a tsk_test_ key, or the console switched to test) is refused
(permission_denied, environment_not_allowed) and no deletion is queued (no
entity.delete_requested audit event; the refusal itself is audited): use
a live key or the live console. A registered application erases in the
projects it owns in either environment. The worker:
- writes a tombstone so the entity can never resurrect — later events
for it are dropped at ingestion and it is excluded from all future
dataset builds. From then on the rule previews (
POST /labels:preview,POST /pattern-labels:previewandPOST /markers:preview) leave it out, neither counting nor returning it, even before step 3 rewrites its rows, and its events no longer set a label preview's censor clock. A preview retried with the sameIdempotency-Keyruns again rather than replaying an earlier answer; - deletes the entity's ledger rows, its per-slot state rows and score history, and its segment membership rows under every segment registry of the environment (current and superseded, and those of builds that never finished). A segment build running at the same time skips tombstoned entities and, once its new registry is live, removes any member whose tombstone was written during the build; a failed build removes all of its member rows. Segment member listings never return an erased entity;
- rewrites the canonical event store by manifest supersession: every file
containing the entity's rows is replaced by a copy without them, in one
atomic
deletionmanifest, verified by row accounting before commit; - deletes the entity's rows from every registered entity-scoped control
database table (label observation ledgers, alert history, stored webhook
payloads and similar stores), its dead-letter items with their stored
payloads, its seeds in recorded lookalike jobs, and every stored
Idempotency-Keyresponse that names it (a retry with that key then runs the request again). A dead-letter item is the entity's by identity only: its row records the entity, or an identity field of the parked message names it (the event's canonical key or entity, or the id field of the source's mapping, catalog, label table or binding), compared as a typed value the way intake reads it. A value that merely contains the id never matches: erasing12leaves an item holding123,x#12or12:00. When a message body holds several records (a JSON array or NDJSON), only the entity's records are removed (the others keep their bytes as received), together with the message's own attribute and header values naming the entity, and the item stays for the others. A record naming the entity together with another entity goes with the entity, and so does an item that cannot be split: that other entity's failed-ingest diagnostic copy is removed to honour the erasure, and the completion event counts such items (collateral_items). An item no identity field attributes (an opaque or malformed message) is never deleted. When it was received before the deletion started (a replayed copy counts from the original's receipt) and contains the id as a whole word, the deletion staysin_progressuntil the dead-letter retention (30 days) removes it; the completion event records how many such items it waited for and when the last one expired (retained_until_sweep). A message received after the deletion started is new data: if mapping cannot attribute it, it is parked unattributed and expires with the 30-day dead-letter retention, and any replay of it is mapped again, attributed and dropped by the erasure tombstone. The same rules cover failed-ingest messages that could not be parked as items at all (held for operator triage and removed after the same 30-day retention): one whose ingest message names the entity as its primary entity (not merely among another entity's secondary entities), in this project, is deleted in every stored version, and one that arrives after the deletion started is never stored; one that cannot be attributed and contains the id as a whole word holds the deletion until that retention removes it (at most 30 days after the end of the day it arrived; one that names no single project may be another project's and is never deleted by hand either); - removes the entity's person properties from destination deliveries still waiting to be retried (other entities in the same batch keep their pending delivery, byte for byte) and from destination backfill resume positions, so nothing about the entity is sent after the deletion. Properties a vendor such as PostHog already received stay in that vendor's project;
- rewrites every platform output artifact in the exports bucket that names the entity — export files and manifests, embedding projections, retained audience versions, lookalike lists and holdout control manifests — in place without its rows (download links already issued for those objects serve the rewritten files), and removes their earlier object versions. Copies you delivered to your own buckets, warehouses or destinations are yours to erase;
- rewrites every comparator upload of the project that holds the entity's
rows (CSV or Parquet, in the data bucket) without them, in the same format,
removes every earlier version of the file, and validates it again with
refreshed counts and fingerprint. The audit event carries counts only. An
evaluation still pinned to the old file settles
failedwith reasoncomparator_rewritten_by_erasureand writes no report, and a recompute naming the old fingerprint is refused with that reason. Reports already written keep their recorded fingerprint and are never edited; - rewrites the raw source input you sent or connected, in place without the
entity's rows: every landed snapshot of the environment's sources (the
snapshot's Parquet parts, its rejected rows, its manifest row counts and
fingerprints, and its profile's examples, top values and ranges,
recomputed over the rows that stay), and every uploaded file (CSV or
Parquet, compressed or not, read exactly as its sync reads it). A row is
the entity's only when an identity field of the source's own
interpretation names it as its primary entity, typed and normalized as the
sync did: a landed snapshot with the mapping version and crosswalk version
its sync used, an uploaded file with those of the sync that ingested it
and the current ones (other interpretations: batch catalog, label table,
approved automatic binding or, before a binding, the onboarding identity
candidates). A row the entity shares with another entity, a row no
identity field attributes that holds the entity's id, and raw text that
holds it (a rejected line, a malformed CSV line, a file no source
attributes such as a comparator file) are never erased automatically:
Tessera's operators review them (a redacted preview, the id masked)
and either delete them through the same rewrite or record that they are
not the entity's; the deletion completes once every item is resolved, and
its completion record counts both decisions. Earlier object versions
are removed where they hold the entity. Retained raw JSON batches drop
their payload (typed
entity_erasure), and ingestion receipts redact the entity's entries. From the deletion's start, receipts, sync rejections and the mapping and upload preview samples already hide the entity (a rejected line that holds its id included), and every later sync drops the rows attributed to it before they land (counted as anerased_rowsitem on the sync'sdetails). A snapshot that a running sync is still writing, and a file of a source with a queued or running sync, is erased by the next pass, after the sync ends. A snapshot that automatic onboarding has not finished taking in (its handoff pending, failed or stopped) is erased by the first pass after the handoff completes or a newer snapshot supersedes it: a later read of the snapshot must find the rows it took in before, or records already taken in would be taken in again. Resume onboarding re-drives a failed or stopped handoff. A rewritten uploaded file's attachment record is updated with it, so later syncs keep working. A kept rejected row keeps its row number: it names the row of your source file as the sync landed it. Deleting a source deletes its landed snapshots and uploaded files with every version, after the deletion's response; the raw-input sweep deletes any landed object of a deleted source it finds.
Physical deletion of superseded event-store files waits until no pinned or
unexpired snapshot still resolves to them; the record reports
awaiting_snapshot_release (with the blocking snapshot ids) until then, and
completed — with an entity.delete_completed audit event — only after the
files are physically gone. A snapshot that nothing pins stops protecting files
7 days after it was taken; snapshots taken before 2026-10-14 keep their 90-day
expiry. A dataset build pins the snapshot it was built from, so a build made
before the deletion holds the files until its pin is released: when its files
are deleted (dataset build retention), once no other
build or preparation holds the same snapshot.
The event-store bound. The superseded files are deleted at the first
entity-deletion cycle (every 5 minutes) after every snapshot created before
the erasure has expired or been released: at most 7 days after the erasure,
unless a pinned build made before it, or an automatic preparation in flight,
still holds one. Only then does the record reach completed and emit
entity.delete_completed. Each dataset build's file state counts the
deletions it still holds (blocking_erasures).
A build that failed keeps its intermediate files (under
projects/{project_id}/datasets/{fingerprint}/ or event-datasets/) for a
relaunch, and they hold the rows it read, so it holds its snapshot until they
are gone: as for any build, the files go first and the pin after. For a
snapshot that holds a deletion back, the entity-deletion worker (every 5
minutes) settles the builds naming it:
- it fails a build that has recorded nothing for longer than its Job may run
(25 hours,
build_deadline_exceeded); - it deletes every version of every file of each failed build naming the snapshot and checks that none is left;
- then it releases the snapshot.
A read of a failed build (GET /datasets/{id}, GET /event-datasets/{id},
or the dataset listing when it fails the build) releases the snapshot too,
but only once no build naming it has files left; a read deletes nothing, and
it skips the release while the worker is settling the same snapshot.
Unless another build that has not failed, a build whose files are being
deleted (until that deletion has removed them all), or a preparation that has
not failed or been refused (a succeeded one included), uses the same snapshot,
the release ends the snapshot's protection there and then, whatever its own
expiry, and the deletion proceeds in the same worker run. Building that
failed build again with the same snapshot_id is then refused with
400 failed_precondition (snapshot_expired): create a new build, which
takes a new snapshot of the current data. Jobs that read a snapshot without
pinning it (segment explanations and cards given --snapshot-id, the
identifier-index Job and the TES-001 entity inspector) refuse a released
snapshot when they start, with the same snapshot_expired error. A Job
already reading a snapshot when it is released may meet a file the deletion
has removed and then fails closed; the identifier-index Job also re-checks
the release (and its own run deadline) before writing its index and is
cancelled if it happened, and a deletion does not complete while an
identifier-index Job of the project may still be running.
Messages in transit. Pub/Sub keeps the messages of Tessera's internal event topics (the ingestion topic, and the internal topic that holds events waiting for a replay) for 7 days, including messages that were already processed. Entity deletion does not rewrite those copies: they expire within 7 days, and every consumer checks the entity's tombstone before it stores anything, so they cannot bring a deleted entity back.
The project-level awaiting_gcs_hard_delete phase introduced above does not
silently redefine entity-level completion. Aligning entity-level deletion
records with provider soft-delete retention is tracked as a separate
follow-up.
Grace window and restore
A project purge begins with a 7-day grace window. While the purge is
pending, no tenant data is mutated, and the organization owner can
cancel it with POST /projects/{project_id}:restore: the project returns to
active, the purge becomes canceled, and a project.restore audit event
is recorded. API keys revoked by the delete stay revoked — reissue keys
after a restore.
Once the window elapses and the worker starts, restore fails with
400 failed_precondition; there is no undo for a started or completed
purge. Entity-level deletions have no grace window — they begin at the
next worker cycle.
What is retained and why
The surviving records of a purged project are exactly:
- The audit trail —
audit_eventsrows and the project's audit exports underprojects/{project_id}/audit/in the exports bucket. Deleting them would destroy the evidence that the purge (and everything before it) happened; audit retention and export-verified partition drops are governed by the audit-log policy, not by project deletion. - A scrubbed project tombstone — the
projectsrow survives withstatus="deleted"and all configuration (including the name) scrubbed to NULL, preserving organization history and audit referential integrity. - The purge record itself — status, timestamps, per-store deletion counts, final zero provider-retained counts, the last provider deadline, and the total-zero verification result.
- Immutable entity tombstones — the minimum SQL erasure index survives so delayed or replayed work cannot resurrect a previously erased entity. These rows contain no raw entity identifier: only one-way/pseudonymous digests, environment/type metadata, deletion provenance, and the creation time.
For entity-level deletion, the deletion record survives with the raw
entity_id scrubbed on completion; the pseudonymous entity_hash
(a per-project keyed HMAC) remains as the stable reference, and all audit
events of the flow reference the entity only by that hash — an erasure
record never re-retains the erased identifier. The tombstone itself stores
only a one-way digest of the entity key.
Three further records of an erased entity survive by design:
- Suppression entries — a suppression list entry naming the entity (for
example a self-exclusion) keeps the exclusion, but the raw
entity_idis replaced by the same keyedentity_hashduring the erase pass. The set's size and its audit history stay consistent, the entry no longer reveals the identifier, and the permanent tombstone already prevents any new data for that entity from being accepted and delivered. Listing the set returns the pseudonym in place of the erased id. - Sampled read-audit events —
state.readaudit events record the entity keys a read returned. Like the rest of the audit trail they are kept for the audit-log retention period and are not rewritten by an entity deletion. - Unattributable dead letters, until they expire — a parked message no
identity field attributes to any entity is not deleted on a mere
occurrence of the id, which could be another entity's data. One received
before the deletion started that contains the id as a whole word holds the
deletion
in_progressuntil the 30-day dead-letter retention removes it, and the completion event counts it. Later messages follow the general dead-letter retention.
Aggregate artifacts
Fitted encoders (quantile knots and categorical statistics) and model
checkpoints are statistical aggregates computed over training splits.
Individual entity records are not retrievable from them, and they are
not subject to per-entity deletion: nothing is erased inside a model.
Models are retrained on dataset builds that exclude erased entities (the
builder consults the tombstone index), and older files are deleted whole: by
model file retention, by an explicit purge of a model
version's files, or by project purge (everything under
projects/{project_id}/… in the artifacts bucket).
Evaluation reports are a correction to that rule. A report can name entities in its advisory and diagnostic blocks:
- Reports written from 2026-10-14 on name an entity only by its
pseudonym,
hash:plus the keyedentity_hashof the project (the same value deletion records keep): comparator violations, the G4 and G5 diagnostics (the G5 list bounded to 100, with the full count), and the event-model diagnostics, which name events byhash:plus a keyed digest of the event id. A pseudonym links back to a person only for someone holding both the platform key and the raw id. - Reports written before may hold raw entity ids or event ids in those blocks. They are immutable records, pinned by fingerprint, and are never edited: they go with their model's files (model file retention or an explicit purge) or with the project. G4 and G5 carriers belong to models that cannot be bundled; archive and purge them. A promotable report with comparator violations is replaced by evaluating again against the redacted comparator and bundling anew.
- Baseline feature matrices (
features-{train,val,test}.npyunder a report'sbaseline/, one row per target point) are no longer written; the ones written before are deleted by a one-time pass of the purge worker, which verifies none remain. - Leakage evidence in an aborted build's violation report holds a count and keyed digests of the identifier values it found, never the values; the existing reports were rewritten in that form.
Customer map editions keep three aggregate traces after a sweep. Each nightly or Update now edition of the customer map is aligned to the previous edition: one rotation, scale and shift is fitted on the customers both maps share and applied to every point. The deletion sweep removes an erased customer's point from every edition, and an edition's metadata never holds a customer id. Three values in that metadata were computed with the customer and stay as written after the sweep:
-
The alignment coupling. A customer erased after an edition was drawn was one of the points that edition's alignment was fitted on. The recorded fit then no longer matches the remaining shared points of the two swept editions exactly, and the difference can be solved in closed form for that customer's two positions.
-
The data hour.
data_up_tois the newest customer event time, truncated to the hour; it can be the hour of the erased customer's newest event. -
Counts and shares. These still include a customer erased after the edition was drawn:
- the verdict's customer counts and its grouped shares (to 3 significant digits), and the number of shared customers;
- the "what changed" block: the map-wide totals of new, departed and moved customers, and each group's size before and after. On full maps these are exact counts. When either map was sampled they are shares to 3 significant digits, and the new and departed totals are not recorded;
- the stability figures (how far shared customers moved, how many map neighbours they kept, and the share that kept their group), to 3 significant digits.
Compared with the swept files, the group sizes can show which group the erased customer was in at that edition, and the totals whether they were new, moved or departed. This is the same class of trace as the alignment coupling (HD-955), and weaker. A customer erased before an edition is drawn is left out of it entirely: the edition checks the previous edition's customers for erasure and leaves an erased one out of every comparison, even before the sweep has rewritten that previous edition.
None of these carries a customer id. The same values travel in the
export.completed webhook and stay in its delivery record, so they last as
long as that record as well as the edition, and reach the project's webhook
endpoints. The fix for the coupling (fitting on a keyed-hash subset of
customers, or re-aligning swept pairs) is tracked in the hardening register
(HD-955).
Model file retention
An archived model version's files can be deleted whole, either automatically
by the project's retention setting (off by default; 1 to 365 days after the
archive) or on request by an admin (purgeModelFiles). Archiving alone deletes
nothing. The setting covers both environments, so a request acting in the
test environment (a tsk_test_ key, or the console switched to test) may only
keep files longer: turning retention on or shortening it is refused
(permission_denied, environment_not_allowed); use a live key or the live
console. A registered application keeps both directions in the projects it
owns, also when it acts in test: its partner flow turns both retentions on to
bound how long an erasure takes to complete.
What is deleted. Every live and noncurrent generation in the artifacts bucket under exactly the prefixes the model version owns, and nothing else:
| Kind | Prefix under projects/{project_id}/ |
|---|---|
| Training run | runs/{run_id}/ (checkpoints and run outputs) |
| Event model | event-models/{model_id}/ |
| Evaluation reports | evals/{report_id}/ of every report of the model |
| Backtests | backtests/{backtest_id}/ of the model |
| Bundles | bundles/{bundle_id}/ of the model |
| Entity baseline model | entity-baseline-models/{model_id}/, once that family is registered |
Every prefix is checked against the rows of the same project before anything is deleted; a plan that names anything else is refused and deletes nothing.
What is kept. Dataset builds, including their fitted encoders, feature specs and shards (other models share them; they go with dataset deletion or project purge); shared checkpoint copies and bindings (they belong to the registered export); dossiers; the audit trail; and the Cloud SQL metadata rows of the model, its runs, reports, backtests and bundles, whose file URIs become historical pointers.
When a deletion starts. Only once every hold is clear and the 24-hour
drain is over. A deletion waits while a slot serves or a source binds one of
the model's bundles, while work on the model is still running (training,
evaluations, backtests, preparation, replays, or a labeled job), while the
project is being deleted, while the organization's legal hold is active, and
while a fact the deletion must preserve cannot be verified. The drain is 24
hours after the archive and after the model was last in use, the longest
time any job may keep reading a model's files. An explicit request is due 24
hours after it is made at the earliest, its cancellation window: until the
deletion starts, cancelModelFilePurge withdraws it, and a cancel never
makes a deletion start sooner than the request allowed.
After it starts. A purged model version can never be activated, rolled back to, bundled or evaluated again, and its evaluation report and card are no longer served; dossiers assembled before the purge keep their recorded content.
The provider's soft-delete window. The artifacts bucket keeps deleted
objects as soft-deleted copies for 7 days, as the storage provider's
protection against accidental deletion. At files_deleted_at, no readable
generation of the model remains; the offline soft-deleted copies are removed
by the provider by provider_retained_until, and the deletion completes only
when a fresh enumeration finds none. The one option that would remove this
window, disabling soft delete on the artifacts bucket, is a platform
infrastructure decision outside this feature: the bucket's backup posture
requires the window today.
Dataset shards
Training dataset shards are immutable, fingerprinted artifacts: their
content addresses are pinned by model and bundle lineage, so they are never
rewritten in place, and per-entity redaction inside a build is deliberately
not offered. Instead, a build is deleted whole: by
dataset build retention once nothing uses it, by
an explicit purge, or by project purge. Every artifact of the build goes with
it: its shards, manifest, encoders, its derived label store (derived/, the
label observations the build assembled, by entity), its supervision audit,
its build state, and its baseline evaluations without a model. When its files
are deleted, the build's pinned event-store snapshot is released, once no
other build or preparation holds it, which lets the erasures waiting on it
complete. Rebuilds exclude tombstoned entities: the dataset builder consults
the tombstone index at assembly, so a build made after an erasure never
contains the erased entity. The project's own label store is erased like any
other per-entity store; a dataset build that pinned one of its manifests
holds the superseded label files until that build's snapshot is released (at
once when the build failed, as above).
Event labels. An event-label observation names the event it is about, not an entity, so deleting an entity erases the event labels of its events:
- every event of the entity (its own events, not another entity's events that name it as a secondary entity) that is removed or refused is first recorded as a one-way digest, never its id. That covers the events the deletion removes and any event of the entity that arrives later and is refused, whether it comes through ingestion, a connector sync, the dead-letter queue or source onboarding;
- the observations of those events are removed from the event-label store, with their deduplication keys, the same way as entity labels (an event dataset build that pinned a manifest holds the superseded files until its snapshot is released). The deletion does this on every pass, and after it completes a periodic sweep does it for events refused later, within 24 hours;
- a label sync that was already committing when an event was recorded can still store a label for it a moment later. So the deletion completes only once a few minutes have passed since the last event it recorded, and then only if a fresh check finds no event label left. If the deletion is interrupted after recording events, that wait still counts from when they were recorded. Events recorded by other paths during the deletion, such as a refused delivery, are swept with it but do not delay its completion; a label that lands for one of them afterwards is removed by the periodic sweep;
- a later sync of your label table never stores a label for any of those events, even when the row is still in your table. It is dropped and counted. When the record cannot be read, the sync fails before it stores any event label, and the next sync reads the same rows again;
- the record names the event id, not the entity. When another entity's event reuses an erased event's id, its event labels are erased and refused too. While both events existed, every dataset build treated that id as ambiguous and used none of its labels.
Dataset build retention
A dataset build's files can be deleted whole, either automatically by the
project's dataset build retention (off by default; 1 to 365 days after the
build became idle) or on request by an admin (purgeDatasetFiles). The
setting sits beside the model file one; with both at the same value, a build
is deleted in the same worker cycle as its last model, or the next. Like the
model file setting, it covers both environments, so a request acting in the
test environment may only lengthen it or turn it off.
Idle. A build is idle when it has finished, every model version trained on it is archived, and nothing reads it in flight. It became idle at the latest of its own end, those archives, and the end of every run, training, evaluation and automatic preparation that read it.
What is deleted. Every live and noncurrent generation in the artifacts
bucket under exactly the prefixes the build owns: datasets/{fingerprint}/
(a sequence build) or event-datasets/{fingerprint}/ (an event build), and
evals/{report_id}/ of every baseline evaluation of the build that has no
model. Every prefix is checked against the rows of the same project before
anything is deleted.
What is kept. The build's record and its frozen manifest (the dataset and recipe reads keep answering the same bytes), evaluations, annotations and label statistics; the label stores; automatic inputs; model files (they go with model file retention); dossiers; the audit trail. Event-store files are removed only by the event store's own collection.
When a deletion starts. Only once every hold is clear and the 24-hour
drain is over. A deletion waits while a model version trained on the build
still has its files, while work reading it is in flight (the build itself, a
run, an event training, an evaluation or an automatic preparation), while the
project is being deleted, while the organization's legal hold is active, and
while the build's manifest cannot be verified for the freeze. An explicit
request is due 24 hours after it is made at the earliest, its cancellation
window: until the deletion starts, cancelDatasetFilePurge withdraws it. An
explicit request can also ask for the files of the build's archived models in
the same call; models that are not archived are never touched.
After it starts. The build can no longer train or evaluate, and a request
that would build it again under the same fingerprint is refused with reason
dataset_files_purged; a build from current data has a new fingerprint. The
provider's 7-day soft-delete window applies as for model files.
Erasure completion bound
How long an erased entity's data can remain in Tessera's stores, counted from
the deletion request. The figures hold for the stores model file and dataset
build retention own and for the event store; they depend on two choices the
project makes: how soon a replacement model is trained, activated and the old
one archived (R, days after the request), and the retention it sets for
model files and dataset builds (D days, the same for both).
| Step | Worst case after the request |
|---|---|
| Logical erasure: ledger, state, event-store rewrite, registered stores, comparator uploads | 1 day |
| A replacement trained on a build that excludes the entity, activated, the old model archived | R days |
| The old model's files: no readable generation | R + D days and one 15-minute cycle |
| The old dataset build deleted and its snapshot pin released | the same cycle or the next |
Superseded event-store files deleted; entity.delete_completed | minutes later (a snapshot older than 7 days stops protecting at its release) |
| No readable copy in Tessera's stores | R + D days, plus under one hour |
| Database backups of the erased rows expire | 31 days (30-day retention) |
| The provider's soft-deleted copies of every deleted generation expire | 7 days after their deletion |
| No copy at all | R + D + 7 days, plus under one hour |
With R = 35 and D = 6, no readable copy remains after 41 days and no copy
at all after 48 days. Anchored on the erasure's logical completion instead of
the request, both figures grow by at most a day.
Limits. The bound holds only for the replacement flow above: a build that many models share is deleted after all of them are purged, and a model that is never archived keeps its files. In a project that already had snapshots created before 2026-10-14, such a snapshot can protect superseded event files until 90 days after its creation. Reports written before 2026-10-14 keep any raw ids they hold until their model's files are deleted. Records that other work packages erase (dead letters, landed snapshots, event labels) follow their own retention, stated above. Copies already downloaded or delivered outside Tessera are not reachable.
Secondary-entity references
A canonical event belongs to its primary entity. Other participants
recorded in secondary_entities (a counterparty, a device, a store) are
part of that primary entity's history: deleting entity A removes A's own
events everywhere, but events owned by other primary entities retain
their secondary_entities entries referencing A — they are another
entity's history, not A's record. Where pseudonymization is required
downstream, exports use the keyed entity_hash in place of raw ids.
The reverse also holds. When A's own event names other entities, the platform keeps a copy of that event in each of those entities' event history, so their timelines show it. Those copies are A's events, and deleting A deletes them too. The other entities' own events stay, and so do their event counts and first- and last-seen times, which name nothing of A.
A copy is looked up under the other entity's id in the form the platform
stores it (Unicode NFC, with line breaks normalized). For events received
before ids were stored in that form, a copy kept under an id that this
normalization changed may not be found. The deletion still completes, and
its entity.delete_completed audit event counts these cases
(ledger_derived_copies_inexact).
For deletions that completed before copies were deleted this way, an operator runs a one-time cleanup. It deletes the copies that carry the copy marker. An older copy, written before that marker existed, is deleted once the platform confirms it is not the other entity's own event: that entity's own events are stored in its event history, a copy never is. This check is trusted only for copies received after every event of the platform carried its own marker, so the cleanup is given that moment for each environment. Copies it cannot confirm (received earlier, received within the last day, or whose day of history has already expired, or that match an event of the other entity only in part) are counted and reported as remaining work, and are never deleted on a guess.
Ids stored in an earlier form
Entity deletion matches the entity id exactly. An identifier sent as a number
now becomes its digits (how identifiers become entity ids), so a
customer sent as 12.0 is the entity 12; events stored before this rule
keep the earlier form 12.0, and label tables may hold 12.000000000. Those
earlier forms are kept as they were stored. A data-subject request for a
customer whose id arrived as a number must name each stored form: submit
an entity deletion for 12 and one for 12.0 (and any other stored
spelling). Raw inputs and parked messages are attributed the way intake reads
them now, so a numeric id field holding 12.0 belongs to 12 and the
deletion for 12 removes them. A parked message that carries an already
mapped event keeps that event's id: one stored as 12.0 belongs to 12.0.
Your operator can tell you whether your project holds ids in an earlier form:
a read-only report counts them per source, label and crosswalk, never listing
an id.
Every deletion covers exactly the id it names. A deletion of 12 never
removes or blocks the text id 12.0, and a deletion of 12.0 never removes
or blocks data that arrives as the number 12. That holds for a deletion made
before this rule too: one that named 12.0 keeps covering what is stored
under 12.0, and the data that now arrives as 12 needs a deletion of 12.
Text ids are compared in one canonical form, Unicode NFC with LF line endings
(how identifiers become entity ids).
A deletion of José covers that id however it is spelled: an é sent as one
character or as e followed by a combining accent, a CRLF line break or a LF.
Before this rule, text ids were stored as sent. A deletion made then for an id
spelled in another form covers only that spelling: submit it again to cover
the id in every form. Records stored then under an id spelled in another form
stay under that spelling, and a deletion looks records up by the canonical id,
so it does not reach them.