Offline-first CRDT-based sync engine for Flutter and Dart applications. It keeps data synchronized across devices with deterministic conflict resolution.
We welcome developers of all levels to contribute! Whether you're looking to:
- π Report bugs or suggest features
- π Improve documentation
- π§ Add custom adapters
- β¨ Enhance the codebase
- π― Share your use cases
Open an Issue or Submit a PR to help make this package even better for everyone!
- π Automatic Sync - Push local operations and pull remote operations.
- π΄ Offline-First - Create/update/delete records without network.
- π Deterministic Merge - Concurrent updates converge predictably.
- π± Multi-Device - Same account on multiple devices stays in sync.
- π― Operation-Based - Sync is based on replayable operations.
- π’ Vector Clocks - Causality tracking across devices.
- β‘ Idempotent Apply - Safe against duplicate operation delivery.
- π§© Adapter-Based - Plug in your own database and cloud backend.
- β Type Safe - Null-safe Dart API.
The package now supports a Bloc-like mental model for teams that prefer event/state flows with minimal boilerplate.
| Bloc Concept | Package Equivalent |
|---|---|
| Event | SyncEvent<T> |
| State | SyncState |
| Bloc | SyncController<T> |
| Repository | SyncRepository<T> |
| Data source | LocalDataSource<T> + CloudDataSource<T> |
| Queue store | SyncLogStore |
abstract class LocalDataSource<T> {
Future<void> insert(T data);
Future<void> update(T data);
Future<void> delete(String id);
Future<T?> getById(String id);
Future<List<T>> getAll();
}
abstract class CloudDataSource<T> {
Future<void> create(T data);
Future<void> update(T data);
Future<void> delete(String id);
Future<T?> fetch(String id);
Future<List<T>> fetchAll();
}enum SyncOperationType { create, update, delete }
enum SyncStatus { pending, syncing, synced, failed }Each local write appends a SyncLog, then StartSync replays pending logs using latest-first ordering.
AddData<T>, UpdateData<T>, DeleteData<T>, StartSync<T>, RetryFailed<T>
SyncInitial, SyncInProgress, SyncSuccess, SyncFailurefinal repository = SyncRepository<User>(
local: localDataSource,
cloud: cloudDataSource,
logStore: InMemorySyncLogStore(),
idResolver: (user) => user.id,
);
final controller = SyncController<User>(repository: repository);
final engine = OfflineSyncEngine<User>(controller: controller);
await engine.add(user);
await engine.sync();UI
β
OfflineSyncEngine
β
SyncController
β
SyncRepository
β
LocalDataSource CloudDataSource
β
SyncLogStore
dart pub add offline_sync_engineor
flutter pub add offline_sync_engineYour pubspec.yaml:
dependencies:
offline_sync_engine: ^latest_versionThen run:
dart pub getor
flutter pub getimport 'package:offline_sync_engine/offline_sync_engine.dart';
void main() async {
final manager = SyncManager(
database: InMemoryDatabaseAdapter(),
cloud: InMemoryCloudAdapter(),
deviceId: 'device_123',
);
// Local write (works offline)
await manager.createOrUpdate('user_1', {
'name': 'John',
'email': 'john@example.com',
});
// Push + pull sync
await manager.sync();
}You provide two adapters:
DatabaseAdapter: local persistence (SQLite, Hive, Isar, etc.)CloudAdapter: backend transport (REST/Firebase/Supabase/etc.)
For demos/tests, use built-ins:
InMemoryDatabaseAdapterInMemoryCloudAdapter
SyncManager is the main entry point:
final manager = SyncManager(
database: myDatabaseAdapter,
cloud: myCloudAdapter,
deviceId: 'unique_device_id',
);
await manager.createOrUpdate('note_1', {'title': 'hello'});
await manager.delete('note_1');
await manager.sync();
final syncing = manager.isSyncing;The engine uses vector clocks with deterministic merge:
- If one version dominates another, dominant record wins.
- If versions are concurrent, fields are merged deterministically.
- Merge result is stable and commutative for concurrent records.
Before publishing your app with custom adapters:
- Ensure
saveOperation+applyOperationare durable. - Keep operation IDs unique and indexed.
- Make
isAppliedfast (index/table/set). - Use retries/backoff for cloud calls.
- Add authentication/authorization at transport layer.
- Add pagination/incremental pull strategy on backend.
- Add monitoring for sync failures.
cd example
dart run main.dart
dart run multi_device_example.dart
dart run delete_example.dart
dart run custom_adapters_example.dartMore details: example/README.md
saveOperationshould persist operation before app crash risk.getUnsentOperationsshould return unsent operations reliably.markOperationSentshould be idempotent.isAppliedshould be idempotency source-of-truth.applyOperationmust be deterministic and safe to replay.
pushshould accept duplicate deliveries safely.pullcan return already-seen operations; manager handles dedupe viaisApplied.- Server ordering should be stable where possible.
MIT License - see LICENSE.