experimentalsince 0.3.0-pre.7

Deletion vocabulary

Canonical terms for the three ways a record stops existing — physical delete, delete marker, and forget tombstone — plus the retention terms built on them.

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 the scopedPurge opt-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) versus derivedResidueDeclined (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.