BE Teck Notes Back to be-teck.com

5 min read

Offline-first is a data decision, not a network one

Queueing requests is the easy part. Offline changes what a record means, because it was created by a device nobody was watching, at a time nobody can verify.

"Make it work offline" is usually heard as a networking requirement: queue the requests, retry when the connection returns, show a spinner in between.

That part is straightforward and it is not where the difficulty is. The difficulty is that a record created offline is a different kind of record, and every rule that governed the online version has to be re-examined against it.

What changes about a record#

An entry created against a server was created in a conversation. The server saw the request, applied its rules, and the result was known immediately to both sides.

An entry created offline is an assertion made by a device, in isolation, about a moment nobody else observed, delivered later. Three things about it are now claims rather than facts:

Time. The device clock is under the user's control.

Sequence. Two devices offline at once will produce events whose true order nobody recorded.

Validity. The rules were not applied when the entry was made. They will be applied when it arrives, which may be after the situation it referred to has changed.

None of these has a general solution. Each has to be decided for the specific record.

Decide time explicitly#

The commonest mistake is to store one timestamp and never say which one it is.

Store both: the time the device claims and the time the server received it. They are different facts, and the gap between them is informative — a large gap is ordinarily a queued punch and occasionally an altered clock.

Then decide, per record type, which one governs. An attendance punch should almost certainly use the claimed time, because the whole point is when the person was there. A financial entry should almost certainly use the server time, because the ordering of money matters more than the device's opinion. What must not happen is that the decision is made implicitly by whichever field somebody selected.

Decide the rules that cannot be checked offline#

Some rules can be applied on the device: this field is required, this number must be positive, this date must be in the past.

Others cannot, because they depend on state the device does not hold: is this person still employed, is this site still active, has somebody else already recorded this, is the order still open.

The offline design has to answer, in advance, what happens when an entry passes the device's checks and fails the server's. There are only three honest answers: accept it anyway with a flag, reject it and tell the person, or hold it for somebody to resolve. All three are defensible. Not choosing means the behaviour is decided by an error handler somebody wrote in a hurry.

Related, and vital: the device must be able to tell try again later from stop retrying this one. Without that distinction a permanently invalid entry retries for ever, draining a battery and filling logs. We marked our payload refusals explicitly as permanent for exactly this reason. The detail is in attendance on a site with no signal.

Deduplicate on an identifier, not on content#

A poor network is not one that fails. It is one that succeeds after the client has given up, so the same entry arrives two or five times.

Fold them by an identifier generated on the device at the moment of creation, carried with every retry. Deduplicating on content — same person, same time, same amount — is wrong in both directions: it merges two genuinely separate events that look alike, and it fails to merge one event whose details were recomputed between attempts.

Authenticate the device, never the message#

If the payload says who it is from, then anybody who can reach the endpoint can be anybody.

Our devices carry their own credential, issued once during an ordinary sign-in inside the app, shown once and stored only as a hash. A queued entry authenticates as that device, and the identity is derived from the credential rather than read from the message.

Two consequences worth planning for. A credential can be revoked, and revocation has to reach the queue — entries already queued on a revoked device should not be honoured simply because they were created before it was revoked. And the emergency stop has to cover this door too: a lock that stopped every screen while phones went on posting would not have stopped anything.

One law, two doors#

The strongest structural rule, and the one most often broken: an offline entry must go through the same function as an online one.

Not a similar function written for the device's convenience. The same one, with a different authentication route into it.

The temptation to write a separate, simpler path is strong, because the device's needs are different and the shared function has awkward edges. It is the same temptation that produces two implementations of any rule, and the result is always the same — they agree at first, they drift, and the drift is silent because each is internally consistent. That is the failure described in a fact that had no owner.

Signals are not activities#

A phone that is offline can still observe things — position, movement, battery. It is tempting to let those observations count toward whatever the record is measuring.

We drew a hard line: a background position sample is a signal, not an activity. It never counts toward the presence window, never counts as activity, never decides which site somebody was at, and never grants access to anything. It is recorded only while the person is deliberately punched in, and the server enforces that rather than the app.

The reason is that a figure derived partly from deliberate acts and partly from ambient observation is a figure nobody can explain. When somebody disputes it, you need to be able to say what was recorded and by whom.

Test the ugly cases, because they are the common ones#

The demo path — go offline, make an entry, come back, see it sync — always works, which is why how to test a works-offline claim in ten minutes is a cheap phone in flight mode rather than a meeting. The ones that matter:

  • Entry made offline, and the underlying thing changes before it syncs.
  • Two devices offline making conflicting entries about the same object.
  • Device offline for a week.
  • Device clock wrong by hours.
  • Sync interrupted halfway.
  • Credential revoked while entries are queued.
  • Same entry delivered five times.

Each of these is ordinary in the field and absent from every test plan written at a desk. The general point — that the test is the worst day, not the demo — is in how we find our own problems.

The short version#

Offline is not about the network. It is about what a record means when it was created by a device nobody was watching.

Store claimed and received time separately and decide which governs. Say in advance what happens when server rules reject an entry the device accepted. Deduplicate on an identifier created at source. Authenticate the device, not the message. Keep observations distinct from acts.

And send both routes through one function, because two implementations of a rule is two answers waiting to disagree.

Have a gap worth closing?

If something in your daily work is broken in a way everybody has stopped complaining about, that is exactly what we want to hear.

Write to hello@be-teck.com

More notes