bedrock_sync
A backend-agnostic, offline-first sync engine for Flutter.
Write data locally, sync to any backend when online — with built-in conflict resolution.
Features
- 📦 Offline-first — reads and writes work without internet
- 🔌 Backend-agnostic — REST, Firebase, Supabase, or your own adapter
- ⚡ Real-time reactive —
watch()streams update your UI instantly - 🔀 Conflict resolution — LastWriteWins, ServerWins, ClientWins, Merge, or Custom
- 🔁 Auto retry — exponential backoff with configurable max retries
- 🌐 Auto sync — triggers on connectivity restore and/or fixed interval
- 🧩 Fully customizable — swap any component (store, queue, adapter, resolver)
- 📊 Sync status stream — show live sync state in your UI
Getting Started
dependencies:
bedrock_sync: ^0.0.1
Quick Start
import 'package:bedrock_sync/bedrock_sync.dart';
// 1. Initialize the engine
final engine = BedrockEngine(
adapter: RestSyncAdapter(baseUrl: 'https://api.example.com'),
config: SyncConfig(
enableLogging: true,
conflictResolver: LastWriteWinsResolver(),
),
);
await engine.init();
// 2. Write locally (works offline instantly)
await engine.write('todos', {'title': 'Buy milk', 'done': false});
// 3. Read from local store
final todos = await engine.read('todos');
// 4. Watch for real-time changes
engine.watch('todos').listen((todos) {
setState(() => _todos = todos);
});
// 5. Manual sync
final result = await engine.sync();
print('Synced: ${result.syncedCount}');
Conflict Resolvers
| Resolver | Behavior | Best for |
|---|---|---|
LastWriteWinsResolver |
Most recent timestamp wins | Notes, drafts |
ServerWinsResolver |
Server always wins | Financial data |
ClientWinsResolver |
Local always wins | User preferences |
MergeResolver |
Field-level merging | Collaborative docs |
CustomResolver |
Your own logic | Complex rules |
// Custom resolver example
SyncConfig(
conflictResolver: CustomResolver(
resolver: (local, server) async {
// return whichever data you want to keep
return local.data['priority'] > server['priority']
? local.data
: server;
},
),
)
Sync Adapters
REST (built-in)
RestSyncAdapter(
baseUrl: 'https://api.example.com',
defaultHeaders: {'Authorization': 'Bearer $token'},
buildUrl: (collection, id) => '/v2/$collection/${id ?? ''}',
transformRequest: (op) => {'payload': op.data},
transformResponse: (res) => res['data'] as Map<String, dynamic>,
)
Firebase (custom)
class MyFirebaseAdapter extends FirebaseSyncAdapter {
final _firestore = FirebaseFirestore.instance;
@override
Future<SyncAdapterResult> push(SyncOperation op) async {
final ref = _firestore.collection(op.collection);
final doc = await ref.add(op.data);
return SyncAdapterResult.success({'id': doc.id, ...op.data});
}
// ...
}
Custom adapter
class MyGraphQLAdapter extends SyncAdapter {
@override
Future<SyncAdapterResult> push(SyncOperation op) async {
// Send to your GraphQL / WebSocket / gRPC server
}
// ...
}
Custom Local Store
Swap the default SQLite store for Hive, Isar, or anything else:
class HiveLocalStore extends LocalStore {
@override Future<void> init() async { ... }
@override Future<void> save(String collection, String id, Map<String, dynamic> data) async { ... }
// ... implement all methods
}
BedrockEngine(
adapter: myAdapter,
store: HiveLocalStore(), // plug it in here
)
Configuration
SyncConfig(
maxRetries: 3, // retry failed operations 3 times
retryStrategy: RetryStrategy.exponential, // 2s → 4s → 8s
syncTrigger: SyncTrigger.hybrid, // sync on reconnect + every 5min
syncInterval: Duration(minutes: 5),
batchSize: 50, // operations per batch
enableLogging: true, // debug logs
onBeforeSync: () async => true, // gate sync with custom logic
onAfterSync: (count) => print('Synced $count'),
onError: (e, stack) => Sentry.captureException(e),
)
Sync Status in UI
StreamBuilder<SyncStatus>(
stream: engine.statusStream,
builder: (context, snapshot) {
final status = snapshot.data;
return Text(
status?.isOnline == true ? 'Online' : 'Offline',
);
},
)
License
MIT
Libraries
- bedrock_sync
- BedrockSync — A backend-agnostic offline-first sync engine for Flutter.