How TerraCore syncs when there is no signal
Local database, change queue, per-record versions and stock adjustments sent as differences: the decisions behind TerraCore's offline mode.

An app that only works with internet is useless in a pasture. Someone records a vaccine at six in the morning, with no signal, and expects the data to still be there when they get back to the house.
Saving locally is the easy part. The hard part starts when two devices edit the same thing without having seen each other, and you have to decide which data wins without stepping on anyone's work.
What works offline
TerraCore is a web app that can be installed as a PWA. Animals, Crops, Supplies, Production, Animal health (the vaccines), Sites and the Dashboard work without a connection. The Finance figures are recalculated on the device and the screen says so.
Some things always need a network, by their nature: signing in, signing up, resetting a password and anything to do with paying. The first time the app is opened on a device also needs a connection.
Local database and a change queue
In the browser, data lives in IndexedDB through Dexie. Changes that have not reached the server yet wait in an outbox, a queue of pending writes.
That queue is sent every 60 seconds while the app is open and every time the connection comes back. On top of that, each device does a full download every 24 hours.
The Dashboard is computed entirely in the browser from that local database, and does not ask the server for a ready-made summary. That is why it works without a network too. Other writes, like inviting a user or changing the profile, are queued the same way and sent when there is a connection.
One version per record
To detect conflicts, each record carries a version number. Any edit bumps it, whether it comes from sync or from the regular API, not only edits made offline.
When the client sends a change, it also sends the version it worked on. The server compares and answers with three lists:
{
"accepted": [],
"conflicts": [],
"rejected": []
}
- accepted: the change was applied.
- conflicts: the record changed on the server since the client's copy. The server does not apply the old change.
- rejected: the change is not valid, and the response carries the reason.
The second group is the key: a stale copy that arrives late does not overwrite what someone else already saved.
Not losing what was edited in the meantime
There is an awkward case: the person edits a record while their previous send is still on its way, and the server returns it as a conflict or a rejection.
If the client threw everything away, that last edit would be lost. Instead it keeps it and resends it on top of the copy the server now holds.
Rejections, on top of that, are not retried on their own. They are stored in their own local table and the person can only discard them, so an invalid write does not sit in the queue trying to send forever.
Inventory as a difference, not a value
This is the example that best explains the problem. Two people adjust the same supply offline: one subtracts 10 kilos because they fed the herd, the other adds 25 because a delivery arrived.
If each one sent the final stock value, the last to sync would erase the other's adjustment. That is why adjustments travel as a difference relative to a base. Subtracting 10 and adding 25 compose, in any order.
The server enforces the same idea: if an absolute quantity arrives from a client whose copy is older than the server's, it is not applied and the record comes back as a conflict.
Pulling changes without pulling everything
The way back, from server to device, has its own care. Downloads are paged by cursor, so everything does not come down at once.
The full download, without a cursor, also removes the local rows the server no longer returns. That is how a deleted record disappears from the device.
And when someone else signs in on the same device, the previous person's local data is wiped. A shared phone on the farm cannot leave another person's work visible.
What we learned testing this
Sync bugs almost never show up in a happy-path test. They show up with two devices and a half-finished send. That is why we test guards and sync with the app's real router and a simulated local database, and every fix of this kind must bring tests that fail without the change.
The idea this work left us fits in a sentence: in an offline system, what you send is not the value you ended up with, but what changed and on which version.
Comments
0 repliesSign in to comment, like and save posts.
No comments yet. Start the conversation.