Threads · 15 / 18 · Sep 7, 2026 · Apache-2.0

You cannot delete what you never encrypted per subject

A soft delete satisfies the right to restrict processing. It does not satisfy the right to be forgotten — and in a database with backups, deleting for real is not something you can do at all. The test is the restore.

Real AES-256-GCM, no hand-waving. What survives a restore only means something if the encryption is real.

The test is the restore

db.softDelete("m1");
expect(db.live()).toHaveLength(0);        // the screen says it is gone

db.restore(anoche);
expect(db.live()[0]!.body).toBe("my number is +507 6123-4567");   // and here it is

The restore was not an attack. It was a Tuesday and a disk failed. The data came back because a soft delete is a state change, and a state change is precisely the sort of thing a backup preserves faithfully.

A hard delete does not save you either:

db.hardDelete("m1");
expect(db.rows).toHaveLength(0);          // gone from today's table
db.restore(anoche);
expect(db.rows[0]!.body).toBe("my number is +507 6123-4567");

Yesterday's backup never heard about the delete. And you are keeping those backups on purpose — that is what they are for.

What works

Encrypt per data subject, and erase by destroying that subject's key:

keyring.destroy("ana");
db.restore(anoche);                       // the ciphertext comes back in full

expect(() => open(keyring, row)).toThrow(UnreadableWithoutKey);
expect(open(keyring, otherRow)).toBe("call me on Thursday");   // her neighbour is untouched

The restore brings back every byte, and the bytes are all that comes back. Per subject is what makes this usable at all: erasure is not per table, so one person's request does not cost everyone else their history.

The trap that every summary of this leaves out

const datos  = db.backup();
const llaves = keyring.snapshot();        // ← the keys live in the same database

keyring.destroy("ana");                   // erased
db.restore(datos); keyring.replace(llaves);

expect(open(keyring, row)).toBe("my number is +507 6123-4567");   // un-erased

The erasure was undone by the same procedure that saved the company. Nobody did anything wrong. The restore ran exactly as designed, and the deletion certificate you sent is now false.

Where the keys live is the whole design. If they are backed up alongside the data, you have encryption at rest — which is worth having — and you do not have erasure. The options are all uncomfortable and you have to pick one: keep the keyring outside the backup set, propagate key destruction into restores as a replay step, or accept that a restore reopens erasures and have a procedure for re-applying them.

Whichever you choose, choose it before the first erasure request, not after.

Two smaller things the tests pin down

Erasure is idempotent. A request arrives twice — because people follow up — and the second one must not fail, or somebody will conclude the first did not work.

Reading an erased row names the subject. decryption failed sends whoever finds it hunting a corruption bug. no key for subject 'ana' tells them the truth: this is not broken, it is erased, and that was the point.

Where this comes from

niiko is multi-tenant and stores customer conversations, so this is not a compliance exercise there — the subjects are real people who write in and sometimes ask to be forgotten. The per-subject keyring and the restore drill both exist because of the failure this repository reproduces.

License

Apache-2.0 — see LICENSE. This is a demonstration, not a package. Copy what you need.


Built by Vorluno — a software studio from Panamá.

// next threadYour CI gate is green because of a pipe