Skip to main content

API guide

Offline-First API Sync: Queues, Retries & Conflicts (2026)

Design offline-first API sync with a durable outbox, idempotent writes, retries, delayed validation, conflict policy, checkpoints, and recovery UX.

·APIScout Team
Share:
Hero image for Offline-First API Sync: Queues, Retries & Conflicts (2026)

Quick answer

Offline-first API sync is not “cache responses and retry later.” It is a contract across a durable local store, a persistent write outbox, an application or backend write path, server validation, retry and rejection handling, and a read-sync checkpoint. The design is complete only when pending writes survive a reload, duplicate delivery is safe, rejected changes have recovery UX, and the user can tell pending, synced, and failed states apart.

Electric, PowerSync, Yjs, RxDB, and PGlite occupy different parts of that contract. Electric streams Postgres reads and leaves writes to an application-selected pattern. PowerSync writes locally to SQLite, queues operations, and uploads them through your backend connector. Yjs persists and merges collaborative documents, not arbitrary API-record invariants. RxDB is a client document database with replication seams but without relational multi-document ACID transactions. PGlite is embedded Postgres; by itself it is not a sync engine.

Responsibility map: every arrow needs an owner

UI and recovery state
        │
        ▼
durable local store ──► durable request outbox
        ▲                       │
        │                       ▼
read-sync checkpoint ◄── source database ◄── backend write API
                                                │
                                                ▼
                                  auth, validation, idempotency,
                                  conflict policy, acknowledgment

The local store makes the app useful without a network. The outbox records intent that has not been acknowledged. The backend write API enforces current authorization and domain rules. The source database remains authoritative where the product requires server authority. Read sync returns accepted state and advances a checkpoint only after the application can reconcile it locally.

That is a responsibility map, not a universal topology. A single-user notes app, a field-service workflow, and a multiplayer editor can assign authority differently. The invariant is that each responsibility and failure transition has an explicit owner.

Preflight invariants before choosing a sync product

Write these rules down before selecting an SDK:

InvariantContract to defineFailure if omitted
Stable identityGenerate durable record and operation IDs before enqueueing.A replay creates duplicates or cannot target the original record.
Idempotent mutationSend one idempotency key per logical operation and persist the server result for that key.A timeout followed by retry applies the same action twice.
Queue ownershipPartition pending operations by authenticated user, tenant, and environment.One account can upload another account’s offline work on a shared device.
Current authorizationRevalidate permissions on upload; do not treat old local access as current authority.A delayed write bypasses a role or membership change.
Deletion semanticsCarry tombstones or explicit delete operations until every required reader has observed them.Deleted records reappear after a later pull.
Schema versionStore the payload schema and provide an outbox migration or quarantine path.A new app version cannot safely decode old queued work.
Conflict basisCarry a server version, ETag, revision, or other comparison token where needed.The server cannot distinguish a normal update from a stale overwrite.
User-visible stateModel pending, syncing, synced, needs-attention, and rejected.The UI claims success while writes remain local or blocked.

navigator.onLine can be a wake-up hint, but it is not proof that the API, credentials, or write dependency is healthy. Successful response validation and checkpoint progress are stronger signals.

Write-path scorecard

PatternDurable local stateUpload/write ownerRead pathConflict and rejection ownerBest fit
App-owned outboxYour SQLite, IndexedDB, or native storeYour worker and backend APIYour delta endpoint, event log, or subscriptionYour application and serverTeams that need a custom HTTP contract or must integrate an existing API.
Electric read sync + selected write patternChosen by the app; persistent local writes are one documented optionYour API, local optimistic/persistent layer, or another documented write patternElectric Shapes stream Postgres changes over HTTPYour merge, validation, rollback, and reconciliation codePostgres-backed apps that want Electric’s read path while retaining write-path control.
PowerSyncClient-side SQLite plus its upload queueYour backend connector applies queued operations to the source databasePowerSync refreshes local SQLite from the sourceYour connector and application policyApps that want local SQL reads/writes and accept a service + connector architecture.
Yjs documentYjs document persistence such as IndexedDBA chosen Yjs provider/server and its auth layerYjs update exchangeCRDT convergence plus your semantic rulesShared text, canvas, or document state where the document model fits.
RxDBClient document database with a selected storage adapterReplication push handler or supported adapterReplication pull/live-resync handlerServer/master default or a custom handler; domain invariants remain yoursDocument-oriented clients that need local queries and explicit replication seams.
PGlite aloneEmbedded Postgres in memory, filesystem, or IndexedDBNot providedLocal SQL onlyNot provided across devicesRich local relational queries when you will add a separate write/read sync design.

Electric: read sync is not a universal write engine

Electric’s current core path is read-path sync from Postgres through Shapes. Its write guide documents several application choices, including online API writes, optimistic local state, persistent local state, and through-the-database patterns. Those choices have different latency and offline behavior, but none removes the need to decide who validates, merges, rolls back, or explains a delayed rejection.

For a persistent optimistic write, local state can appear successful before the server accepts it. Preserve enough operation and prior-state data to rebase or roll back. If a server constraint rejects the write after reconnection, turn that outcome into a resolvable product state rather than silently overwriting the local value. Electric self-hosting also couples Postgres logical replication with persistent service state, so backup and restore plans must account for both sides.

Sources: Electric read-path sync (ELECTRIC-01), write patterns (ELECTRIC-02), and deployment responsibilities (ELECTRIC-03).

PowerSync: local SQL still needs a backend write contract

PowerSync clients write to local SQLite. Those changes become queued PUT, PATCH, or DELETE operations that the application uploads through a backend connector. The connector must authenticate the current user, validate the operation, apply the accepted change to the source database, and classify failures.

An acknowledgment is not merely “the network request returned.” PowerSync’s consistency model advances through upload acknowledgment and refreshed synced data/checkpoints. The UI therefore needs to distinguish locally committed, uploaded, and reflected states when that difference matters to the workflow.

The upload queue is FIFO. An unhandled operation can block later work behind it, so “retry forever” is not an error policy. Decide whether each failure should pause for reauthentication, retry later, be replaced by a corrected operation, move to a review/quarantine flow, or be explicitly discarded by the user. Preserve the failed payload and reason until the user or application completes that transition.

Sources: architecture overview (POWERSYNC-01), consistency and checkpoints (POWERSYNC-02), writing data (POWERSYNC-03), custom conflict resolution (POWERSYNC-04), self-hosting boundary (POWERSYNC-05), service license (POWERSYNC-06), and web client package metadata (POWERSYNC-07).

Durable outbox and retry policy

Store the outbox in the same durable transaction as the local state change when the storage system supports it. A practical operation record needs enough information to resume after a crash and explain a failure:

{
  "operation_id": "stable UUID",
  "idempotency_key": "stable per logical mutation",
  "actor_scope": "user and tenant partition",
  "resource_id": "stable client-generated ID",
  "method": "PATCH",
  "payload_schema": 4,
  "base_revision": 18,
  "attempt_count": 0,
  "next_attempt_at": "durable timestamp",
  "state": "pending"
}

Do not copy this shape blindly; make it match the server’s actual idempotency, versioning, auth, and deletion contracts. The important part is that retries, ownership, and recovery state survive app restarts.

Classify failures before retrying

  • Network interruption or selected transient server failure: retain the operation and retry with capped exponential backoff plus jitter. Honor server retry guidance when present.
  • Authentication expired: pause that user’s queue, refresh or request sign-in, then retry under the same identity. Never upload it as the next signed-in account.
  • Authorization or validation rejection: do not loop automatically. Keep the operation visible, record a safe reason, and offer correction, export, or discard according to product policy.
  • Version conflict: fetch or use the current server version, then run the declared merge/manual-resolution policy. Do not silently rewrite the base revision.
  • Malformed or unsupported response: treat it as a failed attempt even if the transport succeeded. Validate status, content type, and response schema before acknowledging locally.
  • Poison operation in a FIFO queue: stop, quarantine, repair, or deliberately discard it through an auditable action. Do not drop it in memory just to unblock later writes.

A dead-letter tray is a recovery surface, not a trash can. It should retain the operation identifier, user-safe error class, last attempt, and available resolution actions without exposing sensitive payloads in telemetry.

Conflict policy: convergence is not correctness

Choose policy by data semantics rather than by which algorithm sounds most advanced.

Data shapeReasonable starting policyRequired guardrail
Ephemeral presence or last-seen stateLast-write-wins may be acceptable.Use a trusted ordering source and accept that an earlier value is discarded.
Independent scalar fieldsField-level version checks or merge.Validate cross-field invariants after merge.
Inventory, payments, permissions, unique namesServer-authoritative transaction and explicit rejection.Never infer correctness from client clocks or CRDT convergence.
Shared text/canvas/documentYjs or another document CRDT can fit.Keep authorization, document lifecycle, and business-record updates outside the CRDT assumption.
Ambiguous user intentManual resolution with both versions.Preserve provenance and make the chosen result auditable.

Yjs shared types converge when updates are exchanged, and y-indexeddb can persist a document offline. Persistence and network transport are separate providers. That is valuable for collaborative document state, but it does not enforce arbitrary uniqueness, authorization, or multi-record business invariants. An offline-loadable web product also needs an app-shell/service-worker strategy beyond document persistence.

Sources: Yjs shared types (YJS-01), offline document persistence (YJS-02), and connection providers (YJS-03). For a broader collaboration-layer comparison, see Liveblocks vs PartyKit for realtime collaboration APIs.

Local database boundaries

RxDB supports local document reads and writes plus pull/push replication. Its default conflict behavior and custom handler seam are useful, but its document model does not provide relational multi-document ACID transactions. Storage and server capabilities also cross core and commercial plugin boundaries, so verify the exact adapter and license needed by each target platform instead of assuming every feature belongs to the core package.

Sources: RxDB platform and storage index (RXDB-01), replication (RXDB-02), transactions and conflicts (RXDB-03), tradeoffs (RXDB-04), and plugin boundary (RXDB-05).

PGlite provides embedded Postgres for browser, Node.js, and Bun runtimes with local persistence options. It is not, by itself, multi-device synchronization. The documented PGlite Electric plugin is currently labeled alpha and read-only: it syncs remote data into PGlite but does not provide outbound local-write sync or conflict resolution. Pairing local SQL with a sync product still requires an explicit write path.

Sources: PGlite overview (PGLITE-01), filesystems (PGLITE-02), and alpha read-only Electric plugin (PGLITE-04). Compare the runtime layers in the PkgPulse PGlite, Electric SQL, and Triplit guide.

Test matrix for failure semantics

TestSetupPass condition
Reload persistenceEnqueue a write offline, terminate the app, then restart.Local state and the same operation/idempotency key remain pending.
Duplicate replayAccept the server write, drop the response, then retry.The server returns the original result without applying the mutation twice.
Stale authenticationQueue as user A, expire credentials, and reconnect after user B signs in.A’s operation pauses in A’s partition and never uploads as B.
Delayed validationAccept locally, then have the server reject a domain constraint.UI changes from pending to needs-attention and preserves a recovery path.
FIFO poison operationPut an unrecoverable operation ahead of valid work.The queue reports the blocker and follows the declared quarantine/repair policy.
Simultaneous editUpdate the same record or field from two clients at the same base revision.The selected conflict policy is deterministic, visible, and invariant-safe.
Delete while offlineDelete on one client and edit an old copy on another.Tombstone and resurrection policy produce the documented result.
Schema upgradeRestart a newer app with an older queued payload.The outbox migrates, quarantines, or exports it without silent loss.
Checkpoint interruptionStop after upload acknowledgment but before refreshed read state is applied.Restart reconciles without duplicate mutation or false “synced” state.
Partial outageKeep reads available while the write API fails, then reverse it.UI and monitoring identify which path is degraded.

For mobile-specific payload, versioning, and delta-read design, see Designing APIs for Mobile Apps in 2026. For a managed-backend comparison that should remain a separate decision, see Convex vs Supabase.

Monitoring and recovery UX

Track signals that answer whether user intent is safe, not just whether a background worker ran:

  • queue depth by safe tenant/user partition
  • age of the oldest pending operation
  • counts by pending, retrying, needs-attention, and rejected
  • failures by network, auth, validation, conflict, schema, and server class
  • retry attempts and next eligible attempt time
  • last successful upload acknowledgment
  • last applied read-sync checkpoint
  • checkpoint lag and records waiting for reflected server state
  • number and age of quarantined operations

Avoid logging full offline payloads, tokens, or sensitive record contents. Correlate with operation IDs and sanitized error classes.

The user-facing model should be equally explicit:

  • Pending: saved on this device; upload not yet acknowledged.
  • Syncing: an attempt is active.
  • Synced: the declared acknowledgment/checkpoint condition has completed.
  • Needs attention: the server rejected or could not interpret the change; offer a next action.
  • Blocked: another queued operation prevents progress; identify the affected work without exposing internals.

Do not show a permanent success checkmark at local commit time if the workflow requires server acceptance. Conversely, do not make the app unusable merely because a read checkpoint is temporarily stale when local work is safe.

Source scope and limitations

This guide was refreshed against official documentation on July 31, 2026. Electric and PowerSync APIs and maturity labels are volatile, so recheck their write, consistency, and SDK pages for the exact target platform before implementation.

The claim map uses ELECTRIC-01/02/03, POWERSYNC-01/02/03/04/05/06/07, YJS-01/02/03, RXDB-01/02/03/04/05, and PGLITE-01/02/04. Official vendor documentation establishes product behavior and boundaries; it is not independent proof of performance, reliability, security, privacy, or suitability. This guide therefore makes no storage-quota percentage, browser-support percentage, pricing/free-tier, fastest, scale, compliance, or comparative reliability claim.

No tool is a universal offline architecture. Validate the exact client platform, persistence adapter, server schema, auth model, operational ownership, and recovery UX in your own failure tests before shipping.

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.