DEV Community

Nabeel Hassan
Nabeel Hassan

Posted on Originally published at nullstud.io

Build the Sync Spine Before You Build a Single Screen

The first time a client asked me for "offline mode", it arrived three weeks before launch, in the same tone as "can we also have a dark theme". I said yes, because storing data on a device is not hard. Then I sat down with the actual question and realized I had agreed to rebuild the data model.

Offline is not a feature you bolt on. It is a decision about where the truth lives, and it is far cheaper to make in week one than in week twenty.

There are three offline apps, and they share one name

One word covers three very different builds. Naming which one you are actually being asked for is the most useful thing you can do in the first hour.

Level 1: it does not fall over. The app still needs the network to do anything useful, but it behaves like adult software without it. Cached screens render, a half-finished form survives, and it recovers on its own when signal comes back. This is a few days of care, not an architecture. A surprising share of "we need offline mode" requests are fully satisfied here, and finding that out early is worth more than any library you could pick.

Level 2: capture now, send later. The user creates work while disconnected: photos, readings, notes, a signature, a completed job sheet. It queues locally and uploads when there is signal. Data flows one way, which is exactly what keeps it affordable. Nothing on the server contradicts the queue, so there is nothing to reconcile.

Most field software wants this and only this. The same split runs through my own app, LectureNotes AI: recording the lecture must never depend on connectivity, while transcribing and summarizing it is heavy work that can happily wait for a better connection.

Level 3: a real replica. The device holds a working copy of shared data, the user edits it offline, the server changes too, and both sides have to converge. This is where the engineering cost lives: conflict resolution, partial sync, schema migrations on devices that have not phoned home in a month, and a class of bug that only appears when two clocks and two copies disagree.

The distance between level 1 and level 3 is most of the budget. Almost nobody says which one they mean.

The question that sets the price

Out of everything you can ask, one question moves the number more than the rest:

Can two people change the same record while both are offline?

If every user only creates their own records, or each record belongs to one person for the duration of a job, you never need conflict resolution and you stay at level 2. If two disconnected devices can edit the same row, you are at level 3 and the conflict rules are part of the scope, in writing, before anyone estimates.

Four more that change the shape of the build:

How long is the worst realistic outage? Ninety seconds in a lift is a retry problem. A full shift underground is an architecture. A site visited once a week is a different product again. Ask for the worst case, not the average: that number decides how much data lives on the device.

How much data has to be there? The whole database rarely fits and rarely should. The working set is usually narrow: this week, this region, these accounts, these assets. Defining that subset is a product conversation, not a technical one.

What happens if the device is lost? An offline app is a copy of company data in a coat pocket. Encryption at rest, a wipe path, a retention rule for the local copy, and a clear answer about what a stolen phone exposes. For regulated data this can decide the whole design, so it belongs in week one, not in the security review before launch.

What must be true before the user walks away from signal? Offline apps need a warm-up: data pulled, assets cached, queue empty. A user who leaves with a stale copy carries an app that is confidently wrong all day, so the ones that work in the field show when the device last synchronized and whether it is safe to go dark.

Conflicts get skipped, then eat the project

If two copies of a record can change independently, you need a rule. There is no clever default. Pick per record type, not once for the whole app.

  • Last write wins. Cheap, simple, and it silently destroys work. Fine for a status flag. Not fine for anything a person spent ten minutes typing.
  • Ownership per field. The office owns the customer address, the field owns the job outcome. Both edits survive because they never touch the same field. It resolves far more real cases than people expect, and costs a conversation rather than a merge engine.
  • Append instead of overwrite. Record events rather than states: readings taken, parts used, notes added. Two devices appending to a list never conflict, current state is derived, and you get an audit trail for free.
  • Send it to a human. A small review queue for the collisions your rules cannot settle, with a named person who actually works it. A system that claims not to need one is hiding collisions, not resolving them.

One rule sits above the other four: never silently discard something a user made. If the app cannot merge it, it keeps it and tells somebody. A dropped form is not a sync bug to your user, it is a reason to go back to paper.

What it changes everywhere else

IDs get created on the device. A record exists before the server has ever seen it, so it cannot wait for a server-assigned identifier. Decide this early or retrofit it painfully, because it leaks into every foreign key and every log line.

You stop trusting the clock. Device clocks drift, get set by hand and cross time zones. Ordering events by device timestamp is a bug with a delayed fuse.

Old versions stay in the wild. An app that sat in a drawer for six weeks comes back and syncs against a schema that has moved on. Versioned payloads and a server that can still read an old client are part of the job, so even a greenfield offline build inherits the discipline of legacy maintenance.

Airplane mode is not a test. Toggling the radio off gives you a clean binary that real users almost never experience. What belongs on the test plan is flapping connections, half-completed uploads, credentials that expire mid-sync, a device syncing at 2% battery, and two devices editing the same record on purpose.

When offline-first is the wrong call

When the data must be authoritative at the moment of the action. Payments, stock allocation, anything where two people committing the same thing offline creates a real world problem. On Fyuel, the accounting platform we build for the fuel trade, the ledger is the authority by design, and two disconnected devices both writing to it would be a defect rather than a feature.

When the app is inherently live. Shared real-time collaboration needs a server in the loop anyway. Build for reconnection quality instead of independence.

When users are never actually offline. Desk software with a nervous stakeholder needs level 1 and an honest conversation.

How I scope one now

  1. Watch the work happen. Where people stand, for how long, and what they do with paper when the app fails them.
  2. Name the level per feature, not for the whole app. Most apps are level 2 with one or two areas at level 3.
  3. Write down the working set: what lands on the device, keyed to what, and how it refreshes.
  4. Decide the conflict rule per record type, including which ones go to a human.
  5. Build the sync spine first. One record type moving all the way through, tested against bad networks, before any screens exist.
  6. Ship it into a real dead zone. A depot basement finds in a day what a simulator will not find in a month.

Step 5 is the one people fight me on, because a sync spine demos badly and a screen demos well. It is still the right order. Every offline project I have watched go sideways went sideways in the sync layer, and by then it had screens bolted to it.

Offline-first is not about surviving a rare outage. It is about building for where the work actually happens. Covoisino, a ride-sharing app for hitchhikers we built to make hitchhiking safer, meets its users at motorway junctions rather than at desks, and its verification step is a QR check between two people already standing together. Design from that reality and the software fits the job. Design from the office and your users keep a clipboard in the van.

Originally published on the Null Studio blog.

Top comments (1)

Collapse
 
raknaos profile image
Raknaos

Ownership per field is the rule I'd bet most teams underestimate — 'the office owns the address, the field owns the outcome' resolves the bulk of conflicts that would otherwise reach a merge engine. Append-only for event-ish records paid off the same way for me: deriving state from an event log costs storage but makes 'what got lost and when' answerable after the fact, which last-write-wins never is.

'Airplane mode is not a test' belongs above every mobile QA desk. The failures that actually reach users are flapping radios, expired credentials mid-sync, a device resuming at 2% battery — none reproducible by toggling the radio off. On the review queue: per record type to a named person, or one shared inbox? The first is the one that survives an ops team that never reads generic queues.