SyncVault ๐Ÿ”„๐Ÿ—„๏ธ

Stop writing connectivity logic.

SyncVault is an offline-first data synchronization layer for Flutter apps. It automatically queues API requests when the device is offline and syncs them with exponential backoff when the connection returns.

Designed for reliability in low-network environments.

pub package License: MIT

๐Ÿš€ Features

  • Offline Queue: Automatically intercepts failed requests and stores them locally.
  • Auto-Sync: Detects network restoration and processes the queue in order (FIFO).
  • Persistence: Uses a pluggable storage adapter (Hive or SQLite).
  • Idempotency: Supports idempotency keys to prevent duplicate execution.
  • Configurable Retry: Exponential backoff with jitter, configurable max retries.
  • Dead Letter Queue: Failed requests (after max retries) are removed and can be handled via callback.
  • Status Stream: Real-time sync status for UI updates.
  • Event Bus: Track individual action lifecycles (queued โ†’ started โ†’ success/failed).
  • Flutter Widgets: Ready-to-use SyncVaultListener, SyncStatusBuilder, and SyncEventBuilder.

๐Ÿ“ฆ Installation

Add this to your pubspec.yaml:

dependencies:
  sync_vault: ^1.0.0

Then run:

flutter pub get

๐Ÿ›  Usage

1. Initialize

Initialize SyncVault in your main.dart before running the app:

import 'package:sync_vault/sync_vault.dart';
import 'package:path_provider/path_provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Get storage directory (required for Hive)
  final dir = await getApplicationDocumentsDirectory();

  await SyncVault.init(
    config: SyncVaultConfig(
      baseUrl: 'https://api.myapp.com',
      storagePath: dir.path,
      maxRetries: 3,
    ),
  );

  runApp(MyApp());
}

2. Make Offline-Safe Requests

Instead of calling Dio or Http directly, use the SyncVault client:

// POST request
final response = await SyncVault.instance.post(
  '/jobs/complete',
  data: {'jobId': 123, 'status': 'DONE'},
  idempotencyKey: 'job_123_complete', // Prevents duplicate execution
);

if (response.isQueued) {
  print("Saved offline! Will sync later.");
} else if (response.isSent) {
  print("Sent immediately! Status: ${response.response?.statusCode}");
}

// GET, PUT, DELETE, PATCH are also available
await SyncVault.instance.get('/users');
await SyncVault.instance.put('/users/123', data: {...});
await SyncVault.instance.delete('/users/123');
await SyncVault.instance.patch('/users/123', data: {...});

3. Listen to Sync Status

Perfect for showing a "Syncing..." indicator in your UI:

SyncVault.instance.statusStream.listen((status) {
  switch (status) {
    case SyncStatus.syncing:
      showToast("Uploading offline data...");
    case SyncStatus.synced:
      showToast("All caught up!");
    case SyncStatus.pending:
      showToast("${await SyncVault.instance.pendingCount} items waiting");
    case SyncStatus.error:
      showToast("Sync error occurred");
  }
});

4. Track Individual Actions (Event Bus)

Listen to specific job completions โ€” perfect for updating UI when background sync finishes:

// Listen to all events
SyncVault.instance.onEvent.listen((event) {
  print('Action ${event.id}: ${event.status}');

  if (event.isSuccess) {
    // Action completed successfully
    print('Response: ${event.response}');
  } else if (event.isPermanentFailure) {
    // Action failed permanently (dead letter)
    print('Error: ${event.error}');
  }
});

// Filter events for a specific action
SyncVault.instance.eventsFor('action-id').listen((event) {
  // Only events for this action
});

// Filter by idempotency key
SyncVault.instance.eventsForKey('my-idempotency-key').listen((event) {
  // Only events with this key
});

Event Statuses:

Status Description
queued Action was saved to offline queue
started Worker began processing this action
success Server returned 2xx response
failed Action failed (will retry)
deadLetter Action permanently failed after max retries

5. Manual Sync

Trigger sync manually if needed:

final successCount = await SyncVault.instance.processQueue();
print('Synced $successCount items');

6. Check Pending Actions

// Get count
final count = await SyncVault.instance.pendingCount;

// Get all pending actions
final actions = await SyncVault.instance.getPendingActions();

// Clear all pending (use with caution)
await SyncVault.instance.clearQueue();

๐ŸŽจ Flutter Widgets

SyncVault provides ready-to-use widgets for common UI patterns:

SyncVaultListener

Listens to sync events and calls callbacks โ€” wrap your screen to react to background sync:

SyncVaultListener(
  // Called when any action completes successfully
  onSuccess: (event) {
    if (event.id == myActionId) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Synced!')),
      );
    }
  },
  // Called when an action permanently fails
  onDeadLetter: (event) {
    showError('Sync failed: ${event.error}');
  },
  // Optional: filter by action ID or idempotency key
  actionId: 'specific-action-id',
  idempotencyKey: 'specific-key',
  child: YourScreen(),
)

SyncStatusBuilder

Rebuilds automatically when sync status changes:

SyncStatusBuilder(
  builder: (context, status) {
    return Icon(
      status == SyncStatus.syncing
          ? Icons.sync
          : Icons.cloud_done,
      color: status == SyncStatus.error
          ? Colors.red
          : Colors.green,
    );
  },
)

SyncEventBuilder

Rebuilds when events occur for a specific action โ€” great for per-item sync indicators:

SyncEventBuilder(
  actionId: myActionId,
  builder: (context, lastEvent) {
    if (lastEvent == null) {
      return Icon(Icons.cloud_upload); // Queued
    }
    if (lastEvent.status == SyncEventStatus.started) {
      return CircularProgressIndicator(); // Syncing
    }
    if (lastEvent.isSuccess) {
      return Icon(Icons.check, color: Colors.green);
    }
    return Icon(Icons.error, color: Colors.red);
  },
)

๐Ÿ—๏ธ Architecture

SyncVault follows a Store-and-Forward architecture:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Your App      โ”‚
โ”‚   (Request)     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   SyncVault     โ”‚
โ”‚   (Intercept)   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
    โ”‚ Online? โ”‚
    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
         โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
   Yes       No
    โ”‚         โ”‚
    โ–ผ         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Dio  โ”‚ โ”‚ Queue โ”‚
โ”‚ (Send)โ”‚ โ”‚(Store)โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜
              โ”‚
              โ–ผ
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚  Hive  โ”‚
         โ”‚(Persist)โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ”‚
    On Network Restore
              โ”‚
              โ–ผ
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚ Worker โ”‚
         โ”‚(Process)โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  1. Interceptor: Captures outgoing requests via SyncVault.instance.post(), etc.
  2. Queue Manager: Serializes requests to local storage (FIFO order).
  3. Network Watcher: Listens for connectivity changes via connectivity_plus.
  4. Worker: Dequeues requests and retries with exponential backoff.

โš™๏ธ Configuration

SyncVaultConfig(
  // Required
  baseUrl: 'https://api.example.com',

  // Optional: Custom storage path for Hive
  storagePath: '/path/to/storage',

  // Optional: Maximum retry attempts (default: 3)
  maxRetries: 5,

  // Optional: Custom Dio instance
  dio: myCustomDio,

  // Optional: Timeouts
  connectTimeout: Duration(seconds: 30),
  receiveTimeout: Duration(seconds: 30),

  // Optional: Custom storage adapter
  storage: MyCustomStorageAdapter(),
)

๐Ÿ’พ Storage Adapters

SyncVault supports multiple storage backends out of the box:

Hive (Default)

Hive is the default storage adapter - fast, lightweight, and perfect for most use cases.

await SyncVault.init(
  config: SyncVaultConfig(
    baseUrl: 'https://api.example.com',
    storagePath: dir.path, // Required for Hive
  ),
);

SQLite

For apps that prefer relational databases or need more complex queries:

import 'package:sync_vault/sync_vault.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Create SQLite adapter
  final sqliteAdapter = SqfliteStorageAdapter();

  await SyncVault.init(
    config: SyncVaultConfig(
      baseUrl: 'https://api.example.com',
      storage: sqliteAdapter, // Use SQLite instead of Hive
    ),
  );

  runApp(MyApp());
}

You can also specify a custom database path:

final sqliteAdapter = SqfliteStorageAdapter(
  databasePath: '/custom/database/path',
);

๐Ÿ”Œ Custom Storage Adapter

Implement StorageAdapter to use your own persistence layer:

class MyStorageAdapter implements StorageAdapter {
  @override
  Future<void> init() async { ... }

  @override
  Future<void> saveAction(SyncAction action) async { ... }

  @override
  Future<void> removeAction(String id) async { ... }

  @override
  Future<List<SyncAction>> getAllActions() async { ... }

  @override
  Future<void> clear() async { ... }

  @override
  Future<void> dispose() async { ... }

  @override
  Future<int> get count async { ... }
}

๐Ÿงช Testing

SyncVault is designed to be testable. Reset between tests:

tearDown(() async {
  await SyncVault.reset();
});

๐Ÿ“ฑ Example

Check out the example directory for a complete Todo app demonstrating all features.

cd example
flutter run

๐Ÿ”’ Idempotency

Use idempotency keys to prevent duplicate operations when retrying:

await SyncVault.instance.post(
  '/payments',
  data: {'amount': 100},
  idempotencyKey: 'payment_${orderId}_${timestamp}',
);

If the same idempotency key is already in the queue, SyncVault will handle it appropriately.

๐Ÿ“Š Retry Behavior

Scenario Behavior
Network error Retry with backoff
Timeout Retry with backoff
5xx Server error Retry with backoff
429 Rate limited Retry with backoff
4xx Client error Fail permanently
Max retries exceeded Remove (dead letter)

๐Ÿค Contributing

PRs are welcome! Please read the contributing guidelines before submitting.

  1. Fork the repo
  2. Create your feature branch from dev (git checkout -b feature/amazing-feature dev)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request against dev

๐Ÿ‘ค Author

Nabhodipta Garai (@brownboycodes)

"Buy Me A Coffee"

๐Ÿ“„ License

MIT ยฉ 2025 Nabhodipta Garai


Built with โค๏ธ for Flutter developers who work in the real world, where networks are unreliable.

Libraries

sync_vault
SyncVault - An offline-first data synchronization layer for Flutter.