
Keep an erased person from being copied back in
When the application holds a copy of ERP records and erases a person, the ERP still holds the original. Without a safeguard, the next sync would find the record and write it back: the erasure receipt would say complete while the personal data returned within one schedule period.
So when the application keeps a copy itself, an erasure that reaches a copied record also files a block against the ERP record it came from. A copy that only mirrors the ERP is different: the ERP answers requests about its records, so an erasure leaves that copy to it, and the copy removes what the ERP removes at its next complete pass. Every later pass, full or incremental, skips a blocked record and counts the skip in its report and its audit row. The block applies to every pipeline that reads the same ERP records, so two copies of one person are both stopped.
The block never holds the ERP identifier. It stores a keyed fingerprint of it, computed with a secret derived from the application’s own, so an identifier that is a small number cannot be recovered by trying every number. A block is lifted only when a complete pass over the whole ERP entity no longer finds the record: there is then nothing left to copy back. A lifted block is kept as evidence for the audit-log retention period, then removed; a record erased again is blocked again.
The engine refuses rather than guess. If the secret that made the fingerprints changes, pipelines with active blocks stop instead of copying people back. If a pipeline’s source changes identity, by a renamed connection, another entity or another key format, it stops until an operator decides, with a written reason, whether its blocks carry over or are released.
Example — A former client contact asks to be erased
A client contact asks to be erased. Their record in the application is a copy of an Odoo record. The local copy is erased and blocked, and the erasure receipt names Odoo as where requests about the original are handled; the next hourly sync skips the copy and reports one suppressed record.
For engineers
A system of record decides what happens to its records’ personal data and answers the people they concern; the application follows it, and owns only what it keeps beyond or apart from the source. A binding states this once, in subjectRights, and the default needs no declaration at all. Which postures a binding may take depends on how it uses the ERP.
| Posture | Read-through resource | Inbound pipeline | Outbound or two-way pipeline |
|---|---|---|---|
HANDLED_AT_SOURCE (default) | the erasure receipt names the system of record as where requests about its rows are handled | the copy follows the ERP: an erasure leaves it, nothing is blocked, and its orphan policy (DELETE or IMPERSONALIZE, required once it copies personal data) removes what the ERP removes | allowed: the ERP answers for what it received; outbound rows are the application’s own |
NO_SUBJECT_DATA | allowed; refused beside any field declared as personal data | allowed; refused beside any copied field declared as personal data | allowed on the same terms |
OWNED_COPY | refused: a read-through keeps no copy | the application keeps the copy and erases it itself: scrub or delete, and a block; required when the copy holds a personal field the ERP does not | refused |
FORWARDED_TO_SOURCE | one declared operation the engine also calls for the rows linked to the erased person, best effort | refused: nothing sends a delete through a pipeline | refused |
The following is the binding of Wonder Todos’ externalCustomers pipeline, without its key and comments. Its name and email fields declare identity and contact categories, so NO_SUBJECT_DATA would be refused, and the copy keeps internalNote, the team’s own note, which Odoo does not hold, so it is an owned copy:
binding: {
dialect: HttpApiTransportDialect.ODOO_JSONRPC,
system: 'odoo',
entityRef: 'res.partner',
subjectRights: { posture: SystemOfRecordSubjectRightsPosture.OWNED_COPY },
purposeRefs: ['task-planning-service'],
},
An erasure whose subject reaches such a copy scrubs or deletes the local rows and blocks their records; its receipt also records that Odoo answers requests about the records the copy came from. No acknowledgement is asked for, under any posture.
Where the block is written, and when it lets go
The block is written at the two places an erasure changes a copied row: the scrub of a row kept under retain-and-impersonalize, and the delete of a row removed as a dependent of the erased subject. When the erasure and the block live in different databases, the block commits first, so a failed erasure leaves the block in place rather than risking a re-import; the unscrubbed row is then reported as a live suppressed copy until the erasure is retried.
A block identifies [system of record, remote entity, key shape, remote key]. It is released only by a completed full pass over the whole entity, with no source filter, that did not see the key. A pass that ran under a filter, or a pass spread over several runs that saw the key in an earlier run, releases nothing. A record that a pass wrote back because an erasure committed during its load is detected and counted as a live suppressed row, never silently skipped forever.
When the source identity changes
Renaming the system-of-record connection, moving a binding to another entity or API version, or changing its key codec changes the identity the blocks were filed under. The digests cannot be recomputed, because that would need the plaintext keys the list exists never to hold, so the pipeline refuses to run until an operator calls POST /internal/admin/external-data/erasure-suppressions/reconcile with the resourceType, an action and a reason:
action | When it is right |
|---|---|
carry-over | the same records keep the same keys under the new identity, such as a renamed connection or a new API version |
release | the identity no longer names the same records, such as changed key values; recorded at high risk |
The decision is the pipeline’s alone; another pipeline reading the same entity is not affected.