Offline-first synchronization engine for Drift-backed Dart and Flutter apps. Pure Dart core; pluggable transport.
Three-phase reconciliation between a local Drift database and a remote server:
- Upload pending local changes (
create/update/delete). - Reconcile client_ids — assign mobile-generated
client_ids to server rows that lack them (e.g. rows created by another client). - Download deltas — incremental fetch via a
synced_sincecursor.
You provide:
- A Drift database that implements
SynchronizerDb. - One
SyncTypeHandlerper entity type (with optional REST adapter). - A dependency graph for entity ordering.
The orchestrator does the rest.
@DriftDatabase(tables: [Wallets, LocalChanges, SyncMetadata])
class AppDatabase extends _$AppDatabase with SynchronizerDb {
AppDatabase(super.executor);
// Implement getPendingLocalChanges, insertLocalChange,
// concludeLocalChange, getLocalSyncMetadata,
// updateEntityLocalSyncMetadata against your Drift tables.
// See example/ for a full implementation.
}class WalletSyncHandler extends SyncTypeHandler<Wallet, String, int>
with RestSyncTypeHandler<Wallet, String, int>,
Claimable<Wallet, String, int> {
WalletSyncHandler(this.db, this.api);
final AppDatabase db;
final WalletApi api;
@override String get entityType => 'wallet';
@override String getClientId(Wallet w) => w.clientId;
@override int? getServerId(Wallet w) => w.id;
@override DateTime? getLastSyncedAt(Wallet w) => w.lastSyncedAt;
@override String getRev(Wallet w) => w.rev ?? '1';
// ... fetch / put / delete / persist methods
}class AppSync extends DriftSynchronizer<AppDatabase> {
AppSync({
required super.appDatabase,
required super.typeHandlers,
required super.dependencyManager,
required super.requestAuthorizationService,
});
}
final sync = AppSync(
appDatabase: db,
typeHandlers: {WalletSyncHandler(db, api), /* ... */},
dependencyManager: SyncDependencyManager(),
requestAuthorizationService: AuthService(),
);
await sync.sync();Each entity has an EntitySyncState:
NeverSynced— initial.Healthy(lastSync, cursor)— fully in sync.Degraded(deferred, failed, ...)— synced, some items stuck.FailedSyncState— permanent error, won't retry automatically.
Read via db.getEntitySyncState(entityType); surface to UI for sync banners.
Handlers return a typed PersistOutcome from
persistLocal. The orchestrator reads the cursor from the outcome — never
recomputes from input — so cursor advance is always exactly aligned with
what was actually written.
Pass any SyncLogger to the synchronizer constructor. Default delegates
to the bundled DriftSyncLogger, which routes through optional crash
reporting. Override to integrate Sentry, Crashlytics, or anything else.
final sync = AppSync(
// ...
logger: SentryLogger(),
);Verify your SynchronizerDb implementation with the bundled contract suite:
import 'package:test/test.dart';
import 'package:drift/native.dart';
import 'package:drift_sync_core/testing.dart';
void main() {
runSynchronizerDbContractTests(
makeDb: () async => AppDatabase(NativeDatabase.memory()),
closeDb: (db) async => (db as AppDatabase).close(),
);
}Failures here indicate a contract violation that will manifest as sync flakiness in production.
- ✅ Pure Dart core (no Flutter dependency).
- ✅ Pluggable transport (REST adapter included).
- ✅ Pluggable logger.
- ✅ Typed outcomes and sync state.
⚠️ Pre-1.0 API — minor releases may break.
See LICENSE.