FFDB Docs

Offline replicas

Connect the logical protocol to a transactional local replica adapter.

Keep a durable local replica

Offline replicas connect the sync protocol to durable runtime-specific local storage.

A correct adapter preserves atomic rows, cursor, pending mutations, and rejections across interruption and restart.

Use it when users must read or queue writes without continuous network access.

Requirements for Offline replicas

  • Prerequisite — A supported sync schema and authenticated user scope.
  • Prerequisite — A durable transactional storage engine for production; MemoryReplica is test-only.
  • Required value — Replica instance, push/pull batch sizes, lifecycle/network triggers, retry policy, and scope-derived storage key.
  • Required value — A UI path for pending, rejected, error, and last-synced state.

OfflineSyncClient

The adapter provides transactional row replacement, cursor persistence, pending mutation storage, rejection tracking, and deterministic typed row reads. mutate() makes inserts, partial updates, and deletes visible locally in the same atomic operation that queues them. FFDB ships IndexedDB for browsers, built-in SQLite for Node 24+, native-SQLite contracts for React Native, and a memory adapter only for tests.

sync.tsts
import { OfflineSyncClient } from "@ffdb/sync-client";

const sync = new OfflineSyncClient(ffdb, replica, {
  pushBatchSize: 100,
  pullBatchSize: 1000,
});

await sync.mutate(mutation);
const localRow = await sync.getRow(mutation.table, mutation.primary_key);
const localRows = await sync.listRows(mutation.table);
await sync.sync();

Subscribe and schedule

  • sync() deduplicates concurrent calls and reports snapshot, push, pull, idle, or error phase.
  • Pending mutations are pushed in bounded batches before pulling later server changes.
  • Use application lifecycle and connectivity signals as hints, not proof that a network request will succeed.
  • Keep a retry/backoff policy outside tight render loops and surface rejected mutations to the user.
sync-lifecycle.tsts
const unsubscribe = sync.subscribe((state) => {
  renderSyncState({
    phase: state.phase,
    pending: state.pending,
    lastSyncedAtMs: state.lastSyncedAtMs,
    error: state.error,
  });
});

window.addEventListener("online", () => {
  void sync.sync();
});

await sync.sync();
// Call unsubscribe() when the application scope is disposed.

Replica choices

  • Browser: import IndexedDbReplica from @ffdb/sync-client/browser and use a database name scoped to the project plus signed-in authorization identity.
  • React Native / Expo: wrap the runtime driver as NativeSQLiteDriver and use NativeSQLiteReplica.
  • Node 24+: import NodeSQLiteReplica from @ffdb/sync-client/node and use an owner-only SQLite path; close it during graceful shutdown.
  • Tests: MemoryReplica is useful for contract tests but loses rows, cursor, and pending mutations on reload or process exit.
  • Custom engines: implement every ReplicaTransaction method atomically, including snapshot replacement and cursor movement.

Offline replicas workflow

  • 1. Construct OfflineSyncClient with the FFDB client and replica.
  • 2. Subscribe the UI to sync state.
  • 3. Enqueue mutations before reporting them durable.
  • 4. Trigger sync on explicit user action and reasonable connectivity/lifecycle hints.
  • 5. Destroy or replace cached scope when authorization changes.

Verify offline replicas

Rows, cursor, and queued work survive the expected runtime lifecycle and converge after connectivity returns.

Troubleshoot offline replicas

  • The adapter commits rows and cursor separately — fix the transaction boundary before production use.
  • A background retry loop drains battery or floods requests — add bounded backoff and lifecycle gates.

Continue from Offline replicas

  • Use IndexedDbReplica in browsers or NodeSQLiteReplica in Node 24+.
  • Use the React Native adapter where applicable.