Skip to main content
The default behavior is “last write wins per field”. Updates to different fields on the same record don’t conflict with each other. The server processes operations in the order received, so if two users modify the same field, the last update to reach the server wins. For most apps, this works fine. But some scenarios demand more complex conflict resolution strategies.

When You Might Need Custom Conflict Resolution

Retail inventory: Two clerks ring up the same item while offline. You need to subtract both quantities, not replace one count with the other. Healthcare records: A doctor updates diagnosis while a nurse updates vitals on the same patient record. Both changes matter, you can’t lose either. Order workflows: Once an order ships, it should lock. Status must progress logically (pending → processing → shipped), not jump around randomly. Collaborative documents: Multiple people edit different paragraphs simultaneously. Automatic merging prevents losing anyone’s work.

How Data Flows Through PowerSync

Understanding the data flow helps you decide where to implement conflict resolution.

Client to Backend

When a user updates data in your app:
  1. Client writes to local SQLite - Changes happen instantly, even offline
  2. PowerSync queues the operation - Stored in the upload queue
  3. Client sends operation(s) to your backend - Your uploadData function processes it
  4. Backend writes to source database - Postgres, MySQL, MongoDB etc.

Backend to Client

When data changes on the server:
  1. Source database updates - Direct writes or changes from other clients
  2. PowerSync Service detects changes - Through replication stream
  3. Clients download updates - Based on their Sync Streams (or legacy Sync Rules)
  4. Local SQLite updates - Changes merge into the client’s database
Conflicts arise when: Multiple clients modify the same row (or fields) before syncing, or when a client’s changes conflict with server-side rules.

Understanding Operations & CrudEntry

PowerSync tracks three operation types:
  • PUT - Creates new row or replaces entire row (includes all non-null columns)
  • PATCH - Updates specific fields only (includes ID + changed columns)
  • DELETE - Removes row (includes only ID)

CrudEntry Structure

When your uploadData receives transactions, each one has this structure:

What Your Backend Receives

Client-side connector sends:
The following structure is only received by the backend if the transactions are not mutated in your client’s uploadData function
Backend API receives:
Operations are idempotent - your backend may receive the same operation multiple times. Use clientId and the operation’s ID to detect and skip duplicates.

Implementation Examples

The following examples demonstrate the core logic and patterns for implementing conflict resolution strategies. All client-side code is written for React/Web applications, backend examples use Node.js, and database queries target Postgres. While these examples should work as-is, they’re intended as reference implementations, focus on understanding the underlying patterns and adapt them to your specific stack and requirements.

Strategy 1: Timestamp-Based Detection

The idea is simple: add a modified_at timestamp to each row. When a client updates a row, compare their timestamp to the one in the database. If theirs is older, someone else changed the row while they were offline, so you treat it as a conflict. This is great for quick staleness checks. You are not merging changes, just stopping outdated writes, similar to noticing a Google Doc changed while you were editing a local copy. The only real catch is clock drift. If server and client clocks are out of sync, you can get false conflicts. And if clients generate timestamps themselves, make sure they all use the same timezone.

Database Schema

Source database (Postgres):

Backend Conflict Detection

Backend API (Node.js):
Timestamps can be unreliable if servers have clock skew. Additionally, if clients are writing timestamps (rather than letting the database generate them), ensure all clients use the same timezone/localization as the server. For critical data, use sequence numbers instead.

Strategy 2: Sequence Number Versioning

Instead of timestamps, you can use a version number that increments on every change. It works like a counter on the row. Each time someone updates it, the version increases by one. When a client sends an update, they include the version they last saw. If it doesn’t match the current version in the database, another update happened and you reject the write. This avoids clock drift entirely because the database manages the counter, so clients can’t get out of sync. The tradeoff is that it’s all or nothing. You can’t merge simultaneous edits to different fields. You only know that the row changed, so the update is rejected. Use this when you want strong conflict detection and are fine asking users to refresh and redo their edits rather than risking corrupted data.

Database Schema

Source database (Postgres):

Backend Conflict Detection

Backend API (Node.js):

Strategy 3: Field-Level Last Write Wins

Here things get more fine-grained. Instead of tracking changes for the whole row, you track them per field. If one user updates the title and another updates the status, both changes can succeed because they touched different fields. You store a timestamp for each field you care about. When an update comes in, you compare the client’s timestamp for each field to what’s in the database and only apply the fields that are newer. This allows concurrent edits to coexist as long as they are not modifying the same field. The downside is extra complexity. You end up with more timestamp columns, and your backend has to compare fields one by one. But for apps like task managers or form builders, where different parts of a record are often edited independently, this avoids a lot of unnecessary conflicts.

Database Schema

Source database (Postgres):

Client Schema with Metadata

Client schema:

Client Updates with Timestamps

Client code:

Backend Field-Level Resolution

Backend API (Node.js):

Strategy 4: Business Rule Validation

Sometimes conflicts aren’t about timing at all, they’re about your business rules. Maybe an order that has shipped can’t be edited, or a status can’t jump from pending to completed without hitting processing or prices can only change with manager approval. This approach isn’t about catching concurrent edits. It’s about enforcing valid state transitions. You look at the current state in the database, compare it to what the client wants, and decide whether that move is allowed. This is where your domain rules live. The logic becomes the gatekeeper that blocks changes that don’t make sense. You can also layer it with other methods: check timestamps first, then validate your business rules, and only then apply the update.

Backend with Business Rules

Backend API (Node.js):

Strategy 5: Server-Side Conflict Recording

Sometimes you can’t automatically fix a conflict. Both versions might be valid, and you need a human to choose. In those cases you record the conflict instead of picking a winner. You save both versions in a write_conflicts table and sync that back to the client so the user can decide. The flow is simple: detect the conflict, store the client and server versions, surface it in the UI, and let the user choose or merge. After they resolve it, you mark the conflict as handled. This is the safest option for high-stakes data where losing either version isn’t acceptable, like medical records, legal documents, or financial entries. The tradeoff is extra UI work and shifting the final decision to the user.

Step 1: Create Conflicts Table

Source database (Postgres):

Step 2: Sync Conflicts to Clients

Sync Streams / Sync Rules:

Step 3: Record Conflicts in Backend

Backend API (Node.js):

Step 4: Build Resolution UI

Client UI (React):

Strategy 6: Change-Level Status Tracking

This approach works differently. Instead of merging everything in one atomic update, you log each field change as its own row in a separate table. If a user edits the title of a task, you still apply an optimistic update to the main table, but you also write a row to a field_changes table that records who changed what and to which value. Your backend then processes these changes asynchronously. Each one gets a status like pending, applied, or failed. If a change fails validation, you mark it as failed and surface the error in the UI. The user can see exactly which fields succeeded and which didn’t, and retry the failed ones without resubmitting everything. This gives you excellent visibility. You get a clear history of every change, who made it, and when it happened. The cost is extra writes, since every field update creates an additional log entry. But for compliance-heavy systems or any app that needs detailed auditing, the tradeoff could be worth it. The implementation below shows the full version with complete status tracking. If you don’t need all that complexity, see the simpler variations at the end of this section.

Step 1: Create Change Log Table

Source database (Postgres):

Step 2: Client Writes to Both Tables

Client code:

Step 3: Backend Processes Changes

Backend API (Node.js):

Step 4: Display Change Status

Client UI (React):

Other Variations

The implementation above syncs the field_changes table bidirectionally, giving you full visibility into change status on the client. But there are two simpler approaches that reduce overhead when you don’t need complete status tracking:

Insert-Only (Fire and Forget)

For scenarios where you just need to record changes without tracking their status. For example, logging analytics events or recording simple increment/decrement operations. How it works:
  • Mark the table as insertOnly: true in your client schema
  • Don’t include the field_changes table in your Sync Rules
  • Changes are uploaded to the server but never downloaded back to clients
Client schema:
When to use: Analytics logging, audit trails that don’t need client visibility, simple increment/decrement where conflicts are rare. Tradeoff: No status visibility on the client. You can’t show pending/failed states or implement retry logic.

Pending-Only (Temporary Tracking)

For scenarios where you want to show sync status temporarily but don’t need a permanent history on the client. How it works:
  • Use a normal table on the client (not insertOnly)
  • Don’t include the field_changes table in your Sync Rules
  • Pending changes stay on the client until they’re uploaded and the server processes them
  • Once the server processes a change and PowerSync syncs the next checkpoint, the change automatically disappears from the client
Client schema:
Show pending indicator:
When to use: Showing “syncing…” indicators, temporary status tracking without long-term storage overhead, cases where you want automatic cleanup after sync. Tradeoff: Can’t show detailed server-side error messages (unless the server writes to a separate errors table that is in Sync Rules). No long-term history on the client.

Strategy 7: Cumulative Operations (Inventory)

For scenarios like inventory management, simply replacing values causes data loss. When two clerks simultaneously sell the same item while offline, both sales must be honored. The solution is to treat certain fields as deltas rather than absolute values, you subtract incoming quantities from the current stock rather than replacing the count. This requires your backend to recognize which operations should be cumulative. For inventory quantity changes, you apply the delta (e.g., -3 units) to the current value rather than setting it directly. This ensures all concurrent sales are properly recorded without overwriting each other.

Database Schema

Source database (Postgres):

Backend: Delta Detection and Application

The key is detecting when an operation should be treated as a delta versus an absolute value. You can identify this through table/field combinations, metadata flags, or operation patterns. Backend API (Node.js):

Client Implementation

On the client side, you need to ensure updates are sent as deltas, not absolute values. When a sale occurs, send the change amount: Client code:
The backend receives this as a PATCH operation where opData.quantity = -3, which it then adds to the current quantity rather than replacing it.

Alternative Approaches

1. Metadata Flags: Include operation type in metadata to signal delta operations:
Backend checks metadata and applies accordingly. 2. Separate Transactions Table: Track each quantity change as its own row, then aggregate them. This provides full audit history but requires syncing an additional table. 3. Operation-Based Detection: Infer cumulative operations from the pattern. Negative values likely indicate sales (deltas), while large positive values might be absolute restocks requiring different handling.

Using Custom Metadata

Track additional context about operations using the _metadata column.

Enable in Schema

Client schema:

Write Metadata

Client code:

Access in Backend

Backend API (Node.js):
Common use cases:
  • Track which device/app version made the change
  • Flag operations requiring special handling
  • Store user context (role, department)
  • Implement source-based conflict resolution (mobile trumps web)
  • Pass approval flags or business context

Complete Backend Example

Here’s how to tie it all together in a Node.js backend with Postgres. Backend API (Node.js + Express):

Best Practices

Design for idempotency: Operations arrive multiple times. Check for existing records before inserting, use upserts, or track operation IDs to skip duplicates. Test offline scenarios: Simulate two clients going offline, making conflicting changes, then syncing. Does your resolution strategy behave as expected? Provide clear UI feedback: Show sync status prominently. Users should know when their changes are pending, synced, or conflicted. Consider partial failures: If batch processing fails midway, how do you recover? Use database transactions and mark progress carefully. Log conflicts for analysis: Track how often conflicts occur and why. This data helps you improve UX or adjust resolution strategies. Leverage CRDTs for collaborative docs: For scenarios with real-time collaboration, consider CRDTs to automatically handle concurrent edits. For information on CRDTs, see our separate guide. Collaborative editing without using CRDTs: You can use PowerSync for collaborative text editing without the complexity of CRDTs. See Matthew Weidner’s blog post on collaborative text editing using PowerSync.