Documentation Index
Fetch the complete documentation index at: https://mintlify.com/mainser/cindel/llms.txt
Use this file to discover all available pages before exploring further.
Cindel includes an experimental local-first sync engine that you opt into at database open time. Once sync is configured, your application code continues to use the same typed collections, queries, transactions, and watchers as any non-sync database — Cindel records local mutations in an internal durable outbox and runs a background scheduler against the adapter you provide. You supply the backend-specific push and pull logic; Cindel handles sequencing, checkpointing, and applying remote changes.
This is an early integration API, not a complete production conflict-resolution layer. Before using it for critical data, validate your backend’s idempotency, checkpoint, rejection, reset, and conflict policies with your own app flows. The sync API may change before it is marked stable.
Enabling sync at open time
Sync has no runtime command surface — there is no db.sync property to call. Configure it once in Cindel.open using CindelSyncConfig:
final db = await Cindel.open(
directory: dir.path,
schemas: [UserSchema],
sync: CindelSyncConfig(
adapter: AppSyncAdapter(),
onStatusChanged: (status) {
// Show offline/syncing/pending state in your UI.
},
onError: (error, stackTrace) {
// Report background sync failures.
},
),
);
App code uses the same typed collections, queries, transactions, and watchers whether sync is enabled or not. Sync is purely an internal concern — db.users.put(user) records the mutation in the outbox and returns normally, just as it would without sync.
CindelSyncConfig parameters
| Parameter | Type | Default | Description |
|---|
adapter | CindelSyncAdapter | required | Application-supplied push/pull implementation |
clientId | String? | null | Stable client id for idempotent backend writes. When omitted, Cindel persists and reuses an internal id |
onStatusChanged | void Function(CindelSyncStatus)? | null | Observes status changes for UI feedback |
onError | void Function(Object, StackTrace)? | null | Observes background sync errors |
autoStart | bool | true | Starts the internal scheduler immediately after open |
interval | Duration | Duration(seconds: 5) | Background scheduler poll interval |
batchSize | int | 100 | Maximum pending mutations sent in one push |
Implementing CindelSyncAdapter
The adapter is an interface with two methods. You implement it using your backend’s HTTP client, WebSocket, or any other transport.
abstract interface class CindelSyncAdapter {
Future<CindelPullResult> pull(CindelPullRequest request);
Future<CindelPushResult> push(CindelPushRequest request);
}
Example adapter
final class AppSyncAdapter implements CindelSyncAdapter {
@override
Future<CindelPushResult> push(CindelPushRequest request) async {
// Send request.mutations to your backend idempotently.
return CindelPushResult(acceptedMutationIds: {
for (final mutation in request.mutations) mutation.mutationId,
});
}
@override
Future<CindelPullResult> pull(CindelPullRequest request) async {
// Return backend changes after request.checkpoint.
return const CindelPullResult(checkpoint: '0', changes: []);
}
}
Push: CindelPushRequest and CindelPushResult
The push direction sends local mutations from the durable outbox to your backend.
CindelPushRequest
| Field | Type | Description |
|---|
clientId | String | Stable client id for this database |
lastPulledCheckpoint | String? | Last checkpoint successfully pulled before this push |
schemaVersionByCollection | Map<String, int> | Registered schema version per collection |
mutations | List<CindelSyncMutation> | Up to batchSize pending local mutations |
CindelPushResult
| Field | Type | Description |
|---|
acceptedMutationIds | Set<String> | Mutation ids the backend has durably accepted |
rejectedMutations | List<CindelSyncRejectedMutation> | Mutations the backend will never accept |
correctedChanges | List<CindelRemoteChange> | Backend truth to apply locally after accepting optimistic mutations |
checkpoint | String? | Optional checkpoint produced while handling this push |
Pull: CindelPullRequest and CindelPullResult
The pull direction fetches remote changes from your backend after a known checkpoint.
CindelPullRequest
| Field | Type | Description |
|---|
clientId | String | Stable client id for this database |
checkpoint | String? | Last checkpoint applied locally, or null for an initial pull |
schemaVersionByCollection | Map<String, int> | Registered schema version per collection |
collections | Set<String> | Application collections this database can apply |
CindelPullResult
| Field | Type | Description |
|---|
checkpoint | String | New checkpoint that covers all returned changes |
changes | List<CindelRemoteChange> | Remote changes after the requested checkpoint |
resetRequired | bool | Whether the backend requires the client to reset before continuing |
Sync mutations
Each item in CindelPushRequest.mutations is a CindelSyncMutation:
| Field | Description |
|---|
mutationId | Stable id used by the backend to deduplicate retries |
clientId | Client that created this mutation |
sequence | Monotonic local sequence for this client |
collection | Application collection affected |
operation | CindelSyncOperation.upsert, .delete, or .replaceLinks |
documentId | Document id for document operations |
document | Canonical document snapshot for upserts |
linkName | Link field name for link replacement operations |
targetCollection | Target collection for link replacement operations |
targetIds | Target ids for link replacement operations |
baseCheckpoint | Checkpoint known when the local mutation was recorded |
What is captured as a sync mutation
Cindel captures the following local operations into the durable outbox:
put (typed document upsert)
putAll (bulk upsert)
- Unique-index
putBy… upserts
delete and deleteAll (document deletes)
- Query-based
deleteAll (query deletes)
- Link replacements (
.save() on CindelLink / CindelLinks)
What is NOT supported while sync is enabled
Query updates — filter().updateAll({...}) — are explicitly rejected when sync is enabled. They do not produce canonical per-document mutations and cannot be represented in the outbox.
Sync status and phase
onStatusChanged receives a CindelSyncStatus on every scheduler transition:
| Field | Description |
|---|
phase | Current CindelSyncPhase |
pendingCount | Number of durable local mutations still waiting for adapter acceptance |
lastSyncAt | UTC timestamp of the last completed sync cycle, or null before one succeeds |
lastError | Last background error reported for this status event |
CindelSyncPhase values:
| Phase | Meaning |
|---|
idle | Scheduler is waiting for the next interval |
syncing | Scheduler is actively running push or pull |
offline | Adapter threw a connectivity-class error |
error | Adapter threw a non-connectivity error |
CindelSyncConfig(
adapter: AppSyncAdapter(),
onStatusChanged: (status) {
switch (status.phase) {
case CindelSyncPhase.syncing:
showSyncIndicator();
case CindelSyncPhase.offline:
showOfflineBanner();
case CindelSyncPhase.error:
reportError(status.lastError);
case CindelSyncPhase.idle:
hideSyncIndicator();
}
},
),
Durable outbox
Mutations are persisted in the same database as your app data before being pushed to the backend. If the app is closed or crashes before the adapter accepts a mutation, the outbox survives reopening the database and the scheduler will retry. The internal outbox storage is not part of the public API — it is managed entirely by Cindel.