Documentation menuLedger
Ledger
Tamper-evident storage of declarations and receipts, with evidence sheets.
If a consumer says they withdrew in time and you say they didn't, you need records that you can show were not changed afterwards. inverse-ledger writes every declaration and every receipt as an entry in a hash chain: each entry contains the SHA-256 hash of the previous one, so changing, deleting or reordering any entry breaks the chain.
import { createInverseHandler } from "@sweberdev/inverse";
import { createLedger, fileStore, withLedger } from "@weber-development/inverse-ledger";
const ledger = createLedger({ store: fileStore("./data/inverse.jsonl") });
export const POST = createInverseHandler(
withLedger(ledger, {
company,
onDeclaration: (record) => db.declaration.create({ data: record }), // optional, runs after the ledger
sendReceipt: (receipt) => mailer.send(receipt),
}),
);
withLedger appends a declaration entry before your own onDeclaration, and a receipt entry (or receipt-failed with the error) after sendReceipt.
Stores
fileStore(path): one JSON line per entry. Good for a VPS or a mounted volume.memoryStore(): for tests.- Your own: implement
{ read(): Promise<LedgerEntry[]>; append(entry): Promise<void>; replace(entries): Promise<void> }for Postgres, S3 or anything else. Serverless platforms such as Vercel have no persistent disk, so use your database there.
Verify
const result = await ledger.verify();
// { ok: true, entries: 1342, head: "9f2c…" }
// { ok: false, entries: 1342, brokenAt: 718, reason: "hash mismatch" }
npx inverse-ledger verify ./data/inverse.jsonl
Publish or e-mail the head hash to yourself from time to time (the CLI prints it). Anyone who rewrites the whole chain then cannot match a head you recorded earlier.
Evidence sheet
const sheet = await ledger.evidence("W-7K3QX9PD");
sheet.text; // human-readable: declaration, time of receipt, receipt sent at, hashes, chain position
sheet.json; // the same as data
npx inverse-ledger evidence ./data/inverse.jsonl W-7K3QX9PD > W-7K3QX9PD.txt
npx inverse-ledger export ./data/inverse.jsonl --from 2026-06-19 --csv > declarations.csv
Erasure
Personal data must not be kept longer than needed. ledger.redact(id, { reason }) removes the personal data of a declaration and its receipts but keeps each entry's payload hash, so the chain still verifies and you can show that a declaration existed and when.
await ledger.redact("W-7K3QX9PD", { reason: "retention period ended" });