Conflict resolution
A conflict happens when two devices have both changed the same row since their last sync. Fexl Lite resolves them with last-write-wins on updated_at — the row with the newer timestamp keeps its values, the older write is overwritten. This page covers when conflicts happen, how the rule actually plays out, the special cases (invoices, journal entries) that avoid them by design, and the rare cases where you have to step in.
The rule, in one sentence
If two devices wrote to the same row before either pushed, the row whose updated_at is later wins on the cloud and propagates to both desktops on the next pull.
That’s it. There is no merge, no field-level diff, no operational-transform. The row is overwritten wholesale. We chose this because Fexl Lite’s data shape is overwhelmingly write-once-read-many — the cases where two cashiers edit the same row in the same minute are rare enough that the simple rule’s blast radius is small.
Where conflicts can actually happen
Three categories of row are conflict-prone in practice:
- Customer profile — name, phone, address. Two cashiers might update the same customer’s phone number while both are offline. The later write wins.
- Product price / stock-on-hand metadata — manager updates the price on tablet A, cashier rings a sale on tablet B that adjusts stock. These edit different fields but the row’s
updated_atupdates wholesale, so one write’s other-field changes get clobbered. (Stock counts are not actually a row field; they’re computed from inventory transactions, so this is mostly a price-edit problem.) - Settings document — owner toggles a setting on tablet A, manager toggles a different one on tablet B. The settings record is one big JSON blob; the later push overwrites the earlier toggle. Don’t co-edit settings.
Where conflicts can’t happen — by design
- Invoices and returns — written exactly once in a transaction, never updated after post. The cloud’s primary key is the invoice UUID, so a duplicate UUID push returns 409 Conflict; the desktop retries by remapping its local ID to the cloud-assigned one and stamping
_cloudIdon the original row. No data is lost. - Journal entries — same shape: written once, never edited. Reversals create a new JE rather than modifying the old one.
- Inventory transactions — append-only. New rows for restocks, sales, adjustments. The current stock count is always the sum of the transactions, so two devices both decrementing stock concurrently both succeed and the math stays correct.
- Cash-drawer shifts — bound to a single device’s open shift. A second device can’t open a concurrent shift on the same drawer. Closing posts a transaction; that transaction is append-only.
This is the trick that lets Fexl Lite use the simple last-write-wins rule. The high-stakes financial state is in append-only tables that don’t conflict; the lower-stakes mutable state (customer phone numbers) is rare-write enough that an occasional clobber is acceptable.
How a real conflict resolves
Two desktops, both offline, both editing customer Ali’s phone number:
Tablet A goes first
At 14:02:11, tablet A updates Ali’s phone to +964 770 111 1111. Local SQLite stamps updated_at = 14:02:11 and _isDirty = 1.
Tablet B goes second
At 14:03:42, tablet B updates Ali’s phone to +964 770 222 2222. Local SQLite stamps updated_at = 14:03:42 and _isDirty = 1.
Tablet A reconnects first and pushes
Tablet A’s sync cycle runs at 14:05:00. Push includes Ali’s row with updated_at = 14:02:11. Cloud writes it.
Tablet B reconnects and pushes
Tablet B’s sync cycle runs at 14:05:14. Push includes Ali’s row with updated_at = 14:03:42. Cloud’s existing row has 14:02:11 — older — so the cloud accepts B’s write and overwrites the value. Phone is now +964 770 222 2222.
Both desktops pull and converge
On their next pull, both A and B receive Ali’s row at version 14:03:42 with the +964 770 222 2222 value. Tablet A sees its earlier change overwritten — silently, no notification.
The conflict log
Every clobbered write is recorded in the sync_conflict_log table on the cloud. Each entry carries the row’s table name, primary key, the losing values, the winning values, and the timestamps of each. There is no UI for browsing this log today — it’s a database-level audit trail you query when investigating “I swore I changed that” reports.
If you suspect a conflict ate your edit, ask support to query the log for that row. We surface the relevant entries on demand.
When manual intervention is the only fix
Three rare cases need a human:
- Two devices ring the same physical invoice — e.g. cashier rings a sale on the customer’s card; they’re impatient, walk to the second till, the second cashier rings it again. Both invoices land. Find the duplicate (Reports → Invoice list, filter by customer), cancel one. The cancelled invoice’s accounting entries reverse; the customer is charged once.
- Settings drift — owner edited Sales tab on tablet A, manager edited the same tab on tablet B; the later save clobbered the earlier. Open the tab on whichever device has the right values, save again to push it to cloud.
- Race during cloud-side migration — extremely rare. If a database migration runs while a desktop is mid-push, the push can fail mid-way. Re-run Force sync now on the desktop and the dirty rows retry on the next cycle.
Related
Sync overview
The big-picture sketch — push, pull, the 30-second cycle, the WebSocket.
Cloud architecture
Where conflicts happen in the stack and why scanRows matters.
Manual sync
Force a cycle out of band when you suspect drift.
Sync troubleshooting
When the cycle gets stuck or rows pile up dirty.