isar_sync
Offline-first synchronization framework for Isar-based apps.
Isar stays your local source of truth. isar_sync records every change as an event, ships those events through a pluggable remote adapter (Google Drive, iCloud, or your own), and applies remote events back into your store — so the same data converges across a user's devices, with no server to run.
Install
dependencies:
isar_sync: ^0.1.0-dev.3
or
dart pub add isar_sync
Quick Start
import 'package:isar_sync/isar_sync.dart';
const deviceId = 'device-A'; // stable per install
final queue = InMemorySyncQueue();
final store = InMemorySyncStore();
final cursorStore = InMemorySyncCursorStore();
final applyEngine = DefaultApplyEngine(
store: store,
conflictResolver: const LastWriteWinsConflictResolver(),
// Drop this device's own events echoed back from shared cloud storage.
// REQUIRED for the Google Drive / iCloud adapters.
localDeviceId: deviceId,
);
// Replace with a real adapter: GoogleDriveSyncAdapter or ICloudSyncAdapter.
final adapter = MySyncAdapter();
final core = SyncCore(
queue: queue,
adapter: adapter,
applyEngine: applyEngine,
cursorStore: cursorStore,
);
// Capture a local change...
final capture = QueueBackedChangeCapture(queue: queue, deviceId: deviceId);
await capture.capture(
SyncMutation(
collection: 'notes',
entityId: 'n1',
operation: SyncOperation.upsert,
payload: {
'title': 'hello',
// A UTC updatedAt is required for correct Last-Write-Wins.
'updatedAt': DateTime.now().toUtc().toIso8601String(),
},
),
);
// ...then run one push + pull + apply cycle.
final report = await core.syncOnce();
print('pushed=${report.pushedCount}, pulled=${report.pulledCount}');
Two conventions are load-bearing: every upsert payload must carry a UTC
updatedAt, andDefaultApplyEngine.localDeviceIdmust be set (and equal to the capturedeviceId) for shared-folder cloud adapters. See doc/usage.md §10.
Flutter companion
For ready-made platform integrations (Google Sign-In, icloud_storage), use the
companion package:
dependencies:
isar_sync: ^0.1.0-dev.3
isar_sync_flutter: ^0.1.0-dev.3
- Companion package: isar_sync_flutter · source
- Companion guide: doc/flutter-companion.md
- Multi-device demo (
isar_community+isar_sync_flutter): examples/notes_multi_device
Adapter & integration guides
- Google Drive setup: doc/google-drive-setup.md
- Apple iCloud setup: doc/icloud-setup.md
- Isar integration: doc/isar-integration.md
- Collection mapping template: doc/collection-mapping-template.md
- End-to-end usage: doc/usage.md
- Feature reference: doc/features.md
Sign in on another device with the same Google account or Apple ID and use
the same namespace, and the two devices share events through that remote
store.
Why
Isar is an excellent local database, but as an app grows the gaps show:
- no multi-device sync,
- no change log,
- collaboration/sharing is hard to build.
Teams usually end up adding a backend (more complexity), hand-rolling sync (low reuse, high maintenance), or running servers they didn't want. The core problem:
Local-first is great, but the next step is missing.
isar_sync does not replace Isar. It keeps Isar as-is and adds sync on top: start with no server, extend only when needed, and change as little existing code as possible.
Conceptual model
- State vs Change — Isar holds the current state; isar_sync records the changes. That separation is what makes debugging, reprocessing, and re-syncing possible.
- Event-driven — every mutation is captured as an event: state change → event → sync.
- Eventually consistent — the goal is convergence, not instantaneous agreement.
End-to-end flow
- A local change happens (user edits data).
- Change capture — the Isar write and the
SyncEventare created in one transaction boundary, so data and event are atomic. - The event is queued in a
SyncQueue(not yet on the remote). - Push —
SyncQueue→ adapter → remote storage. - Pull — remote storage → adapter → incoming events.
- Events are de-duplicated and ordered by timestamp.
- Apply — events are applied to the store (upsert/delete), with conflicts
resolved by the
ConflictResolver. - UI updates via Isar's
watch.
flowchart LR
U[User action] --> W[Isar write]
W --> C[Change capture]
C --> E[SyncEvent]
E --> Q[SyncQueue]
Q --> P[Push]
P --> A1[Adapter]
A1 --> R[(Remote storage)]
R --> A2[Adapter]
A2 --> PL[Pull]
PL --> F[Dedup / order]
F --> AP[Apply engine]
AP --> I[Isar update]
I --> UI[UI refresh]
Components
| Component | Responsibility |
|---|---|
SyncCore |
Orchestrates one push → pull → apply cycle (syncOnce()) |
QueueBackedChangeCapture |
Turns local mutations into queued SyncEvents |
SyncQueue |
Outbound queue with retry/backoff (InMemorySyncQueue, FileSyncQueue) |
SyncAdapter |
Abstracts the remote store (GoogleDriveSyncAdapter, ICloudSyncAdapter) |
ApplyEngine |
Applies remote events to local state |
ConflictResolver |
Conflict policy (default LastWriteWinsConflictResolver) |
SyncCursorStore |
Persists the per-adapter pull cursor (FileSyncCursorStore) |
Design principles
- Simplicity first — an understandable pipeline over complex CRDTs.
- Predictability — explicit overwrite rules and apply ordering.
- Debuggability — an event log you can trace and reprocess.
- Incremental — v1 is simple sync; advanced policies/optimizations come later.
Scope
In v1: the event pipeline (capture → queue → push → pull → apply), extensible
ports (SyncAdapter, SyncStore, ConflictResolver, SyncCursorStore),
default Last-Write-Wins resolution, and explicit syncOnce() execution.
Not in v1: fully automatic real-time sync, CRDT-based concurrent editing, global total ordering, and backend-standardized auth/permissions.
Status
Early preview (0.1.0-dev). The API is still evolving.
Docs
Publishing guide: doc/publish-to-pub.md · Version history: CHANGELOG.md