Persistence
NodalMerge persistence is split between node history and blob payloads. This allows you to combine different storage backends without changing sync correctness.Persistence model
At the server layer, persistence is composed from:- Node persistence for room DAG history
- Blob persistence for content-addressed binary payloads
Built-in persistence modes
No persistence (default)
Without--store, server uses in-memory persistence only.
- Fast startup
- No restart durability
- Suitable for local dev and ephemeral demos only
Directory persistence (--store)
With --store <root>, server enables durable local storage.
Built-in on-disk layout includes:
<root>/nodalmerge.dbfor persisted nodes<root>/blobs/<sanitized_room>/<hash>for blob files<root>/blob-tombstones/<sanitized_room>/<hash>for GC tombstones- Topology sidecar stores under the same root (when enabled)
On-disk behavior details
Directory persistence uses SQLite plus file blobs. Key durability behaviors:- Node writes use
INSERT OR IGNOREsemantics for idempotent replays - Batch node persist path reduces commit overhead for accepted packs
- Blob writes use write-to-temp then rename pattern
- Blob load verifies hash integrity and skips mismatched payloads
Hydration and runtime recovery
When durable mode is active, room creation hydrates from persisted data:- Load persisted nodes
- Apply to in-memory graph
- Load persisted blobs into memory blob store
- Resume runtime sync with hydrated state
Backend composition model
Persistence surfaces are designed for composition:- Use one backend for nodes
- Use another backend for blobs
- Expose as one combined server persistence surface
- SQL/document store for nodes
- S3/R2/object storage for blobs
Backup strategy
For durable directory persistence, treat the entire store root as backup scope. At minimum, include:- SQLite node database
- Blob object directories
- Blob tombstone metadata
- Topology sidecar stores (if used)
Restore expectations
After restore:- Rooms hydrate from restored persistence state
- Missing/extra data may surface as deterministic replay differences
- Blob integrity checks can reveal corrupted payload files
Data lifecycle and GC interaction
Blob persistence participates in GC lifecycle:- Live-set derived from room references
- Two-phase tombstone -> delete behavior
- Grace windows prevent premature deletion during state churn
Operational checks
For any durable deployment, verify regularly:- Persisted node count increases under write load
- Blob files appear for expected hash references
- Restart recovers known room state
- Blob GC behavior matches configured interval/grace
- Storage write latency metrics stay within SLO
SDK peer-local persistence (browser clients)
The server persistence described above handles server-side durability. Separately, the JavaScript SDK supports optional peer-local graph persistence in browser clients using IndexedDB. This allows clients to hydrate from local storage on page reload without waiting for a full server sync.IndexedDB adapter setup
Off by default. Enable by passingpersistence to createNodalMergeSdk:
initialize(), the SDK:
- Opens the IndexedDB database
- Loads the room’s node pack and blobs into the WASM
SyncStore - Reports hydration via
sdk.persistence.hydrateReport()
roomId (nodes store) and roomId:hash (blobs store), so multiple rooms can safely share one database name.
Outbox persistence (separate from graph persistence)
offline.persistenceKey controls outbox persistence in localStorage — queued writes that haven’t been flushed to the server yet. This is independent of persistence.*:
sdk.offline.outboxDepth() returns the count of unsent ops. sdk.offline.flush() sends them immediately. sdk.offline.clearPersisted() wipes the localStorage queue.
Recovery patterns
After hydration, inspect the report to decide if recovery is needed:sdk.persistence.flush() before page unload or navigation to ensure the latest graph state is written to IndexedDB. Flush is also invoked automatically on sdk.room.disconnect().
Migration from app-managed storage
If your app previously managed its own IndexedDB nodes/blobs storage:- Remove hand-rolled
idbPut('nodes', 'all', …)and blob cursor hydration. - Pass
persistence: { enabled: true }tocreateNodalMergeSdk. - If you stored data under legacy demo key formats (
nodes/all, flat blob hashes), passpersistence: { migrateLegacyDemo: true }during the first release to migrate in place. - Keep
offline.persistenceKeyif you still need disconnected outbox replay.
Custom persistence adapters
ImplementPeerLocalPersistenceAdapter to use a different storage backend (e.g., OPFS, a custom cache):
NODALMERGE_HEADLESS_BACKEND=file or embedded with a mounted data directory — not IndexedDB.
Common pitfalls
- Assuming
--storeis optional for production durability - Backing up only DB but not blobs (or vice versa)
- Enabling room eviction on non-durable persistence
- Treating object-store direct I/O as replacement for persistence correctness
- Ignoring hash mismatch warnings during blob load