A referee scoring a racket-sports match is one of the least forgiving places to put a mobile app. Venue Wi-Fi comes and goes, one tap can end the set, and what the phone shows has to match what the players on court just saw, because money sometimes rides on the result.
Our first version sent every tap to the server and waited for a response. It held up on a desk and failed in a venue, and the failure changed the question we were asking. It was no longer how to cache data for a flaky network. It was who owns the record while the network is gone. We gave that to the device: the phone holds an event log that is the official record during a match, and the backend stores what the phone has already decided rather than recomputing it.
That is stricter than standard offline-first practice, and it fits three conditions: one writer at a time, a record of something that happened in the physical world, and a network you cannot trust at the moment of the write. Here is how the model works, what it costs, and where it is the wrong choice.
Why a Server-Owned Write Breaks Under an Unreliable Network
The obvious fix for a lost write is to send it again, and that is where server ownership gets risky rather than merely slow. Without an identity on each write, a retry can apply the same change twice, and the client has no way to know whether the original ever landed.
The second problem is subtler. If the server validates or recalculates the result, the domain rules now live in two places and the two copies have to stay identical for the lifetime of the product. Each sport and each tournament format scores differently, with its own edge cases near the end of a game, so a second implementation on the server would have been one more thing to keep in step. When the copies drift, the client and the server disagree about the same event, and nothing in the system can say which one is right.
An Event Log on the Device, Not a Cached Snapshot
The device stores actions rather than state. Each user action becomes an event; the events go into a local log, and current state is derived by replaying the domain rules over that log. Nothing writes a row that says what the score is now.
This is event sourcing scoped to a single device. Martin Fowler's write-up is the reference if the pattern is new: capture every change as a sequence of events, then rebuild state by replaying them.
Each event carries four things:
- The action: point_won, undo, match_ended
- A sequence number (seq) assigned on the device
- Context: which side, which game, what the screen showed at the time
- A timestamp, for display only
Identity and ordering come from seq rather than the clock, so a slow write or a device with the wrong time set cannot change the meaning of the log. The app also writes derived markers in the same transaction, such as game_completed, which is what lets the backend store the log without implementing any domain logic.
The log lives in SQLite, and that local write is the commit point. If it fails, the action did not happen, and the interface does not move. Everything after it, including sync, is cleanup.
Batching, Backoff, and Idempotency Keys in Offline Sync
Sync runs in the background and never sits in the path of a user action. Three properties keep it safe:
- Batched: pending events go up together in one request, not one request per tap.
- Ordered: the device sends in seq order, while the server accepts batches in any order because each event is keyed individually.
- Retried: failures back off at 1s, 2s, 5s, 10s, and 30s, and the device sends whatever is still pending when connectivity returns.
seq doubles as the idempotency key. Replay the same batch and the server ignores what it already holds, which is what makes aggressive retrying safe. Stripe documents the same technique for its API, where a client-generated key lets a request be repeated without performing the operation twice. Duplicate delivery is one of the oldest sources of production pain in distributed systems, and a per-write key is the cheapest defence available.
One step still needs connectivity: claiming the session, which is how the system guarantees a single writer. After that, every action, including the one that ends the session, is just another event in the log.
Designing the Interface When the Network Is Optional
If the device owns the record, there is no reason for the interface to have an offline mode. The app behaves identically either way, and only the background flush depends on connectivity.
That removes a class of UI work that offline-capable apps usually carry: pending states, queued-item counts, retry buttons. We also left out a sync badge, since the person using the app is watching the thing being recorded, not the network.
Two conditions are worth interrupting for, because they change what the user should do: an expired login, and a session that has been claimed by another device.
Undo as an Append, and What Survives a Crash
Corrections are events too. Undo appends a reversing event rather than deleting the original, so the log stays a complete audit trail, which matters when a result is questioned weeks later. Two guards sit on top: a limit on consecutive undos, and suppression of rapid repeat taps on the same control so one action cannot register twice.
Durability follows from the commit point. Because the log is already on disk, an app crash costs nothing: reopening restores the same state with no network. A dead battery behaves the same way, and whatever was pending flushes when the device comes back.
The uncovered case is hardware loss. Events that never left a lost or broken device are gone, and no amount of client-side design fixes that. Recovery is procedural: an operator releases the session, another device claims it, and that device rebuilds state from the log the server already holds.
Single Writer by Design: Ownership Transfer Instead of Merge
Device-owned state only works if exactly one device can write at a time, so ownership is explicit. When a session moves, the previous device locks and says plainly that it was taken over and that unsynced writes were not applied.
We do not merge the two logs. A merged record is wrong in a way nobody can inspect, whereas a dropped tail is visible and bounded. The same logic applies to human overrides: an operator cannot edit a live record while a device still holds the session, because that creates two authoritative versions at once. Corrections happen after the session closes.
The Trade-Offs This Model Accepts
Worth being explicit, because these are the questions a technical reviewer will ask:
- Unsynced writes die with the device. Acceptable when the window is seconds to minutes, and the data is reconstructible by the people present. Not acceptable when the record exists nowhere else.
- No concurrent writers. Adding a second simultaneous editor means adding a real conflict strategy, not relaxing the lock.
- The server cannot correct a live record. Fixes are post-hoc by design, which is a workflow change for teams used to admin overrides.
- The client is trusted. Reasonable for a tool used by one designated operator, wrong anywhere the user benefits from lying.
Cache-and-Sync or Device-Owned: Picking the Model
Most offline-first guidance describes the cache model, where the local store sits in front of a server that owns the data and sync reconciles the two once the network returns. Android's app architecture guide and Flutter's offline-first pattern both lay it out that way, and for a conventional app it is the right default. That decision is then about local databases and sync engines, which our comparison of WatermelonDB, RxDB and PowerSync for React Native covers.
Device ownership is the narrower choice. It earns its place when the device is the only writer and the event being recorded exists outside the database.
When the Client Should Not Be the Source of Truth
Remove any of the three conditions and the server should own the record again.
Keep authority on the server when:
- Several clients write the same record at once, as in chat or collaborative documents.
- The server holds information the client does not, as with payments, tickets or inventory.
- The client cannot be trusted, as in anti-cheat or betting.
- Changing a live value is a normal administrative task.
- Nothing downstream can proceed until all parties agree, like a transfer or a gate.
For a single-writer operator tool, one device holds the truth until it syncs. For a payment, it never does.
Building This on React Native
We built the app in React Native with Expo, with the domain rules kept separate from the screens so the engine could be tested on its own. If you are planning something similar, our React Native development services page explains how we work with in-house teams.
If you want to talk through an offline-first design or the event log in more detail, connect with me on LinkedIn: Harshal Shirke.
Frequently Asked Questions
What is offline-first architecture? An offline-first app reads and writes a local database first and syncs with the server in the background, so it keeps working with no connectivity. In most implementations, the server still owns the record, and the local store is a cache. A stricter variant makes the local event log the official record while the work is happening.
When should the device be the source of truth instead of the server? When a single device is the only writer, the thing being recorded happens in the physical world, and the network cannot be trusted at the moment of the write. If several clients write the same record, the server holds information the client does not, the client cannot be trusted, or an admin needs to change live values, the server should stay authoritative.
What is an idempotency key and how does it stop duplicate syncs? It is an identifier the client attaches to a write so the server can recognise a repeat of the same operation and ignore it. Generating it on the device, for example as a per-event sequence number, makes retries safe: a replayed batch cannot apply the same change twice.
How do you resolve conflicts if two devices work on the same record offline? Either merge the versions, which needs a conflict strategy such as last-write-wins or CRDTs, or prevent the conflict by allowing one writer at a time. Single-writer workflows are usually better served by the second approach: hand ownership to one device, lock the other, and drop its unsynced writes rather than blending two versions of the record.
Can you use event sourcing in a mobile app? Yes, and on a single device it is simpler than the server-side version. Store each user action as an event in a local database such as SQLite, derive current state by replaying those events, and ship the log to the backend. You get offline support, an audit trail, and an undo that appends a reversing event instead of destroying history.

Harshal Shirke
Frontend Engineer
Harshal is a Software Development Engineer with experience building production-grade web and mobile applications using Next.js, React, React Native, TypeScript, and JavaScript. He is passionate about creating fast, scalable, and intuitive user experiences, with a strong focus on performance, accessibility, and clean engineering practices. He enjoys solving complex problems, building products that deliver measurable business impact, and exploring AI-augmented engineering workflows to improve developer productivity and software quality.
