C.W.K.
Stream
Lesson 01 of 04 · published

The Network Is Not the Save

~12 min · local-first, critical-path, durability, anti-pattern

Level 0Dead Zone
0 XP0/32 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"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.

Code

Right order vs. wrong order·typescript
// WRONG: the network is on the save path.
async function submitWrong(turn: PippaGoTurn) {
  const res = await fetch("/api/ask", { method: "POST", body: encode(turn) });
  await saveLocally(turn); // too late — if fetch throws, we never got here
}

// RIGHT: commit locally first, then transport.
async function submit(turn: PippaGoTurn) {
  await commitLocally(turn);        // input is now unlosable
  turn.syncStatus = "pending";
  await enqueueForDelivery(turn);   // may run now, later, or after restart
}

// The order is not a style choice. It's the difference between
// 'durable then delivered' and 'delivered or lost.'

External links

Exercise

Write pseudocode for submitting a message two ways: one where the local save happens after the network call, one where it happens before. Trace what the user has left in each version when the network call throws an exception. The difference — 'nothing' versus 'a durable pending message' — is the entire value of capture-first, in five lines.
Hint
In the after-network version, the exception skips the save entirely. In the before-network version, the save already committed, so the exception only leaves the message in a retryable 'failed' state. Same feature, opposite reliability.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.