Skip to main content
Cindel Sync is an experimental local-first replication layer that lets your app keep working offline while automatically reconciling local changes with a backend you control. You write to typed collections as normal; Cindel records those mutations, pushes them to your backend through a CindelSyncAdapter you implement, pulls remote changes, and applies backend corrections back into the local database — all without any code change to your reads or watchers.

What Sync Is and Is Not

Understanding the boundaries of Cindel Sync will save you time when designing your backend. Sync gives you:
  • Local-first optimistic writes — every write is visible in the local database immediately, before any network round trip.
  • Offline support — local writes still commit and reads return optimistic data while the device is offline.
  • Backend corrections applied locally — if your backend normalizes or corrects a document, Cindel applies that truth locally and watchers fire automatically.
  • A durable outbox — pending mutations survive app close and reopen. When connectivity returns, the scheduler retries automatically.
Sync does NOT provide:
  • A hosted sync server — you supply the backend.
  • Conflict resolution rules — your backend decides who wins.
  • Authentication or access control — your adapter is responsible for auth.
  • Multi-tab coordination on Web — watcher delivery is single-tab even when sync is active.
Cindel Sync is currently experimental. The adapter contract and configuration API may change between minor releases.

Enabling Sync

Pass a CindelSyncConfig to Cindel.open. Everything else — reads, writes, watchers, transactions — stays the same.
After opening, read and write normally. Cindel captures supported local writes automatically — do not create sync mutations by hand.

CindelSyncConfig Fields

If you omit clientId, Cindel persists an internal ID for this local database. Provide an explicit clientId when your backend needs to track which device sent a mutation — for example, to apply per-device access control or ordering rules.

Sync Status

The onStatusChanged callback receives a CindelSyncStatus object you can use for UI indicators and logging.
The phase field is one of: Use pendingCount to show a “2 changes pending” badge. Use lastSyncAt to show a “last synced” timestamp. Always use typed reads and watchers as the source of application data — do not drive UI state from sync callback payloads.

Implementing CindelSyncAdapter

Implement the CindelSyncAdapter interface by providing two methods: push and pull.

Push Flow

push receives a CindelPushRequest containing the pending local mutations and expects a CindelPushResult in return. CindelPushRequest fields:
  • clientId — the local client/device ID
  • lastPulledCheckpoint — the last checkpoint this database pulled
  • schemaVersionByCollection — current local schema versions by collection name
  • mutations — the list of CindelSyncMutation objects to send
Each CindelSyncMutation contains: Return a CindelPushResult:
  • acceptedMutationIds — tells Cindel which pending mutations can be removed from the outbox.
  • rejectedMutations — for mutations your backend will permanently never accept. Reserve rejections for business failures, not temporary network errors.
  • correctedChanges — lets your backend return final truth immediately, such as a server-normalized document.
  • checkpoint — optional on push. If provided, Cindel stores it as the newest known checkpoint.

Pull Flow

pull receives a CindelPullRequest and expects a CindelPullResult containing every remote change after the local checkpoint. CindelPullRequest fields: clientId, checkpoint, collections, schemaVersionByCollection. Return a CindelPullResult:
Use resetRequired: true only when the backend cannot safely continue from the client’s checkpoint. For normal incremental sync, leave it as the default false.
The collection field in every remote change must match a schema registered at Cindel.open. Cindel refuses to apply changes for unknown collections.

Offline Behavior

If your adapter cannot reach the server, throw an error from push or pull. Cindel will catch it and retry later according to interval. While offline:
  • Local writes still commit immediately.
  • Typed reads return local optimistic data.
  • Watchers emit local changes.
  • pendingCount increases as mutations queue in the outbox.
  • The sync phase may become offline.
  • Pending mutations survive app close and reopen.
When the server is reachable again, the scheduler retries automatically — no manual intervention required.

Supported Local Operations While Sync Is Active

Cindel records the following local operations as sync mutations:
  • Typed put and putAll
  • Typed delete and deleteAll
  • Query deletes
  • Link replacements
Query updates (updateFirst / updateAll) are NOT supported while sync is enabled. They do not produce complete document snapshots, so Cindel cannot build a valid mutation from them.Instead of:
Read the objects, mutate them in Dart, then write them back:

Minimal Server Requirements

A production sync backend should provide:
  • Durable storage for accepted mutation IDs and document state.
  • Idempotent mutationId handling — if the same mutation arrives twice, accept it again without applying it twice.
  • Stable checkpoints — clients must be able to pull all changes after a given checkpoint reliably.
  • Access control and authentication — your adapter handles auth; your backend enforces it.
  • Conflict and correction rules — your backend decides the final document shape.
Start with two endpoints:
  • POST /sync/push — accepts local mutations, returns accepted IDs and optional corrections.
  • POST /sync/pull — returns remote changes after a checkpoint.

Testing Sync

Use a FakeSyncAdapter with an online toggle to test sync behavior without a real server. See the Testing page for a full guide.

Common Mistakes

If your backend uses clientId to track devices or enforce ordering, a new random ID on every launch means every run looks like a brand-new device. Previously sent mutations become orphaned — the backend has no way to deduplicate them. Generate the clientId once, persist it (e.g. in SharedPreferences), and reuse it across restarts.
The onStatusChanged and onError callbacks are for indicators and logging only. Do not drive application data from their payloads. Use typed Cindel watchers (watchCollection, watchObject, etc.) as the single source of truth for your UI — they fire automatically when remote corrections arrive.
Your backend must treat mutationId as an idempotent key. If the network drops after your backend applies a mutation but before the adapter returns, Cindel will retry with the same mutationId. Accepting it twice will corrupt data. Always check whether a mutationId was already applied before processing it again.
Every collection name in CindelRemoteUpsert, CindelRemoteDelete, or CindelRemoteReplaceLinks must match a schema you passed to Cindel.open. If the name doesn’t match, Cindel will refuse to apply the change. Make sure your server only returns collections the client has registered.
updateFirst and updateAll are not supported when sync is active. They produce partial field patches, not complete document snapshots, so Cindel cannot build a valid mutation. Read the documents, modify them in Dart, and call put or putAll to write them back.