Skip to content

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-sdk

This 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:

  1. CBOR-encodes the content
  2. SHA-256 hashes it → that's the document's hash
  3. 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→hash

The 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

Next steps ​

  • Documents — types, fields, and the document lifecycle
  • Querying — find, filter, sort
  • Editing — update operators, save, delete
  • P2P Sync — sync protocol, signals, conflict resolution
  • Rendering — render handlers, defaults, your own database