syncdrift_flutter 0.1.1 copy "syncdrift_flutter: ^0.1.1" to clipboard
syncdrift_flutter: ^0.1.1 copied to clipboard

A modern offline-first reactive database framework built on top of Drift. Ships with SWR caching, offline sync queue, real-time push, and file storage — in one package.

pub version pub version Dart SDK Flutter MIT License PRs Welcome

SyncDrift Flutter

Production-grade offline-first reactive database framework for Flutter, built on Drift.
One package. Zero compromise. Full offline–online parity.


SyncDrift is an opinionated, decorator-based persistence layer that wraps Drift/SQLite with:

  • Stale-While-Revalidate (SWR) caching — sub-millisecond reads, background refresh
  • Transactional offline queue — writes survive network loss, sync when back online
  • Real-time inbound push — Supabase Realtime / WebSocket changes applied locally with loop-prevention
  • File/media storage — local cache + offline upload queue with exponential backoff
  • Code generation — type-safe repositories, relationship loaders, and Riverpod providers from annotated Drift tables

All in one import. No separate sub-packages to juggle.


Table of Contents #


Platform Support #

Platform Status
Android ✅ Supported
iOS ✅ Supported
macOS ✅ Supported
Linux ✅ Supported
Windows ✅ Supported
Web ⚠️ SQLite via WASM (no sqlite3_flutter_libs)

Packages #

This repository is a Melos-managed monorepo. End-users only need the two packages below:

Package Version Description
syncdrift_flutter ^0.1.0 Core runtime: caching, sync, realtime, storage
syncdrift_generator ^0.1.0 Build-time code generator (dev dependency)
syncdrift_annotations transitive Annotations consumed by the generator (auto-resolved)

Installation #

Add the following to your app's pubspec.yaml:

dependencies:
  syncdrift_flutter: ^0.1.0

dev_dependencies:
  build_runner: ^2.4.9
  drift_dev: ^2.20.0
  syncdrift_generator: ^0.1.0

Then run:

flutter pub get

Note: syncdrift_annotations is a transitive dependency of syncdrift_flutter and does not need to be added manually.


Architecture #

SyncDrift uses a decorator chain pattern. Each driver layer wraps the one below it, adding behaviour without modifying the core SQLite layer:

┌──────────────────────────────────────────────────────────┐
│               Your Flutter UI / Riverpod Widgets          │
└───────────────────────┬──────────────────────────────────┘
                        │ reads & writes via generated repos
            ┌───────────▼───────────┐
            │  CachedDatabaseDriver │  ← SWR cache, instant reads, TTL eviction
            └───────────┬───────────┘
            ┌───────────▼───────────┐
            │   SyncDatabaseDriver  │  ← intercepts writes → SQLite FIFO queue
            └───────────┬───────────┘
            ┌───────────▼───────────┐
            │  DriftDatabaseDriver  │  ← raw SQLite via Drift
            └───────────────────────┘
               │ outbound                  │ inbound
               ▼                          ▼
    SyncQueueProcessor            RealtimeSyncManager
    ├── RestSyncAdapter           ├── SupabaseRealtimeAdapter
    └── SupabaseSyncAdapter       └── WebSocketRealtimeAdapter
               │                          │
               ▼                          ▼
          Remote API                 Remote API
                          ▲
              SyncdriftStorageManager
              ├── RestStorageAdapter
              └── SupabaseStorageAdapter

Key design principle: The runWithoutQueue() zone ensures real-time inbound writes are applied locally without re-triggering the outbound sync queue — preventing infinite feedback loops.


Quick Start #

1. Annotate Your Tables #

Use SyncDrift's annotations on your existing Drift table definitions:

import 'package:drift/drift.dart';
import 'package:syncdrift_flutter/syncdrift_flutter.dart';

@Repository()
@Cached(ttlSeconds: 60)        // Enable SWR caching with 60-second TTL
@SyncTable()                   // Intercept writes for remote sync
@HasMany(Posts, foreignKey: 'userId')
class Users extends Table {
  IntColumn get id   => integer().autoIncrement()();
  TextColumn get name => text()();
  TextColumn get email => text().withLength(max: 255)();
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

@Repository()
@Cached(ttlSeconds: 30)
@SyncTable()
@BelongsTo(Users, foreignKey: 'userId')
class Posts extends Table {
  IntColumn get id      => integer().autoIncrement()();
  IntColumn get userId  => integer().references(Users, #id)();
  TextColumn get title  => text().withLength(max: 200)();
  TextColumn get body   => text()();
  DateTimeColumn get updatedAt => dateTime().nullable()();
}

2. Run Code Generation #

dart run build_runner build --delete-conflicting-outputs

This emits database.syncdrift.dart alongside your Drift-generated file, providing:

Generated Symbol Type Description
UserRepository class Full CRUD + pagination + reactive watchAll() / watchOne()
PostRepository class Includes loadUsers() relationship loader
syncdriftCacheConfigurations Map<String, Duration> TTL map passed to CachedDatabaseDriver
syncdriftSyncTables Set<String> Table names passed to SyncDatabaseDriver
syncdriftDriverProvider Provider<DatabaseDriver> Riverpod provider for DI
userRepositoryProvider Provider<UserRepository> Auto-generated per-table provider

Watch mode (during development):

dart run build_runner watch --delete-conflicting-outputs

3. Wire Up in main.dart #

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:drift/native.dart';
import 'package:syncdrift_flutter/syncdrift_flutter.dart';
import 'database.dart';
import 'database.syncdrift.dart';

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

  // ─── Layer 1: Core SQLite via Drift ──────────────────────────────────────
  final db        = AppDatabase(NativeDatabase.createInBackground(File('app.db')));
  final rawDriver = DriftDatabaseDriver(db);

  // ─── Layer 2: Outbound sync (REST or Supabase) ───────────────────────────
  final syncAdapter = SupabaseSyncAdapter(supabase: Supabase.instance.client);
  final syncDriver  = SyncDatabaseDriver(
    delegate:   rawDriver,
    syncAdapter: syncAdapter,
    syncTables:  syncdriftSyncTables,   // generated
  );
  await syncDriver.init();              // creates pending_operations tables

  // ─── Layer 3: SWR caching ────────────────────────────────────────────────
  final cachedDriver = CachedDatabaseDriver(
    delegate:         syncDriver,
    cacheManager:     SyncdriftCacheManager(),
    cacheConfigurations: syncdriftCacheConfigurations,  // generated
  );

  // ─── Inbound: Real-time push sync ────────────────────────────────────────
  final realtimeManager = RealtimeSyncManager(
    dbDriver: syncDriver,   // writes bypass outbound queue (loop prevention)
    adapters: [
      SupabaseRealtimeAdapter(supabase: Supabase.instance.client),
      // WebSocketRealtimeAdapter(url: 'wss://api.example.com/ws'),
    ],
  );
  await realtimeManager.start();

  // ─── File / media storage sync ───────────────────────────────────────────
  final storageManager = SyncdriftStorageManager(
    dbDriver:       rawDriver,
    storageAdapter: SupabaseStorageAdapter(supabase: Supabase.instance.client),
    maxRetries:     5,
  );
  await storageManager.init();

  // ─── Start background sync processor ─────────────────────────────────────
  final syncProcessor = SyncQueueProcessor(
    dbDriver:    syncDriver,
    syncAdapter: syncAdapter,
  );
  syncProcessor.init();

  runApp(
    ProviderScope(
      overrides: [
        // Inject the fully decorated driver into the Riverpod graph
        syncdriftDriverProvider.overrideWith((ref) => cachedDriver),
      ],
      child: const MyApp(),
    ),
  );
}

4. Use in Widgets #

// Reading data — reactive, cache-aware
class UsersScreen extends ConsumerWidget {
  const UsersScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userRepo = ref.watch(userRepositoryProvider);

    return StreamBuilder<List<Map<String, dynamic>>>(
      stream: userRepo.watchAll(orderBy: 'created_at DESC'),
      builder: (context, snapshot) {
        if (!snapshot.hasData) return const CircularProgressIndicator();
        final users = snapshot.requireData;

        return ListView.builder(
          itemCount: users.length,
          itemBuilder: (_, i) => ListTile(
            title:    Text(users[i]['name'] as String),
            subtitle: Text(users[i]['email'] as String),
          ),
        );
      },
    );
  }
}

// Writing data — auto-queued for remote sync when offline
Future<void> createUser(UserRepository repo, String name, String email) async {
  await repo.create({'name': name, 'email': email});
  // ↑ Writes to SQLite instantly. If online → syncs immediately.
  //   If offline → queued in pending_operations, drains on reconnect.
}

Feature Deep Dive #

SWR Caching #

SyncDrift implements the Stale-While-Revalidate caching strategy popularized by HTTP RFC 5861:

  1. Cache hit (fresh): Returns cached data immediately. No DB round-trip.
  2. Cache hit (stale): Returns stale data immediately and fires a background refresh.
  3. Cache miss: Fetches from SQLite, populates cache, returns result.
// TTL configured per-table via @Cached annotation
@Cached(ttlSeconds: 60)  // Queries stale after 60 s
class Users extends Table { ... }

// DevTools inspection at runtime
// Register the VM Service extension and open Flutter DevTools → Extensions
// Dart: ext.syncdrift.cache.inspect  — view all cache entries
// Dart: ext.syncdrift.cache.clear    — flush cache

Benchmark (500 reads on a mid-range device):

Mode Time
Raw SQLite ~350 ms
SyncDrift cache hit ~50 ms
Speedup ~7×

Cache is automatically invalidated on any insert/update/delete to that table.


Offline Outbound Sync #

Any write to a @SyncTable-annotated table is intercepted and enqueued atomically in SQLite before hitting the network:

INSERT users → SyncDatabaseDriver
  ├── delegate.insert(...)      // write to SQLite ✓
  └── pending_operations.insert // enqueue operation ✓
        └── SyncQueueProcessor (background)
              ├── [online]  → call SyncAdapter.sync() → DELETE from queue
              └── [offline] → wait for connectivity_plus event → retry

Retry strategy: Exponential backoff — 2^retries seconds (1 s, 2 s, 4 s, 8 s, …). After maxRetries, the operation is moved to failed_operations for inspection.

// Implement your own backend by extending SyncAdapter
class MyApiSyncAdapter implements SyncAdapter {
  @override
  Future<void> sync(String table, String operation, Map<String, dynamic> payload) async {
    // Map to your REST/GraphQL/gRPC calls
  }
}

Built-in adapters:

Adapter Backend
RestSyncAdapter Any HTTP REST API (multipart / JSON)
SupabaseSyncAdapter Supabase PostgREST

Real-time Push Sync #

Inbound changes from the server are applied locally without re-triggering the outbound queue:

// Zone-bound loop prevention — the secret sauce
await SyncDatabaseDriver.runWithoutQueue(() async {
  await dbDriver.insert(table, record);  // applied locally only
});

Built-in adapters:

Adapter Protocol
SupabaseRealtimeAdapter PostgreSQL logical replication via Supabase Realtime
WebSocketRealtimeAdapter Generic WebSocket push feed

Custom adapters implement the RealtimeAdapter abstract class:

abstract class RealtimeAdapter {
  Stream<RealtimeEvent> get events;
  Future<void> connect();
  Future<void> disconnect();
}

Media & File Storage #

// Fetch file — returns local path instantly if cached, downloads if not
final avatarPath = await storageManager.getFile('avatars', 'user_42.jpg');

// Queue upload — file is copied locally immediately; uploaded when online
await storageManager.queueUpload(
  'avatars',
  'user_42.jpg',
  pickedFile.path,
  contentType: 'image/jpeg',
);

// Manually drain the upload queue
await storageManager.triggerUploadQueue();

// Reset backoff timer (e.g. after user retries manually)
storageManager.resetBackoff();

Built-in storage adapters:

Adapter Backend
RestStorageAdapter Generic multipart HTTP (POST/PUT)
SupabaseStorageAdapter Supabase Storage buckets

API Reference #

Full API documentation is available at pub.dev/documentation/syncdrift_flutter.

Core Classes #

Class Description
DatabaseDriver Abstract interface implemented by all driver layers
DriftDatabaseDriver Adapts any GeneratedDatabase to DatabaseDriver
CachedDatabaseDriver SWR cache decorator
SyncdriftCacheManager In-memory TTL cache store
SyncDatabaseDriver Outbound queue interceptor decorator
SyncQueueProcessor Background queue worker
SyncAdapter Abstract sync backend contract
RestSyncAdapter HTTP REST sync adapter
SupabaseSyncAdapter Supabase sync adapter
RealtimeSyncManager Inbound real-time update applier
RealtimeAdapter Abstract realtime source contract
SupabaseRealtimeAdapter Supabase Realtime adapter
WebSocketRealtimeAdapter WebSocket push adapter
SyncdriftStorageManager File cache + offline upload queue
StorageAdapter Abstract storage backend contract
RestStorageAdapter HTTP multipart storage adapter
SupabaseStorageAdapter Supabase Storage adapter
SyncdriftRepository Base class for all generated repositories
PaginatedResult<T> Offset-paginated result with metadata
TerminalSyncException Thrown when a sync operation must not be retried

Annotations #

Annotation Target Description
@Repository() Table Generate a typed repository class
@Cached(ttlSeconds: n) Table Enable SWR caching with TTL
@SyncTable() Table Intercept mutations for outbound sync
@HasMany(T, foreignKey) Table Declare one-to-many relationship
@BelongsTo(T, foreignKey) Table Declare many-to-one relationship

Configuration #

SyncdriftCacheManager options #

SyncdriftCacheManager(
  cleanupInterval: const Duration(minutes: 5),  // default: 5 min
  maxEntries: 500,                               // default: unlimited
)

SyncQueueProcessor options #

SyncQueueProcessor(
  dbDriver:    syncDriver,
  syncAdapter: syncAdapter,
  maxRetries:  5,           // default: 5 — after this → failed_operations
  connectivity: Connectivity(),
)

SyncdriftStorageManager options #

SyncdriftStorageManager(
  dbDriver:       rawDriver,
  storageAdapter: adapter,
  maxRetries:     5,
  customCacheDir: '/path/to/cache',   // default: getApplicationDocumentsDirectory()
)

Versioning #

This project follows Semantic Versioning 2.0.0.

Version Status
0.x.x Current — API stabilisation phase. Minor breaking changes possible between 0.x releases.
1.0.0 Planned — stable API guarantee, full migration guide provided

Pre-1.0 policy: While in 0.x, minor-version bumps (0.x.0) may introduce breaking changes. These are always documented in CHANGELOG.md with a BREAKING label and a migration path. Patch bumps (0.x.y) are backwards-compatible bug fixes only.

See CHANGELOG.md for the full release history.


Contributing #

Contributions are welcome! Please read the guidelines before opening a PR.

  1. Fork the repository on GitHub
  2. Clone your fork locally
  3. Install Melos: dart pub global activate melos
  4. Bootstrap the workspace: dart run melos bootstrap
  5. Create a branch: git checkout -b feat/your-feature
  6. Make changes — ensure all tests pass:
    dart run melos exec -- flutter test
    dart run melos exec -- flutter analyze
    
  7. Open a Pull Request against the main branch

Reporting Issues #

Please use GitHub Issues and include:

  • SyncDrift version (syncdrift_flutter: x.y.z)
  • Flutter version (flutter --version)
  • Minimal reproducible example
  • Full stack trace if applicable

Security #

If you discover a security vulnerability, do not open a public issue. Instead, please email the maintainer directly (see GitHub profile) or use GitHub's private vulnerability reporting.


License #

SyncDrift Flutter is distributed under the MIT License.
See LICENSE for the full text.


Made with ❤️ by Farhan Choksi

1
likes
160
points
20
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A modern offline-first reactive database framework built on top of Drift. Ships with SWR caching, offline sync queue, real-time push, and file storage — in one package.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

connectivity_plus, drift, flutter, http, http_parser, path, path_provider, riverpod, rxdart, sqlite3_flutter_libs, supabase, syncdrift_annotations, web_socket_channel

More

Packages that depend on syncdrift_flutter