isar_sync 0.1.0-dev.3
isar_sync: ^0.1.0-dev.3 copied to clipboard
Offline-first, event-sourced sync for Isar apps: capture local changes and converge them across devices through pluggable cloud adapters (Drive, iCloud).
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