Appearance
Getting Started
Document Store is a content-addressed document database that syncs peer-to-peer. Every document gets a hash based on its content. Edits create new versions, forming a chain back to the original. This makes sync simple — you either have a hash or you don't.
Where it fits
What changed: writes go through Document Store instead of straight to the DB. What didn't: your frontend, your app code, and your familiar database — and the queries you've already written. What disappears: "your servers" — the whole stack runs on the user's device, with sync replacing the call to a backend.
Install
sh
npm install @wishcore/wish-sdkThis includes document-store and its dependencies.
Quick example
A complete app that stores notes and syncs them between peers:
typescript
import { RpcApp, SyncProtocol } from '@wishcore/wish-sdk';
import { createStore } from 'document-store';
import Database from 'better-sqlite3';
// 1. Your rendered DB + the store. The render handler keeps your
// table in lockstep with every accepted write (local or synced).
const db = new Database('./data/rendered.db');
db.exec(`CREATE TABLE IF NOT EXISTS note (
hash BLOB PRIMARY KEY, title TEXT, content TEXT, created INTEGER
)`);
const store = await createStore({
storage: './data/ds.sqlite', // the store's canonical log
renderHandler: async (ops) => {
for (const op of ops) {
if (op.docType !== 'note') continue;
if (op.type === 'upsert') {
const c = op.content as any;
db.prepare(`INSERT OR REPLACE INTO note
(hash, title, content, created) VALUES (?, ?, ?, ?)`)
.run(op.hash, c.title, c.content, c.createdAt);
} else if (op.type === 'delete') {
db.prepare('DELETE FROM note WHERE hash = ?').run(op.hash);
}
}
},
});
// 2. Register document types
await store.registerType('note');
// 3. Set up P2P sync
const sync = new SyncProtocol({
store,
onSync: () => console.log('Synced documents from peer')
});
// 4. Connect to wish-core (auto-detects Unix socket)
const app = new RpcApp({
name: 'MyApp',
protocols: { myapp: sync }
});
await app.whenReady;
// 5. Write through document-store
const uid = app.identities[0].uid;
const [errors, hash] = await store.add('note', {
uid,
title: 'Hello',
content: 'First note',
share: { self: true }
});
// 6. Read directly from YOUR database
const notes = db.prepare('SELECT title, content FROM note ORDER BY created DESC').all();That's a working P2P app. Documents sync automatically when peers connect.
SurrealDB works too
Prefer SurrealDB for rendered state? Use the provided handler: renderHandler: createSurrealRenderHandler(db) — see Rendering for both patterns and the handler contract.
Architecture: writes vs reads
All writes go through document-store — add(), edit(), save(), delete(). The store handles hashing, versioning, validation, signatures, and rendering the latest state into your database.
All reads go directly to your database. Document-store renders each document type into a table (SQLite) or collection (MongoDB). Query them with your database's native tools — SQL queries, indexes, joins, whatever you need.
Writes: App → DocumentStore → [hash, validate, sign, render] → Database
Reads: App → Database (direct)This split is intentional. Writing a full query abstraction across SQLite, MongoDB, and SurrealDB is a massive undertaking — every index, lookup pattern, and aggregation works differently. The persistence layer for writes is simple (insert/update rendered docs). But reads need the full power of your database, and you know your query patterns better than any abstraction can guess.
How it works
Content addressing
When you add a document, the store:
- CBOR-encodes the content
- SHA-256 hashes it → that's the document's
hash - Stores both the rendered document (for queries) and the raw CBOR (for sync)
Version chain
Edits don't modify the original. They create new documents with prev and root pointers:
[original] ← [edit 1] ← [edit 2] ← [edit 3]
hash prev→hash prev→edit1 prev→edit2
root→hash root→hash root→hashThe store maintains a rendered view — the latest state with all edits applied. You query the rendered view; sync transfers the full chain.
Sync
The SyncProtocol from wish-sdk handles everything:
- When a peer connects, they exchange document hashes and timestamps
- Missing documents are fetched and injected into the local store
- When you add or edit locally, call
sync.notifyPeers()to push changes
Rendering — your database
The store renders into your database through a render handler: a fifteen-line function for the common SQLite case, a provided helper for SurrealDB (createSurrealRenderHandler), or your own for any database you know. See Rendering — the contract is four operation types, and it's the same boundary every language binding uses.
Prerequisites
Document Store runs standalone for local storage. For P2P sync, you need:
- A running wish-core instance (Wish SDK docs)
- An identity granted to your app via the Wish Dashboard