react-native-sync-engine
v0.1.0
Published
Offline-first sync engine for React Native. Read/write locally with zero latency; changes queue while offline and merge conflict-free when the connection returns. Open-core: free client, paid hosted sync.
Maintainers
Readme
React Native Offline Sync
An offline-first sync engine for React Native. Read and write data locally with zero latency; changes queue while offline and merge conflict-free when the connection returns — even if two devices changed the same record.
Open-core: the client library is free (this repo). Pair it with a hosted sync backend + dashboard (the commercial product) for cross-device sync.
Why this exists
Offline support is notoriously painful: local persistence, a change queue, network detection, retry, and — the hard part — merging conflicting edits without losing data. This engine makes it "just work" for field apps, travel, delivery, and anywhere the network is flaky.
The merge is a CRDT (Last-Writer-Wins per field, ordered by a Hybrid Logical Clock). That means merges are commutative, associative, and idempotent — every device converges to the same result regardless of order, with no central locking. (See the runnable proof in How conflict resolution works.)
What's included (free client)
| Piece | What it does |
|---|---|
| SyncStore | Local-first DB: instant reads/writes, persistence, outbox, sync loop |
| useCollection / useDocument | Reactive hooks — re-render on local writes or incoming sync |
| useSyncStatus | { online, pending, lastSyncedAt, syncing } for status UIs |
| NetworkController | Online/offline state (wire to NetInfo, or toggle manually) |
| MemoryStorage / AsyncStorageAdapter | Pluggable persistence |
| InMemorySyncServer | In-process mock backend for demos/tests/local dev |
| HttpSyncTransport | Template for a real HTTP/WebSocket backend |
| mergeDocState, HLC | The CRDT primitives, exposed for advanced use |
Quick start
import {
SyncStore, MemoryStorage, InMemorySyncServer,
SyncProvider, useCollection, useSyncStatus,
} from 'react-native-sync-engine';
const store = new SyncStore({
storage: new MemoryStorage(), // or AsyncStorageAdapter for persistence
transport: new InMemorySyncServer(), // or HttpSyncTransport({ baseUrl })
});
store.init();
function Tasks() {
const todos = useCollection('todos');
const status = useSyncStatus();
return (
<>
<Text>{status.online ? 'Online' : `Offline · ${status.pending} queued`}</Text>
{todos.map(t => <Text key={t.id}>{t.title}</Text>)}
<Button title="Add" onPress={() => store.create('todos', { title: 'New', done: false })} />
</>
);
}
export default () => <SyncProvider store={store}><Tasks /></SyncProvider>;Writes apply instantly and persist locally. When online, they push automatically and pull + merge remote changes. When offline, they queue in the outbox and flush on reconnect.
How conflict resolution works
Merging is per field, ordered by HLC timestamp:
- Two devices edit different fields of a record offline → both edits survive.
- Two devices edit the same field → the later write wins, deterministically on every device.
- Deletes are tombstones, so they merge like any other change.
This is verified by a runnable test (commutativity, LWW, idempotency, tombstones, monotonic clock) — all passing. The guarantee is what lets you sync without a server-side lock or a "pick a winner" dialog.
Phone (offline): rename task → "Buy milk"
Tablet (offline): tick task done ✓
↓ both reconnect ↓
Everyone converges to: "Buy milk", done ✓ ← both edits keptPersistence
// npx expo install @react-native-async-storage/async-storage
import { AsyncStorageAdapter } from 'react-native-sync-engine/async-storage';
const store = new SyncStore({ storage: new AsyncStorageAdapter(), transport });AsyncStorageAdapter is a separate import, so apps using MemoryStorage never
bundle AsyncStorage. Implement StorageAdapter to back it with SQLite/MMKV/etc.
Connecting a real backend (the paid part)
The client talks to any SyncTransport. HttpSyncTransport expects a server
exposing POST /push and GET /pull?cursor=:
import { HttpSyncTransport } from 'react-native-sync-engine';
const store = new SyncStore({
storage: new AsyncStorageAdapter(),
transport: new HttpSyncTransport({ baseUrl: 'https://sync.yourapp.com', authToken }),
});A runnable reference backend (zero dependencies) lives in
server/ — cd server && npm start gives you a working
/push + /pull server on http://localhost:4000 to develop against or
self-host. The hosted backend + web dashboard (manage devices, inspect/repair
data, auth, retention) is the commercial product — that's the open-core business
model: free client drives adoption, paid sync service is the revenue.
Wire up real network detection
// npx expo install @react-native-community/netinfo
import NetInfo from '@react-native-community/netinfo';
NetInfo.addEventListener(s => store.getNetwork().setOnline(!!s.isConnected));Run the demo
example/ is a two-device playground: a 📱 Phone and 💻 Tablet
share one in-memory backend. Toggle each Offline, make conflicting edits to the
same task, toggle back Online, and watch them merge — with a live "server truth"
panel.
cd react-native-sync-engine && npm install
cd example && npm install && npm start # press i / a / wDevelop, build & publish
The package ships compiled JS + type declarations from dist/ (built with
tsc); the TypeScript source lives in src/.
npm install # dev deps (typescript, react, @types/react, async-storage)
npm run typecheck # type-check src/ without emitting
npm test # build, then run the test suite (node --test)
npm run build # emit dist/ (JS, .d.ts, source maps)
npm publish # prepublishOnly re-builds + tests, then publishes dist/The test suite proves the CRDT guarantees the README describes — commutativity,
associativity, idempotency, same-field LWW, different-field survival, tombstones,
a monotonic HLC, and the full offline-queue → reconnect → merge lifecycle through
InMemorySyncServer.
License
The client in this repo is MIT (see LICENSE); the hosted server + dashboard is the separate commercial, open-core product.
