Prove your offline history
was not tampered with.
Local-first apps create data on offline devices, edited by different people, synced later in any order. @blocco/ledger gives each device a signed hash chain, merges them deterministically, and surfaces tampering, forks, and forged signatures as typed conflict flags, never silent failures.
$ npm install @blocco/ledgerYou cannot prove a record was not altered after it was written.
When several devices each hold part of the truth, you cannot merge them into one history everyone can independently check.
CRDTs solve merging but trust their inputs. Signed audit logs solve authenticity but assume one writer. This does both, with one small idea.
Tamper-evident by hash chain
Every device keeps an append-only SHA-256 chain. Change one field and the recomputed hash diverges: the chain no longer verifies.
Signed, self-certifying identity
Each entry is signed with a non-extractable ECDSA P-256 key. The device id is the hash of its own public key, so identity cannot be spoofed.
Deterministic merge
Logs from any number of devices merge into one ordered log, identically regardless of arrival order. Commutative and associative over the set of entries.
Typed conflicts, no silent fixes
divergent_id, broken_chain, invalid_signature. The library gives you facts and where they happened; you decide the policy.
Zero deps, isomorphic
Nothing to audit but this. Runs the same in a browser and in Node ≥ 20, touching only globalThis.crypto.subtle. TypeScript strict, generic over your payload.
A substrate, not a framework
No storage, no network, no UI. You bring those; the library gives you the verifiable core, transport-agnostic and honest about scope.
How it fits together
Each device appends to its own chain and signs each entry. Merge unions and orders the logs deterministically; the crypto tier additionally verifies every chain and signature and reports what failed and where.
flowchart LR
subgraph DA["Device A (offline)"]
A0["entry a0"] --> A1["entry a1"]
end
subgraph DB["Device B (offline)"]
B0["entry b0"]
end
A1 -- "signed chain" --> M{{"mergeLedgers"}}
B0 -- "signed chain" --> M
M --> O["one ordered log"]
M --> V{{"verifyAndMergeLedgers"}}
V --> OK["0 conflicts: clean"]
V --> C["typed conflicts:<br/>divergent_id<br/>broken_chain<br/>invalid_signature"]The 60-second version
- Identify Each device generates a keypair with
generateDeviceKey; its id is the SHA-256 of its public key. - Append & sign
appendEntrychains each record off the previous one;signEntrysigns the same bytes the hash covers. - Merge
mergeLedgersunions every device's log, dedupes by id, and returns one deterministic order. - Verify
verifyAndMergeLedgersre-checks chains and signatures. Zero conflicts means everything checks out.
When NOT to use this
A narrow tool. Reach for something else when your problem is a different shape.
| You need | Use instead | Why |
|---|---|---|
| Rich collaborative merge (text, trees) without authenticity | Automerge, Yjs | CRDTs merge concurrent edits field by field, but accept whatever a peer sends. No tamper-evidence. |
| A tamper-evident log with a single writer | An append-only signed audit log | If only one party ever writes, you do not need cross-device merge. Simpler. |
| A full peer-to-peer append-only stack (replication, discovery, storage) | Hypercore | Hypercore is a whole P2P system. This library is just the verifiable core, transport-agnostic. |
| A server-side, publicly auditable transparency log | Trillian | Merkle-tree transparency logs assume a central, always-on server. This is offline-first and serverless. |
| Confidentiality (hiding contents) | An encryption layer | Signatures prove authorship, not secrecy. Payloads are plaintext. |
If you want deterministic multi-writer merge and the ability to prove nobody rewrote history, and you are offline-first, this is the fit.
See a byte flip break verification.
Two devices, real WebCrypto, entirely in your browser. Sign, merge, then tamper.