Trash & Restore (Undo Delete) โ
Every delete is archived automatically into a single compressed, encrypted file โ .flash-trash โ so you can restore recently deleted documents without a full backup.
Hard delete still applies to the LSM engine (fast, small). Trash is a bounded undo window on the side.
How it works โ
deleteOne(doc)
โ
โโโบ archive โ .flash-trash (zstd-style deflate + AES, FIFO cap)
โ
โโโบ hard delete from WAL / memtable / indexes
restoreOne(docId) โ read trash โ insertOne โ remove from trash| Layer | Behavior |
|---|---|
| FlashClientCollection | Archives decrypted JSON (best compression) |
| FlashCollection (raw) | Archives engine buffer on internal deletes (TTL, etc.) |
In-memory (:memory:) | Trash disabled โ no .flash-trash file |
Quick start โ
import { FlashClient } from "flash-zk";
const client = new FlashClient({
storagePath: "./data",
});
const notes = client.collection("notes");
await notes.insertOne({ _id: "n1", title: "Draft", body: "..." });
// Delete โ automatically archived
await notes.deleteOne({ _id: "n1" });
// Undo
const result = await notes.restoreOne("n1");
console.log(result); // { restored: true, docId: "n1" }
const doc = await notes.findOne({ _id: "n1" });
console.log(doc.title); // "Draft"API โ
restoreOne(docId) โ
Restores a document from trash if it still exists and is not already live.
const result = await notes.restoreOne("n1");
// { restored: true, docId: "n1" }
// { restored: false, docId: "n1", reason: "not_in_trash" | "document_exists" | "empty_trash_entry" }listTrash(options?) โ
Lists recoverable deletions (metadata only โ no full document bodies).
const entries = await notes.listTrash({ limit: 20 });
// [{ collection, docId, deletedAt, kind: "json"|"buffer", compressedBytes }]purgeTrash() โ
Clears all trash entries (collection helper delegates to the shared vault).
await notes.purgeTrash();
// or database-wide:
await client.purgeTrash();Configuration โ
Trash is enabled by default on disk-backed databases. Tune limits via engineOptions.trash:
const client = new FlashClient({
storagePath: "./data",
engineOptions: {
trash: {
enabled: true, // default: true
maxEntries: 500, // FIFO โ oldest dropped first
maxBytes: 2 * 1024 * 1024, // 2 MB compressed cap
maxAgeMs: 7 * 24 * 3600 * 1000, // 7 days
},
},
});Disable entirely:
engineOptions: {
trash: {
enabled: false;
}
}Payloads are encrypted with a key derived from your secretKey (trashSecret internally). Trash does not weaken zero-knowledge for live data.
Limits & eviction โ
When any limit is exceeded, oldest entries are removed first (FIFO):
maxEntriesโ e.g. keep last 500 deletesmaxBytesโ total compressed trash file sizemaxAgeMsโ entries older than N ms are purged on write
After eviction, restoreOne returns { restored: false, reason: "not_in_trash" }.
Compaction of SSTables does not affect trash โ trash is independent of LSM compaction.
Trash vs backup vs time-travel โ
| Feature | Use case |
|---|---|
| Trash | Quick undo for recent deletes; tiny disk footprint |
client.backup() / restore() | Full database snapshot |
FlashTimeTravel (MVCC) | Historical reads in transactional layer โ not wired to deleteOne |
File location โ
{storagePath}/{dbName}/.flash-trashSingle file for all collections in the database.
When you dropCollection(name), all trash entries for that collection are removed automatically โ there is nothing left to restoreOne for a collection that no longer exists.
Low-level: FlashTrashVault โ
import { FlashTrashVault } from "flash-zk";
const vault = new FlashTrashVault("./data/my_db/.flash-trash", {
maxEntries: 100,
trashSecret: "derived-or-custom-secret",
});
await vault.open();
await vault.archive({
collection: "users",
docId: "u1",
doc: { _id: "u1", name: "Ada" },
});
const item = await vault.peek("u1", "users");
await vault.close();Most apps should use FlashClientCollection.restoreOne instead of calling the vault directly.
Deletion activity log (optional, permanent) โ
Separate from trash: a metadata-only log of delete/restore/drop events โ no document bodies, not restorable.
| Trash | Deletion log | |
|---|---|---|
| Default | On (disk DBs) | Off โ programmer must enable |
| Purpose | Undo (restoreOne) | Permanent audit / UI history |
| File | .flash-trash | .flash-deletion-log |
| Retention | Bounded FIFO | Permanent until purgeDeletionLog() |
| On disk | Per-entry sealed | Single sealed blob (deflate + AES) |
Enable when you want a durable activity feed without keeping full deleted documents:
const client = new FlashClient({
storagePath: "./data",
engineOptions: {
deletionLog: {
enabled: true,
},
},
});
await notes.deleteOne({ _id: "n1" });
const log = await notes.listDeletions({ limit: 20 });
// [{ collection: "notes", docId: "n1", action: "delete", at: 173..., restorable: true }]
await client.listDeletions({ action: "delete" });
await client.purgeDeletionLog(); // explicit wipe onlyEntries persist on disk for as long as the log stays enabled โ no automatic expiry or FIFO cap. The .flash-deletion-log file is deflate-compressed and AES-sealed with a key derived from your secretKey; it cannot be read without FLASH's decoder (deletionLogSecret internally). Use purgeDeletionLog() or deletionLog.purgeCollection(name) only when you intentionally want to clear history.
dropCollection(name) appends a drop_collection event and keeps prior log rows for that collection.