offline_sync_engine 2.2.0 copy "offline_sync_engine: ^2.2.0" to clipboard
offline_sync_engine: ^2.2.0 copied to clipboard

Offline-first CRDT-based sync engine with automatic conflict resolution. Seamlessly sync data across multiple devices with built-in implementations for quick start.

Offline Sync Engine #

Pub Version License: MIT

Offline-first CRDT-based sync engine for Flutter and Dart applications. Automatically synchronize data across multiple devices with deterministic conflict resolution.

Features #

  • ๐Ÿ”„ Automatic Sync - Seamlessly sync data between local database and cloud
  • ๐Ÿ“ด Offline-First - Work offline, sync when connection returns
  • ๐Ÿ”€ Conflict Resolution - CRDT-based deterministic merge (no conflicts!)
  • ๐Ÿ“ฑ Multi-Device - Same user, multiple devices, always in sync
  • ๐ŸŽฏ Operation-Based - Efficient operation tracking and replay
  • ๐Ÿ”ข Vector Clocks - Precise causality tracking across devices
  • โšก Idempotent - Safe to replay operations
  • ๐Ÿงฉ Modular - Bring your own database and cloud backend
  • โœ… Type Safe - Full Dart type safety
  • ๐Ÿงช Well Tested - Comprehensive test coverage

Installation #

Add to your pubspec.yaml:

dependencies:
  offline_sync_engine: ^2.0.0

Then:

dart pub get

or

flutter pub get

Quick Start #

1. Using Built-in Implementations (for testing/demos) #

import 'package:offline_sync_engine/offline_sync_engine.dart';

void main() async {
  // Create sync manager with in-memory implementations
  final syncManager = SyncManager(
    database: InMemoryDatabaseAdapter(),
    cloud: InMemoryCloudAdapter(),
    deviceId: 'user_device_123',
  );

  // Create or update data
  await syncManager.createOrUpdate('user_profile', {
    'name': 'John Doe',
    'email': 'john@example.com',
  });

  // Sync with cloud
  await syncManager.sync();
}

2. Using Custom Implementations (for production) #

import 'package:offline_sync_engine/offline_sync_engine.dart';

// Implement DatabaseAdapter for your local database
class MySQLiteDatabaseAdapter implements DatabaseAdapter {
  // Your SQLite implementation
  // See example/custom_adapters_example.dart for full example
}

// Implement CloudAdapter for your backend
class MyRestCloudAdapter implements CloudAdapter {
  // Your REST API implementation
  // See example/custom_adapters_example.dart for full example
}

void main() async {
  final syncManager = SyncManager(
    database: MySQLiteDatabaseAdapter(),
    cloud: MyRestCloudAdapter(),
    deviceId: await getDeviceId(),
  );

  await syncManager.createOrUpdate('note', {'title': 'My Note'});
  await syncManager.sync();
}

Core Concepts #

Adapters #

The sync engine requires two adapters:

  1. DatabaseAdapter - Connects to your local database (SQLite, Hive, etc.)
  2. CloudAdapter - Connects to your cloud backend (REST API, Firebase, etc.)

Built-in implementations are provided for testing:

  • InMemoryDatabaseAdapter - In-memory local storage
  • InMemoryCloudAdapter - In-memory cloud simulation

Sync Manager #

The SyncManager is the main interface:

final manager = SyncManager(
  database: myDatabaseAdapter,
  cloud: myCloudAdapter,
  deviceId: 'unique_device_id',
);

// Create or update
await manager.createOrUpdate(recordId, data);

// Delete
await manager.delete(recordId);

// Sync
await manager.sync();

// Check sync status
bool syncing = manager.isSyncing;

Conflict Resolution #

The engine uses CRDT (Conflict-free Replicated Data Type) with vector clocks:

  • Causally ordered updates - Later updates override earlier ones
  • Concurrent updates - Automatically merged (all fields preserved)
  • Deterministic - Same result regardless of sync order
  • No user intervention needed - Conflicts resolved automatically

Example:

// Device 1 (offline): Updates name
await device1.createOrUpdate('user', {'name': 'Alice', 'age': 25});

// Device 2 (offline): Updates city
await device2.createOrUpdate('user', {'name': 'Bob', 'city': 'NYC'});

// After sync, both devices converge to:
// {'name': 'Bob', 'age': 25, 'city': 'NYC'}
// (Last write wins per field, all fields preserved)

Architecture #

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        Your App                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                     โ”‚
                     โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     SyncManager                             โ”‚
โ”‚  - createOrUpdate()                                         โ”‚
โ”‚  - delete()                                                 โ”‚
โ”‚  - sync()                                                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
             โ”‚                        โ”‚
             โ–ผ                        โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   DatabaseAdapter      โ”‚  โ”‚    CloudAdapter        โ”‚
โ”‚  (Your Implementation) โ”‚  โ”‚  (Your Implementation) โ”‚
โ”‚                        โ”‚  โ”‚                        โ”‚
โ”‚  - SQLite              โ”‚  โ”‚  - REST API            โ”‚
โ”‚  - Hive                โ”‚  โ”‚  - Firebase            โ”‚
โ”‚  - SharedPreferences   โ”‚  โ”‚  - Supabase            โ”‚
โ”‚  - ObjectBox           โ”‚  โ”‚  - GraphQL             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Examples #

The example/ folder contains comprehensive examples:

Basic Usage #

cd example
dart run main.dart

Multi-Device Sync #

dart run multi_device_example.dart

Delete Operations #

dart run delete_example.dart

Custom Adapters #

dart run custom_adapters_example.dart

See example/README.md for detailed explanations.

Implementing Custom Adapters #

DatabaseAdapter #

Connect your local database:

class MySQLiteAdapter implements DatabaseAdapter {
  final Database db;

  MySQLiteAdapter(this.db);

  @override
  Future<void> saveOperation(SyncOperation operation) async {
    await db.insert('operations', operation.toJson());
  }

  @override
  Future<List<SyncOperation>> getUnsentOperations() async {
    final results = await db.query(
      'operations',
      where: 'sent = ?',
      whereArgs: [0],
    );
    return results.map((row) => SyncOperation.fromJson(row)).toList();
  }

  @override
  Future<void> markOperationSent(String opId) async {
    await db.update(
      'operations',
      {'sent': 1},
      where: 'opId = ?',
      whereArgs: [opId],
    );
  }

  @override
  Future<bool> isApplied(String opId) async {
    final result = await db.query(
      'applied_ops',
      where: 'opId = ?',
      whereArgs: [opId],
    );
    return result.isNotEmpty;
  }

  @override
  Future<void> applyOperation(SyncOperation operation) async {
    // Mark as applied
    await db.insert('applied_ops', {'opId': operation.opId});

    // Apply to records table
    final existing = await getRecord(operation.recordId);
    final newRecord = SyncRecord(
      id: operation.recordId,
      data: operation.payload ?? {},
      version: operation.version,
      tombstone: operation.isDelete,
    );

    if (existing == null) {
      await db.insert('records', {
        'id': newRecord.id,
        'data': jsonEncode(newRecord.data),
        'version': jsonEncode(newRecord.version.toJson()),
        'tombstone': newRecord.tombstone ? 1 : 0,
      });
    } else {
      final merged = existing.merge(newRecord);
      await db.update(
        'records',
        {
          'data': jsonEncode(merged.data),
          'version': jsonEncode(merged.version.toJson()),
          'tombstone': merged.tombstone ? 1 : 0,
        },
        where: 'id = ?',
        whereArgs: [merged.id],
      );
    }
  }

  @override
  Future<SyncRecord?> getRecord(String id) async {
    final results = await db.query(
      'records',
      where: 'id = ?',
      whereArgs: [id],
    );

    if (results.isEmpty) return null;

    final row = results.first;
    return SyncRecord(
      id: row['id'] as String,
      data: jsonDecode(row['data'] as String),
      version: VersionTracker.fromJson(jsonDecode(row['version'] as String)),
      tombstone: row['tombstone'] == 1,
    );
  }
}

CloudAdapter #

Connect your backend API:

class MyRestCloudAdapter implements CloudAdapter {
  final String baseUrl;
  final http.Client client;

  MyRestCloudAdapter(this.baseUrl, {http.Client? client})
      : client = client ?? http.Client();

  @override
  Future<void> push(List<SyncOperation> operations) async {
    final response = await client.post(
      Uri.parse('$baseUrl/sync/push'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(operations.map((op) => op.toJson()).toList()),
    );

    if (response.statusCode != 200) {
      throw Exception('Failed to push operations');
    }
  }

  @override
  Future<List<SyncOperation>> pull() async {
    final response = await client.get(
      Uri.parse('$baseUrl/sync/pull'),
    );

    if (response.statusCode != 200) {
      throw Exception('Failed to pull operations');
    }

    final data = jsonDecode(response.body) as List;
    return data.map((json) => SyncOperation.fromJson(json)).toList();
  }
}

Testing #

Run tests:

dart test

With coverage:

dart test --coverage=coverage
dart pub global activate coverage
dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib

See test/README.md for testing guide.

API Reference #

SyncManager #

Main sync engine class.

Constructor:

SyncManager({
  required DatabaseAdapter database,
  required CloudAdapter cloud,
  required String deviceId,
})

Methods:

  • Future<void> createOrUpdate(String recordId, Map<String, dynamic> data) - Create or update a record
  • Future<void> delete(String recordId) - Delete a record (tombstone)
  • Future<void> sync() - Synchronize with cloud
  • bool get isSyncing - Check if sync is in progress

DatabaseAdapter #

Abstract interface for local database. Implement this for your database.

Methods:

  • Future<void> saveOperation(SyncOperation operation)
  • Future<List<SyncOperation>> getUnsentOperations()
  • Future<void> markOperationSent(String opId)
  • Future<bool> isApplied(String opId)
  • Future<void> applyOperation(SyncOperation operation)
  • Future<SyncRecord?> getRecord(String id)

CloudAdapter #

Abstract interface for cloud backend. Implement this for your API.

Methods:

  • Future<void> push(List<SyncOperation> operations)
  • Future<List<SyncOperation>> pull()

Models #

  • SyncOperation - Represents a single create/update/delete operation
  • SyncRecord - Represents a synchronized data record
  • VersionTracker - Tracks versions across devices (vector clock)

Use Cases #

  • ๐Ÿ“ Note-taking apps - Sync notes across devices
  • โœ… Todo apps - Collaborative task lists
  • ๐Ÿ’ฐ Expense trackers - Multi-device budget tracking
  • ๐Ÿ“Š CRM apps - Offline-capable customer data
  • ๐Ÿฅ Healthcare apps - Patient records sync
  • ๐ŸŽฎ Games - Cross-platform game state sync
  • ๐Ÿ›’ E-commerce - Offline shopping cart
  • ๐Ÿ“ฑ Any offline-first app - Build apps that work offline

Best Practices #

  1. Unique Device IDs - Use UUID or device-specific identifiers
  2. Periodic Sync - Call sync() periodically (e.g., every 30s when online)
  3. Background Sync - Use workmanager or background_fetch for periodic sync
  4. Handle Tombstones - Filter out records where tombstone == true
  5. Operation Batching - Sync batches efficiently (handled automatically)
  6. Error Handling - Wrap sync calls in try-catch for network errors
  7. Testing - Use InMemory* implementations for unit tests

Performance #

  • Efficient - Only syncs operations since last sync
  • Scalable - Handles thousands of operations
  • Lightweight - Minimal memory footprint
  • Fast - Optimized merge algorithms

Benchmarks (on typical devices):

  • 1000 operations: < 500ms
  • Merge 100 records: < 50ms
  • Sync 50 operations: < 1s (network dependent)

Limitations #

  • Field-level LWW - Last write wins per field (merge strategy)
  • No Time Travel - Cannot rollback to previous versions (add versioning layer if needed)
  • No Partial Sync - Syncs all operations (add filtering if needed)
  • Memory-based - Operations kept in memory during sync (optimize for very large datasets)

Roadmap #

  • โŒ Delta sync (only sync changed data)
  • โŒ Compression for large payloads
  • โŒ Pluggable merge strategies
  • โŒ Schema versioning
  • โŒ Encryption support
  • โŒ Real-time sync with WebSockets
  • โŒ Flutter web support optimizations

Contributing #

Contributions welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new features
  4. Ensure all tests pass
  5. Submit a pull request

License #

MIT License - see LICENSE file for details.

Copyright (c) 2026

Support #

Changelog #

See CHANGELOG.md for version history.

Acknowledgments #

This package implements CRDT (Conflict-free Replicated Data Type) concepts inspired by academic research and production systems like:

  • Amazon DynamoDB
  • Apache Cassandra
  • Riak
  • CouchDB
  • sqflite - SQLite for Flutter
  • hive - Fast NoSQL database
  • firebase_database - Firebase Realtime Database
  • supabase - Open source Firebase alternative

Made with โค๏ธ for the Flutter & Dart community

6
likes
0
points
398
downloads

Publisher

unverified uploader

Weekly Downloads

Offline-first CRDT-based sync engine with automatic conflict resolution. Seamlessly sync data across multiple devices with built-in implementations for quick start.

Repository (GitHub)
View/report issues

Topics

#offline #sync #crdt #database #multi-device

License

unknown (license)

Dependencies

meta

More

Packages that depend on offline_sync_engine