GrowBook works fully offline. No account, no signal, no problem — the local database is the source of truth, always. But some people want their plants on two phones, or shared with a partner who also forgets to water things. So I added cloud sync and family sharing. That's where "offline-first" stopped being an architecture diagram and started being a series of small, uncomfortable decisions.
The setup: every device has its own local database and works alone. Sync is a thing that happens on top — opportunistically, when there's a network — not something the app waits on. The rule I held to was that the app must never block on the network for a core action. You tap "watered," it's saved locally and the UI updates now; the cloud finds out later.
The hard question isn't uploading or downloading. It's: two devices changed the same thing while offline. Who wins?
Every row in GrowBook carries an updatedAt timestamp and an isDeleted flag (soft deletes, so a deletion can sync like any other change instead of a row silently vanishing). When I sync, I compare timestamps and the later write wins. Standard last-write-wins. Simple, predictable, and wrong just often enough to think about.
Here's the case that made me pick a direction on purpose. It's a shared plant. You water it and mark it done on your phone. Your partner, not knowing, waters it too and marks it done on theirs. Two "completed" events, same plant, roughly the same time. What should the app show?
I decided: on sync, download first, and let the remote win on conflicts. My reasoning was specifically about family sharing — if someone else in your household already recorded a completion, that's real information, and clobbering it with your local copy erases something a real person did. In my sync code the comment just says: "remote wins on conflicts (e.g., family member completions)." That one parenthesis is the whole philosophy. It's not that remote is technically more correct — it's that in a shared household, the other person's action is not noise to be overwritten.
The stuff nobody warns you about, in a bulleted heap because that's how it arrived:
What I keep coming back to: sync isn't a technical feature you add, it's a set of value judgments you encode. "Last write wins" sounds neutral, but choosing whose write wins when two humans in the same house both did the right thing — that's a product decision, and pretending it's just engineering is how apps end up quietly deleting things people care about.
If you've built sync: what's your conflict rule, and did you pick it deliberately or inherit it from whatever the library did by default? I picked mine, and I'm still not sure it's right — only that it's mine.
— building GrowBook in public, #5
One product-level option is not to call two waterings a conflict at all: preserve both as event history, yet show a single current care state with a small “also marked by…” trace. That keeps real actions auditable without turning the main screen into merge UI. For actual attribute collisions, show the source and time, then offer an undo only when the choice affects the care schedule. The real test is whether the household still trusts the history after a disagreement.
The conflict example is a good sign that family sharing is creating a real workflow rather than just adding sync for its own sake. Have you seen shared households return to GrowBook more consistently than single-device users, or is that evidence still too early?
I like that you framed it as a product decision, not just a sync checkbox. In internal tools I usually make conflicts boring and visible: save both events when the action has real-world meaning, then only collapse them later if the user can undo it. Last-write-wins is fine for settings, but it gets scary when it represents something a person actually did.