Production-ready offline-first synchronization engine for React Native with WatermelonDB.
- Offline-First Architecture - Write locally first, sync in background
- Automatic Sync - Background sync with configurable intervals
- Network Detection - Auto-sync on reconnection
- Conflict Resolution - Multiple strategies (Last-Write-Wins, Server-Wins, Client-Wins, Custom)
- Retry Logic - Exponential backoff for failed operations
- Type-Safe - Full TypeScript support
- Observable - Reactive sync status updates
- Optimistic UI - Instant user feedback
- React Hooks - Easy integration with React Native apps
- UI Components - Pre-built sync status indicators
- Battle-Tested - Extracted from production apps
npm install @loonylabs/react-native-offline-syncnpm install @nozbe/watermelondb @react-native-community/netinfo @react-native-async-storage/async-storageimport { appSchema } from '@nozbe/watermelondb';
import { syncQueueTableSchema, createTableSchemaWithSync } from '@loonylabs/react-native-offline-sync';
const schema = appSchema({
version: 1,
tables: [
// Add sync queue table
syncQueueTableSchema,
// Your tables with sync metadata
createTableSchemaWithSync('posts', [
{ name: 'title', type: 'string' },
{ name: 'content', type: 'string' },
]),
],
});import { BaseModel } from '@loonylabs/react-native-offline-sync';
import { text } from '@nozbe/watermelondb/decorators';
class Post extends BaseModel {
static table = 'posts';
@text('title') title!: string;
@text('content') content!: string;
}import { SyncEngine } from '@loonylabs/react-native-offline-sync';
import { database } from './database';
// Create API client
const apiClient = {
push: async (payload) => {
const response = await fetch('https://api.example.com/sync/push', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return response.json();
},
pull: async (payload) => {
const response = await fetch('https://api.example.com/sync/pull', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return response.json();
},
};
// Initialize sync engine
const syncEngine = new SyncEngine({
database,
tables: ['posts'],
apiClient,
syncInterval: 5 * 60 * 1000, // 5 minutes
conflictStrategy: 'last-write-wins',
});
await syncEngine.initialize();import { useSyncEngine, OfflineBanner } from '@loonylabs/react-native-offline-sync';
function App() {
const { sync, syncStatus, pendingChanges, isSyncing } = useSyncEngine(syncEngine);
return (
<View>
<OfflineBanner networkDetector={syncEngine.getNetworkDetector()} />
<Button onPress={sync} disabled={isSyncing}>
{isSyncing ? 'Syncing...' : `Sync (${pendingChanges} pending)`}
</Button>
</View>
);
}Main orchestrator for all sync operations.
const syncEngine = new SyncEngine({
database: Database, // WatermelonDB instance
tables: string[], // Tables to sync
apiClient: ApiClient, // API client for server communication
conflictStrategy?: ConflictStrategy, // Default: 'last-write-wins'
syncInterval?: number, // Default: 300000 (5 min)
maxRetries?: number, // Default: 3
enableBackgroundSync?: boolean, // Default: true
syncOnReconnect?: boolean, // Default: true
pushBatchSize?: number, // Default: 50
debug?: boolean, // Default: false
});
await syncEngine.initialize();
await syncEngine.sync();
syncEngine.destroy();Access sync engine state and operations.
const {
sync, // () => Promise<SyncResult>
syncStatus, // 'idle' | 'syncing' | 'error'
lastSyncAt, // number | null
pendingChanges, // number
error, // Error | null
isSyncing, // boolean
} = useSyncEngine(syncEngine);Monitor network connectivity.
const {
isOnline, // boolean
isConnected, // boolean
isInternetReachable, // boolean | null
type, // string | null
} = useNetworkStatus(networkDetector);Perform optimistic UI updates.
const { execute, isOptimistic } = useOptimisticUpdate(database, syncEngine);
const createPost = async (data) => {
return execute('posts', 'CREATE', async (collection) => {
return await collection.create((post) => {
post.title = data.title;
post.content = data.content;
});
});
};Visual indicator of sync status.
<SyncStatusBadge syncEngine={syncEngine} />Banner shown when device is offline.
<OfflineBanner
networkDetector={networkDetector}
message="You are offline"
/>Pull-to-refresh with sync.
<ScrollView
refreshControl={<SyncRefreshControl syncEngine={syncEngine} />}
>
{/* content */}
</ScrollView>- Last-Write-Wins (default): Most recent timestamp wins
- Server-Wins: Server data always takes precedence
- Client-Wins: Local data always takes precedence
- Custom: Provide your own resolution function
const syncEngine = new SyncEngine({
// ... other config
conflictStrategy: 'custom',
customConflictResolver: (context) => {
// context: { tableName, recordId, localData, serverData, localUpdatedAt, serverUpdatedAt }
// Return 'local', 'server', or merged data object
return {
...context.serverData,
localField: context.localData.localField, // Keep local value
};
},
});Your backend needs to implement two endpoints:
Receives local changes to apply on server.
Request:
{
"changes": [
{
"tableName": "posts",
"operation": "CREATE",
"recordId": "local-id-123",
"data": { "title": "Hello", "content": "World" }
}
]
}Response:
{
"success": true,
"results": [
{
"recordId": "local-id-123",
"serverId": "server-id-456",
"serverUpdatedAt": 1234567890,
"error": null
}
]
}Returns server changes since last sync.
Request:
{
"lastSyncAt": 1234567890,
"tables": ["posts"]
}Response:
{
"timestamp": 1234567900,
"changes": {
"posts": {
"created": [{ "id": "1", "title": "New Post" }],
"updated": [{ "id": "2", "title": "Updated Post" }],
"deleted": ["3"]
}
}
}- Batch Operations: Use
pushBatchSizeto control batch sizes - Sync Interval: Adjust based on your app's needs
- Tables: Only sync tables that need it
- Network Detection: Disable if not needed
- Debug Mode: Disable in production
- Requirements & Conventions - Required schema setup, soft deletes, API client
- Error Handling Guide - Error types, recovery strategies, best practices
- API Reference - Complete API documentation
Contributions are welcome! Please feel free to submit a Pull Request.
MIT © Loonylabs
Built with: