Melon-dbMelon-db
Sync

Sync

Watermelon-compatible pull/push with retry, conflicts, and checkpoints.

Melon-db sync follows a Watermelon-compatible pull/push model. This page summarizes the protocol; see also Frontend, Backend, Data types, and FAQ.

Protocol steps

StepAction
PullpullChanges({ lastPulledAt, schemaVersion, migration? }) → { changes, timestamp, schemaVersion? }
Applydb.applyRemoteChanges(changes, { conflictPolicy, conflictResolver? })
PushpushChanges({ changes: await db.getLocalChanges(), lastPulledAt })
Ackdb.markLocalChangesPushed()
CheckpointcheckpointStore.setLastPulledAt(timestamp) + setLastSchemaVersion(version)

Sync status

synchronize() emits:

  • idle → pulling → pushing → complete
  • retrying with { phase, attempt } when pull/push retries
  • paused with { reason: 'offline' } when network monitor reports offline
  • failed with a SyncError (checkpoint and outbox preserved for retry)

Retry and cancellation

import { DEFAULT_RETRY_POLICY, synchronize } from '@melon-db/sync';

await synchronize({
  db,
  pullChanges,
  pushChanges,
  retryPolicy: DEFAULT_RETRY_POLICY,
  signal: abortController.signal,
});

Conflict policies

  • server-wins (default)
  • skip-existing
  • client-wins
  • last-write-wins
  • merge-by-field — overlay pending local field patches onto remote rows
  • custom — call your own conflictResolver for each remote create/update/delete

Custom conflict resolver

Set conflictPolicy: 'custom' and provide conflictResolver. The resolver receives local row, remote payload, and outbox entry, and returns apply (with merged record) or skip. Use clearOutbox: false on apply to keep local changes queued for push (same as merge-by-field).

import { mergeRemoteWithPendingFields, type ConflictResolver } from '@melon-db/db';

const resolver: ConflictResolver = (ctx) => ({
  action: 'apply',
  record: mergeRemoteWithPendingFields({
    local: ctx.local,
    remote: ctx.remote,
    pendingFields: ctx.outboxEntry?.pendingFields,
    primaryKey: ctx.primaryKey,
  }),
  clearOutbox: false,
});

await synchronize({
  db,
  pullChanges,
  pushChanges,
  conflictPolicy: 'custom',
  conflictResolver: resolver,
});

React hooks

Use @melon-db/db-react:

  • MelonSyncProvider
  • useSync() / useSyncStatus()

Reference backend

bun run sync-server
bun run postgres:up
bun run sync-server:postgres

See @melon-db/sync-server.

Try the Sync playground.