# API: sync and merge Source: https://selfstore.dev/docs/api-sync Complete reference for selfstore/sync - SyncConfig and the five merge strategies, the HLC metadata, merge, detectConflicts and changes. The merge engine as a pure function you can run in a test. The merge engine, exposed as pure functions. The narrative version is [multi-device sync](https://selfstore.dev/docs/sync) and [how sync works](https://selfstore.dev/docs/how-sync-works); this page is the surface. Most apps only ever touch `SyncConfig`, through [`SimpleOptions.sync`](https://selfstore.dev/docs/api-store). The rest is here because the engine being callable is what makes its behaviour testable rather than magic. ## SyncConfig ```ts interface SyncConfig { idField?: string; // default: 'id' ids?: Record; // per-collection id field strategies?: Record; // per-collection strategy fallback?: MergeStrategy; } ``` ```ts const store = await selfstore('app', { sync: { ids: { people: 'uuid' }, strategies: { tags: 'grow-set', settings: 'lww-map' } } }); ``` ## The five strategies | Strategy | Behaviour | Use it for | | --- | --- | --- | | `lww-set` | A set of records; the later edit of a record wins, deletes propagate | The default. Lists of things. | | `lww-map` | Field-level last-write-wins **within** a record | Documents two devices edit in different fields | | `grow-set` | Records are added, never removed by merge | Append-only logs, tags | | `lww-register` | The whole collection is one value; the later write replaces it | Single-value settings | | `manual` | No automatic merge; conflicts are surfaced, nothing is chosen | Data where losing a side is unacceptable | `lww-map` is the one worth knowing about. Under `lww-set`, two devices editing different fields of the same record means one edit is dropped - the later record wins whole. Under `lww-map`, both survive, because the clock is kept per field (`ColMeta.fields`). ## Metadata ```ts type Hlc = string; // hybrid logical clock type Id = string; interface SyncMeta { node: string; clock: Hlc | null; cols: Record; } interface ColMeta { clocks: Record; deleted: Record; hashes: Record; fields?: Record>; // lww-map only } ``` `deleted` is why a removal propagates instead of resurrecting: a delete carries a clock, so a device that never saw the record still knows the deletion is newer than its copy. There is no CRDT runtime here. The clocks travel **inside the backup file**, which is why sync needs no server. | Function | Signature | | --- | --- | | `createNode` | `() => string` | | `createMeta` | `(node?: string) => SyncMeta` | | `stamp` | `(meta, collections, config, wallNow?) => SyncMeta` | ## Merging ```ts interface ReplicaState { collections: Record; meta: SyncMeta; } interface MergeResult { collections: Record; meta: SyncMeta; conflicts: Conflict[]; } interface Conflict { collection: string; id: Id; local?: unknown; remote?: unknown; kept: 'local' | 'remote'; } ``` | Function | Signature | Notes | | --- | --- | --- | | `merge` | `(a, b, config, base?) => MergeResult` | Deterministic: same inputs, same output, on any device | | `detectConflicts` | `(a, b, config, base?) => Conflict[]` | The same detection without applying anything | | `changes` | `(before, after, config) => Record` | What a merge actually moved | ```ts interface CollectionChanges { added: number; updated: number; removed: number; } ``` **A conflict is journaled, not hidden.** `kept` says which side won, and both values ride along, so an app can show what was overwritten or offer to restore it. Auto-resolution without a record is how local-first data quietly loses edits; see [sync](https://selfstore.dev/docs/sync). `detectConflicts` is the one to reach for when you want to warn **before** merging - a "this will overwrite 3 records" confirmation. ## Determinism, and how to test it `merge` is pure. Two devices merging the same pair in the opposite order reach the same state, which is what makes serverless sync safe: ```ts import { merge, createMeta, createNode } from 'selfstore/sync'; const cfg = { fallback: 'lww-set' as const }; const ab = merge(a, b, cfg); const ba = merge(b, a, cfg); expect(ab.collections).toEqual(ba.collections); ``` That property is the whole reason there is no server: convergence is a consequence of the function, not of an authority. See [testing](https://selfstore.dev/docs/testing) for wiring this into your own suite. Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt