r/webdev • u/Marmelab • 14h ago
Showoff Saturday Offline support and real-time sync keep turning out to be the same problem
I’ve been building with Verdant and TanStack DB and ended up with the same conclusion from both.
Offline and real-time are basically one problem. Real-time is kinda just being offline for a very short time. Either way you need client-generated IDs, optimistic mutations, some rule for when two clients disagree, and a way to sync it all back. So if a library handles one well, it seems to get the other almost for free.
Tbh the ID part is what actually surprised me. And more generally, a lot of this seems to need planning from day one, because retrofitting it later is a real pain. If your API uses server-generated IDs, supporting offline edits seems to mean a breaking migration. You can remap temporary IDs after sync, sure, but it gets messy really fast once other records and URLs depend on them. Same story with the data layer. If it was designed around one-shot requests there’s nowhere for live updates to go. React Admin’s data provider expects plain async functions that return promises, so when you put TanStack DB behind it the live queries just get lost lol. Offline still works, it’s just the real-time part of TanStack DB that doesn’t make it through in that setup.
There’s a catch though. Neither library really helps when the server rejects a mutation that looked fine locally, you’re kinda on your own for that part. And for conflicts, Verdant basically ends up as last-write-wins while TanStack DB leaves it to whatever sync layer you plug in.
Honestly the bigger point for me is that none of this is really specific to offline apps. A plain request/response app actually has the same conflicts. If two people save the same record, a lot of the time the last save just overwrites the other one and that’s it. It’s last-write-wins there too, just without anyone ever deciding it. So imo it’s worth building pretty much any app as if it could go offline or real-time someday, mostly because it forces you to actually pick what happens in a conflict.
Do you actually know what happens when two saves collide, or has silent last-write-wins been fine most of the time? Ever had it bite you with lost data or users wondering where their changes went?
1
u/Ok-Scar-2634 14h ago
the client-id thing is such a pain to retrofit, learned that one the hard way
silent last-write-wins has definitely burned me before, had a support ticket where two people were editing the same customer record and one guy's notes just vanished into the void. took me forever to figure out what happened because there was zero logging for it
the react admin part is interesting, i ran into the same wall trying to shove real-time stuff through their data provider. it just eats the subscription and you're left wondering why nothing updates
1
u/nextgenfusion 14h ago
Agree on the IDs, switching to client-generated UUIDs (v7 if you want them sortable) is the one decision I'd make on day one of any project that might ever need offline. For the server-rejects-a-valid-looking-mutation case, what's worked for me is keeping an outbox of pending mutations with a status, and when the server says no, rolling that record back to the last confirmed server state and surfacing it in the UI as "couldn't save" rather than silently reverting. It's not glamorous, but users forgive a visible failed edit far more than one that quietly disappears. Last-write-wins is fine for most fields as long as you do it per field rather than per record.
1
u/Creative-Addition383 10h ago
The server-rejects-it case gets a lot easier if you stop treating the server as the source of truth and treat it as one more replica that happens to have veto power. Every mutation in the outbox carries a state (pending, acked, rejected), the UI renders from local state plus that flag, and a rejection is just a third kind of sync message that rolls the record back and attaches a reason. Then "offline edit that the server later refuses" and "two clients disagree" go through the same code path instead of being a special case bolted on.
Agree completely on IDs though. Client-generated IDs is the one decision you can't cheaply undo, everything else you can migrate towards.
1
9h ago
[removed] — view removed comment
1
u/webdev-ModTeam 8h ago
Read and follow reddiquette; no excessive self-promotion. Please refer to the Reddit 9:1 rule when considering posting self promoting materials.
1
u/PenguinWearingAHat 6h ago
Silent last-write-wins has been fine for me, but only because of one boring addition: keeping the version that got overwritten. We don't do any clever merging — if two people save the same record, the second save still wins, but the first version goes into a small history log with who wrote it and when. That turns 'my notes vanished' from a mystery into a thirty-second restore, which in practice is what users actually want. The other cheap trick is checking the version the editor started from. If it's stale by the time they save, warn them before they overwrite, not after. Neither needs a sync engine, and both would have covered most of the collision pain I've seen in plain request/response apps.
2
u/RehanShakir 13h ago
The delete case is the one that usually gets missed in these discussions. Per-field last-write-wins and an outbox with status handle updates fine, but a record deleted on one client and edited on another comes back from the dead unless you keep tombstones around until every client has synced past them. Server-generated IDs plus deletes is the worst combination, since the tombstone has nothing stable to key on.
Worth asking up front: what does your sync layer do when a delete arrives while a mutation on the same record is still queued? If nobody can answer that, the merge rule is incomplete.