Deletion vocabulary
noy-db is an encrypted, offline-first document store; a single database instance is called a
vault, and every record is persisted as an encrypted
envelope. "Deleting" a record means different things depending on whether the vault
syncs and whether the goal is removal or provable erasure — and the vocabulary
crosses package boundaries: the envelope field travels the store contract every to-* backend
must honor, sync enforces the semantics, and periods reclaims the space.
The terms below pin each meaning down.
Physical delete
The plain case: collection.delete(id) on a non-synced collection removes the envelope from
the store. Nothing is left behind. This is what delete() always did, and still does wherever no
sync target is configured.
Delete marker
On a synced vault (since 0.3.0-pre.7, on latest from 0.3.0), collection.delete(id) instead
writes a version-ordered envelope with _del: true and an empty body. The marker is a first-class
write: it propagates on push()/pull() like any edit, so an offline peer can no longer
resurrect the deleted record by pushing its stale copy — the marker outranks it. Reads treat a
marker as absence (get() → null), and a legitimate re-create at a higher version
resurrects the id. The _del field is part of the store contract (see
Stores): every to-* store round-trips it unchanged.
Forget tombstone
What vault.forget() leaves behind: a crypto-shredded envelope whose
per-record key material is destroyed, making the body permanently undecryptable while proving —
via the ledger — that the record existed and was erased. Unlike a delete marker, a forget
tombstone is terminal: sync never overwrites it with
a live envelope regardless of version, conflict resolvers are never consulted against it, and no
re-create resurrects the id. Rule of thumb: delete converges, forget is forever.
Erasure residue
What vault.forget() reports when it could not erase something it found —
surfaced rather than silently dropped, so an audit can tell exactly what survived and why. Two
distinct classes:
- Scope residue (
scopedPurgeResidue) — under thescopedPurgeopt-in, an undeclared collection's sealed-CEK envelopes or blobs are skipped-and-reported (skipped-undeclared-sealed-cek/skipped-undeclared-blob-scan) rather than scanned. - Derived residue — a materialized-view output row belonging to the forgotten subject that
resisted erasure. Since
0.3.0-pre.13(#785) the reason is split by compliance meaning:derivedResidueUndecodable(ownership unconfirmed — the row's ownership stamp couldn't be decoded, e.g. it was elevated above tier 0) versusderivedResidueDeclined(ownership confirmed — the row decoded and matched, but the tier gate declined its erasure, leaving a live tier-holder-decryptable copy). Reading "undecodable" as "couldn't tell whose it was" is only true for the first; the second is a known, deliberate retention. That tier gate is documented on tiers.
Resurrection window
The interval during which purging a delete marker would be unsafe: a peer that has been offline since before the delete still holds a live copy, and without the marker its next push would silently bring the record back. No single vault can prove every peer has converged — which is why marker purging is gated on an operator-asserted safe-point, the closed accounting period.
Freeze / target-purge
The two reclamation operations periods builds on a closed period
(since 0.3.0-pre.8): freeze (vault.freezePeriod()) physically purges the period's delete
markers from the local store; target-purge (vault.purgePeriodTargets()) sweeps them off
push-only sync targets (backup/archive roles), never off bidirectional peers. Both record a
companion + ledger entry and leave forget tombstones, history, and live records untouched.
For the full mechanics, start at sync → deletes converge as delete markers.