> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nodalmerge.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Persistence

> Configure and operate NodalMerge persistence safely across in-memory, local durable, and composed backend deployments.

# 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

Durability is treated as the combined property of both halves for operational decisions such as safe room eviction.

## 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.db` for 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 IGNORE` semantics 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

Room IDs are sanitized for filesystem-safe blob/tombstone paths.

## Hydration and runtime recovery

When durable mode is active, room creation hydrates from persisted data:

1. Load persisted nodes
2. Apply to in-memory graph
3. Load persisted blobs into memory blob store
4. Resume runtime sync with hydrated state

This allows restart recovery while preserving deterministic replay semantics.

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

This is useful for patterns like:

* SQL/document store for nodes
* S3/R2/object storage for blobs

The architecture keeps transport and merge semantics stable regardless of backend pairing.

## 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)

Backup policy should align with your replay/audit requirements, not only “latest state” expectations.

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

Always run post-restore validation with known test rooms before serving production traffic.

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

Do not treat GC as a storage-only concern; it is tightly coupled to room reachability semantics.

## 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 passing `persistence` to `createNodalMergeSdk`:

```ts theme={null}
const sdk = await createNodalMergeSdk({
  wsUrl: "ws://your-host/ws/runtime",
  roomId: "my-room",
  persistence: {
    enabled: true,
    adapter: "indexeddb",
    dbName: "nodalmerge-peer-local",  // default
    dbVersion: 1,                      // default
    debounceMs: 500                    // default auto-save interval
  }
});

// Hydration runs during initialize(), before connect().
await sdk.room.connect();
```

On `initialize()`, the SDK:

1. Opens the IndexedDB database
2. Loads the room's node pack and blobs into the WASM `SyncStore`
3. Reports hydration via `sdk.persistence.hydrateReport()`

Room data is scoped by `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.*`:

```ts theme={null}
const sdk = await createNodalMergeSdk({
  wsUrl: "...",
  roomId: "my-room",
  offline: {
    persistenceKey: "nodalmerge:my-room:outbox"
  }
});
```

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

```ts theme={null}
const report = sdk.persistence.hydrateReport();
// { roomId, nodesPack, blobsRestored, canonicalHash }

if (!report || !report.canonicalHash) {
  // No local state — will sync fresh from server
}
```

If local persistence is corrupt or mismatched:

```ts theme={null}
// Re-hydrate from the last good persisted snapshot
const freshReport = await sdk.persistence.recover();

// Wipe all local state for this room and start clean
await sdk.persistence.clearRoom();
```

Call `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:

1. Remove hand-rolled `idbPut('nodes', 'all', …)` and blob cursor hydration.
2. Pass `persistence: { enabled: true }` to `createNodalMergeSdk`.
3. If you stored data under legacy demo key formats (`nodes/all`, flat blob hashes), pass `persistence: { migrateLegacyDemo: true }` during the first release to migrate in place.
4. Keep `offline.persistenceKey` if you still need disconnected outbox replay.

### Custom persistence adapters

Implement `PeerLocalPersistenceAdapter` to use a different storage backend (e.g., OPFS, a custom cache):

```ts theme={null}
import { createNodalMergeSdk, PeerLocalPersistenceAdapter } from "nodalmerge-sdk-js";

const myAdapter: PeerLocalPersistenceAdapter = {
  kind: "my-storage",
  isAvailable: () => true,
  open: async () => { /* open connection */ },
  hydrate: async (store, roomId) => { /* load and return report */ },
  schedulePersist: (store, roomId) => { /* debounce a save */ },
  flush: async (store, roomId) => { /* immediate save */ },
  recover: async (store, roomId) => { /* reload from last good state */ },
  clearRoom: async (roomId) => { /* wipe room data */ }
};

const sdk = await createNodalMergeSdk({
  wsUrl: "...",
  roomId: "my-room",
  persistence: { enabled: true, adapter: myAdapter }
});
```

For server-side pod equivalents, use `NODALMERGE_HEADLESS_BACKEND=file` or `embedded` with a mounted data directory — not IndexedDB.

## Common pitfalls

* Assuming `--store` is 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

## Related pages

* [operators/server-setup](/operators/server-setup)
* [operators/gc-and-lifecycle](/operators/gc-and-lifecycle)
* [architecture/storage-and-blobs](/architecture/storage-and-blobs)
* [operators/deployment-topologies](/operators/deployment-topologies)
* [sdk/javascript](/sdk/javascript)
