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:- Client writes to local SQLite - Changes happen instantly, even offline
- PowerSync queues the operation - Stored in the upload queue
- Client sends operation(s) to your backend - Your
uploadDatafunction processes it - Backend writes to source database - Postgres, MySQL, MongoDB etc.
Backend to Client
When data changes on the server:- Source database updates - Direct writes or changes from other clients
- PowerSync Service detects changes - Through replication stream
- Clients download updates - Based on their Sync Streams (or legacy Sync Rules)
- Local SQLite updates - Changes merge into the client’s database
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: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 amodified_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):Strategy 2: Sequence Number Versioning
Instead of timestamps, you can use aversion 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 frompending 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:- Sync Streams
- Sync Rules (Legacy)
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 afield_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 thefield_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: truein your client schema - Don’t include the
field_changestable in your Sync Rules - Changes are uploaded to the server but never downloaded back to clients
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_changestable 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
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: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: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):- 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