"The send button and the save button look the same. Wiring them to the same action is the bug behind a thousand lost messages."
The Confusion at the Heart of Lost Input
Almost every "the app ate my message" story has the same root cause: the app treated send as save. You typed, you hit the button, and the only place your words existed was in flight over the network. When the request failed — tunnel, timeout, backgrounded app — there was nothing to fall back to, because nothing had been written down first.
Capture-first refuses this. It splits one apparent action into two, in a fixed order: commit locally, then transport. The save is a local, synchronous-feeling durable write. The send is a separate, retryable network attempt that reads from what was already saved. The network is never on the save path — only on the delivery path.
The Critical Save Path
Think of it as a rule about what sits between the user's intent and durability. The critical save path is every step that must succeed for the user's input to be safe. If the network is on that path, then durability is only as reliable as the connection — which on a train is a coin flip. Capture-first keeps the network off the critical save path entirely:
- User submits → write to the local durable store → done, input is safe.
- Separately, a sync worker reads the store and attempts delivery, now, later, or after a restart.
By the time any packet leaves the phone, the question is already unlosable. Delivery can fail all it wants; it can't take your words with it.
Order Is the Whole Trick
The magic is entirely in sequence. await commitLocally() must finish before fetch() is even called. Reverse them — or run them together and hope — and you've quietly put the network back on the save path. This is the single most common way a "local-first" client isn't actually local-first: the local write exists, but it happens after the send, as a cache of a success that may never come.