Search selfstore
v1.8.21

selfstore vs Yjs

TL;DR: Not rivals, and the pairing is the point: Yjs merges concurrent edits losslessly, selfstore gives that document an encrypted home the user owns and no server. Storing Yjs updates in a selfstore file is a supported, tested pattern.

What Yjs is

The CRDT that won. A high-performance implementation of shared types - text, arrays, maps - that merge without a coordinator and without losing a keystroke, under the hardest case there is: two people typing in the same paragraph at the same moment. It is what sits under a great many collaborative editors, and if your problem is concurrent editing, nothing here competes with it.

What Yjs deliberately does not decide is everything around the document. Persistence is a provider you choose (y-indexeddb), transport is another (y-websocket, and a server to run it), and encryption at rest, a backup the user can hold, a destination they own and the screens to connect one are simply not in scope. That is a healthy design. It also means a Yjs app still has the whole storage question in front of it.

What selfstore is

That question, answered, with no server: an IndexedDB working copy, portable encrypted ZIP backups on an open spec, durable homes on storage the user already has, and deterministic multi-device merge.

Its own merge is last-write-wins, which is honestly the wrong answer for a paragraph two people are typing in. So do not ask it to be: keep Yjs for the document, and let selfstore carry it.

They compose, and here is why that works

selfstore merges binary files by a union on their id. Yjs updates are commutative and idempotent - apply them in any order, apply one twice, you land on the same document. Store each update under the id of its own bytes, and the union is the CRDT merge: no device’s update is lost when the copies meet, and folding them is the whole read path.

const doc = new Y.Doc();
const fold = () => {
  for (const f of store.allFiles())
    if (f.mime === UPDATE_MIME) Y.applyUpdate(doc, f.bytes, FOLDED);
};
fold();                 // what this device holds
store.onChange(fold);   // and what arrives from another
doc.on('update', (u, origin) => {
  if (origin !== FOLDED) void store.putFile({ bytes: u, mime: UPDATE_MIME });
});

putFile defaults a file’s id to the SHA-256 of its bytes, which is what makes this safe: a single stable id for the whole document would put two devices’ different bodies under one key, and a union has no clock to order them - one would be dropped in silence. The library refuses that by default.

Yjs selfstore
Solves Concurrent edits, losslessly Durability, portability, destinations
Merge Real CRDT Last-write-wins per record
Live collaboration Yes, with a server No
Local persistence A provider you add Built in
Encryption Yours to build Built in, AES-256-GCM + Argon2id
User-holdable backup Yours to build First-class, spec’d ZIP

Use Yjs alone when

Use both when

The complete integration - compaction pass and its honest limit included - is examples/yjs-document.ts.