dynos_sync 0.1.5 copy "dynos_sync: ^0.1.5" to clipboard
dynos_sync: ^0.1.5 copied to clipboard

🛡️ Production-hardened, high-performance sync engine for Dart & Flutter. Built for 50k+ writes/sec, delta-pulls, and zero-jank offline-first reliability.

dynos.fit logo

dynos_sync #

Production-grade offline-first sync engine for Dart & Flutter. Built by the team at dynos.fit to power high-concurrency workout synchronization.

Pub.dev Pub Points MIT License Security Audit: 130 Tests Performance: 50k+ writes/sec


What it does #

dynos_sync sits between your local database and remote backend. Every write goes to the local DB first, gets queued for sync, and drains to the server when connectivity allows. On launch, it delta-pulls only what changed. Conflicts are resolved automatically.

Flutter App  -->  SyncEngine  -->  Local DB (Drift/SQLite)
                      |
                      +--->  Sync Queue  -->  Remote API (Supabase/REST)

Database and backend agnostic. Plug in any local store, any remote API, any queue backend via clean interfaces.


Features #

Category What you get
Offline-first writes write() persists locally + queues for sync in one atomic call
Delta sync pullAll() compares timestamps, only fetches tables with changes
Conflict resolution Last-write-wins, server-wins, client-wins, or custom callback
Runtime table registration addTable() / removeTable() after engine is running
PII redaction Sensitive fields masked with [REDACTED] before storage and push
Row-Level Security Local RLS gate blocks writes with mismatched user_id
Exponential backoff Failed pushes retry with 2^n delays, capped at maxBackoff
Poison pill isolation Permanently failing entries are dropped after maxRetries
Batch push drain() pushes up to batchSize entries per cycle
Auth expiry handling AuthExpiredException emits SyncAuthRequired event, stops drain
Cross-user isolation logout() wipes queue, local data, and timestamps
Event stream Broadcast stream of typed events for UI/logging integration
Background isolate IsolateSyncEngine wraps sync in a dedicated isolate
Payload size limits Rejects payloads exceeding maxPayloadBytes
Drain lock Prevents concurrent drain operations

Installation #

dependencies:
  dynos_sync: ^0.1.2

Quick start #

1. Create the engine #

final sync = SyncEngine(
  local: DriftLocalStore(db),
  remote: SupabaseRemoteStore(client: client, userId: () => uid),
  queue: DriftQueueStore(db),
  timestamps: DriftTimestampStore(db),
  tables: ['tasks', 'notes'],
  userId: uid,
  config: const SyncConfig(
    batchSize: 50,
    sensitiveFields: ['password', 'ssn'],
    conflictStrategy: ConflictStrategy.lastWriteWins,
  ),
);

2. Write data #

await sync.write('tasks', id, {
  'id': id,
  'title': 'Buy milk',
  'updated_at': DateTime.now().toUtc().toIso8601String(),
});

3. Sync on app launch #

await sync.syncAll();          // drain pending + pull remote changes
await sync.initialSyncDone;    // wait in splash screen

4. Add tables at runtime #

await sync.addTable('categories');              // pulls immediately
await sync.addTable('tags', pull: false);       // register only
sync.removeTable('deprecated_table');           // stop syncing

5. Logout #

await sync.logout();  // wipes queue, local data, timestamps

API reference #

SyncEngine #

Method Description
write(table, id, data) Write locally + queue for sync
remove(table, id) Delete locally + queue deletion
push(table, id, data, {operation}) Queue sync without local write (for custom DAOs)
drain() Push all pending queue entries to remote
pullAll() Delta-pull changes from remote for all registered tables
syncAll() Full cycle: drain() then pullAll()
addTable(table, {pull}) Register a new table at runtime, optionally pull immediately
removeTable(table) Unregister a table from sync
logout() Wipe all local sync state (queue, data, timestamps)
dispose() Close the event stream
Property Type Description
tables List<String> Currently registered tables (unmodifiable)
events Stream<SyncEvent> Broadcast stream of sync lifecycle events
isDraining bool Whether drain() is currently executing
initialSyncDone Future<void> Completes after first syncAll() finishes
userId String? Current authenticated user for RLS checks
config SyncConfig Engine configuration

SyncConfig #

Parameter Default Description
batchSize 50 Max entries to drain per cycle
queueRetention 30 days How long to keep synced entries before purging
stopOnFirstError true Stop drain on first failure vs skip and continue
maxRetries 3 Retries before dropping a poison pill entry
sensitiveFields [] Field names to mask with [REDACTED]
useExponentialBackoff true Enable 2^n retry delays
conflictStrategy lastWriteWins How to resolve local vs remote conflicts
onConflict null Custom conflict resolver (required when strategy is custom)
maxPayloadBytes 1 MB Max payload size before rejection
maxBackoff 60s Cap on exponential backoff duration

Events #

Event Fields When
SyncDrainComplete timestamp After drain() finishes
SyncPullComplete timestamp, table, rowCount After pulling a table
SyncAuthRequired timestamp, error Auth token expired during sync
SyncConflict timestamp, table, recordId, localVersion, remoteVersion, resolvedVersion, strategyUsed Conflict resolved
SyncPoisonPill timestamp, entry Entry permanently dropped
SyncRetryScheduled timestamp, entry, nextRetryAt Retry scheduled with backoff
SyncError timestamp, error, context Non-fatal error during sync

Exceptions #

Exception When
AuthExpiredException Remote returns 401/403
PayloadTooLargeException Payload exceeds maxPayloadBytes
RlsViolationException Pulled row has wrong user_id
SyncRemoteException Remote returns an error response
SyncDeserializationException Failed to parse remote response

Store interfaces #

Implement these to plug in your database and backend:

abstract class LocalStore {
  Future<void> upsert(String table, String id, Map<String, dynamic> data);
  Future<void> delete(String table, String id);
  Future<void> clearAll(List<String> tables);
}

abstract class RemoteStore {
  Future<void> push(String table, String id, SyncOperation op, Map<String, dynamic> data);
  Future<void> pushBatch(List<SyncEntry> entries);  // default: loops push()
  Future<List<Map<String, dynamic>>> pullSince(String table, DateTime since);
  Future<Map<String, DateTime>> getRemoteTimestamps();
}

abstract class QueueStore { /* enqueue, getPending, markSynced, ... */ }
abstract class TimestampStore { /* get, set */ }

Bundled adapters: DriftLocalStore, DriftQueueStore, DriftTimestampStore, SupabaseRemoteStore.


Performance #

Benchmarked with in-memory stores on standard hardware:

Operation 10k records 100k records
Bulk write ~130ms ~1,400ms
Queue drain ~2ms ~3ms
Delta pull < 1ms ~3ms
PII masking overhead ~4ms ~44ms

Security #

The engine passes a 130-test security audit across 12 categories:

  • HIPAA & health data compliance (PHI masking, audit trail, session timeout)
  • Cross-user data isolation (full wipe on logout)
  • Injection prevention (SQL, NoSQL, XSS, path traversal stored as literals)
  • Auth & RLS enforcement (local user_id gate, auth expiry events)
  • Conflict resolution integrity (deterministic, all strategies tested)
  • Flood resilience (100k parallel writes, drain lock)
  • OWASP Mobile Top 10 coverage
  • GDPR compliance (right to erasure, data minimization)

See Security Audit Report for details.


Documentation #


License #

MIT -- see LICENSE.

Built by the dynos.fit team.

1
likes
160
points
207
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

🛡️ Production-hardened, high-performance sync engine for Dart & Flutter. Built for 50k+ writes/sec, delta-pulls, and zero-jank offline-first reliability.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#sync #offline-first #database #supabase #high-performance

License

MIT (license)

Dependencies

drift, meta, supabase, uuid

More

Packages that depend on dynos_sync