Writing Shooting sports
Offline-First Isn't a Feature, It's the Whole Architecture
Match day happens in a gravel pit in a valley with no bars. The scores still have to be right, and the shooter is standing there waiting to sign.
Competitive shooting matches happen where the land is cheap and the neighbors are far away. That means gravel pits, valleys, and rural ranges. Which means no signal, or one bar that disappears when you walk twenty feet to the next stage.
The scoring still has to work. A range officer times a competitor, records hits per target, applies penalties, and the shooter reviews and signs off before walking away. If that transaction fails, you have a dispute with no record, and in a sport where placement is decided by hundredths of a second, that is not a minor inconvenience.
Most software treats offline as degradation. A cached read-only view, a banner apologizing, writes disabled until you reconnect. For match scoring, that is useless. The moment you need to write is precisely the moment you have no connection.
Offline-first inverts the default
Plenty of apps claim they work offline too. The question that actually matters is which side is authoritative.
In a conventional app the server is the source of truth and the client is a view of it. Offline means the view is stale, and writes are blocked because they cannot be confirmed.
In an offline-first app the device is the source of truth for what happened on it. The range officer’s tablet records the run, and that record is complete and valid the instant it is made. Sync is not what makes it real; sync is how it propagates. If the tablet never sees a network again, the match still ran and the scores still exist.
That inversion changes everything downstream. You cannot use server-assigned IDs, because the record exists before any server sees it. You cannot validate against server state, because there isn’t any, so validation rules ship to the device. And you cannot treat sync failure as an error state, because it is the normal state for most of the day.
The UI has to be honest about this without being alarming. ShotKeeper marks a run LOCAL ONLY, not submitted. It isn’t an error and it isn’t a warning. It’s a fact. The RO knows exactly where the record lives, and that it is safe.
Conflict is a domain problem, not a database problem
The generic answers to sync conflict, last-write-wins and vector clocks and CRDTs, solve the mechanical question of which bytes survive. They cannot tell you which outcome is correct, because correctness here is defined by the rulebook, not the data structure.
If two tablets recorded the same competitor on the same stage, that is not a merge. It is a procedural error, and resolving it silently would be the worst possible behavior. Someone has to look.
So conflict resolution follows the sport’s own authority structure. Scores are attested. The RO records, the shooter reviews and approves. That approval is part of the record. When two records disagree, the one carrying attestation wins, and if both do, it escalates to a human, because in the real world it already would have.
Designing sync around who has authority rather than which timestamp is larger meant most conflicts stopped being conflicts. They resolved the way the sport resolves them.
Bluetooth is its own kind of offline
Shot timers connect over BLE, and BLE is unreliable in a way that has nothing to do with the internet. Devices sleep, drop, and refuse to re-pair. The timer is a peripheral that may vanish mid-string.
Same principle: the timer is an input, never a dependency. If it disconnects, scoring continues with manual entry, and the UI says the timer is not connected rather than pretending. The worst version of this product is one that stops working because an accessory went to sleep.
Why this matters outside shooting sports
Offline-first gets treated as a niche concern for field apps. I have come to think of it as a general discipline, because building it forces three questions most software never asks:
What is actually authoritative, and where does it live? Most apps have never answered this, which is why their offline behavior is incoherent.
What does this mean when it fails? Not “show an error.” What is the true statement about the state of the world, and how do we tell the user?
Who resolves disagreement, and by what authority? In every domain there is already an answer. Software that ignores it invents a worse one.
I have built systems where connectivity failure means a police officer arrives without information they should have had, and systems where it means a shooter’s stage time is in dispute. The stakes are wildly different. The architecture question is identical: what remains true when the network does not.