Offline-First Web Apps: A Field Guide for Bad Internet and Old Hardware

The assumption that breaks most software

Almost every web app is built on one quiet assumption: the internet is there, and it's fast. Remove that assumption and most of the modern stack falls over. The single-page app can't fetch its data. The managed database three regions away is unreachable. The three megabytes of JavaScript never finish downloading on a shared 2 Mbps line, and the CDN font request hangs for eight seconds before the text finally renders.

For a large part of the world, that is just an ordinary working day. Grid power drops out several times a day, and the nearest fibre line can be hours away. Software built for a good connection doesn't degrade gracefully there — it simply stops.

Offline-first is the fix, and it is less about clever client-side caching than people assume. It is mostly an architecture decision you make early and cheaply.

What "offline-first" actually means

It does not mean "keeps working in aeroplane mode for five minutes because of a service worker." That is offline-tolerant, and it tends to be fragile — the first uncached request or the first cache-version mismatch breaks it.

Offline-first means the primary copy of the data a user needs lives close to that user — same building, same network — and the internet is used only for the things that genuinely require it: pulling in changes made elsewhere, pushing local changes back out, and doing both when nobody is waiting on the result.

For an institution running on one flaky uplink, that looks like:

  • one low-power machine per site running the application and a full copy of the

database

  • every day-to-day request served over the local network, never touching the

outside link

  • a scheduled sync, usually overnight, that moves only what changed

The user never knows which mode they are in. The page just loads.

The local server is the whole trick

A cheap mini-PC or even a spare desktop running Linux is enough. It holds the app and the database, and every device on the LAN talks to it directly. A page load becomes a request that travels a few metres over a gigabit switch instead of thousands of kilometres over a congested satellite link.

The performance difference is not subtle. Page loads that took 20–25 seconds over the shared uplink drop to under 200 milliseconds on the local mirror. Very little of that gain comes from clever code — it comes from not asking a slow, expensive connection to do work it never needed to do.

Choosing the database

You have two sane options for the local copy:

| Option | Good for | Watch out for | |---|---|---| | SQLite | single-site deployments, read-heavy workloads, zero admin | concurrent writes are serialised; plan around it | | MySQL / MariaDB | multiple sites, heavier write load, existing tooling | one more service to keep alive on the box |

Start with SQLite unless you already know you need more. A single file you can copy, back up, and inspect with any tool is worth a lot when the person maintaining the machine is also the person teaching a full timetable.

Keep the front-end honest

An offline-first backend is wasted if the front-end still pulls 2 MB of JavaScript and a webfont from a CDN on first load. On the hardware these deployments actually run on — 8-year-old lab PCs, low-end Android tablets — that is the difference between a usable page and a spinner.

Practical rules that hold up:

  • Self-host every asset. Fonts, icons, scripts. No third-party requests on

the critical path — they are the requests most likely to hang.

  • Ship route-level code splitting. The average visitor should not download

the admin dashboard bundle.

  • Send metadata, not payloads, to list views. A list of articles needs

titles and dates, not every full body.

  • Server-render the first paint. A crawler or a slow device should get real

HTML on the first response, not an empty <div id="root">.

Syncing without a distributed-systems degree

Most institutional data does not need conflict-free replicated data types. It needs a defensible, boring rule:

1. Give every syncable row an updated_at timestamp and a origin-site id. 2. On sync, newer updated_at wins. 3. For anything where silently losing an edit is unacceptable — grades, financial entries — use an append-only log instead of in-place updates, and reconcile on read.

Run the sync as a nightly cron job when bandwidth is cheap and idle. If a site is offline for three days, it syncs three days of changes when it comes back. Nobody has to think about it.

Where offline-first is the wrong call

It is not free. You are trading a small amount of architectural discipline and a piece of hardware per site for resilience. That trade is worth it when:

  • connectivity is genuinely unreliable, or
  • the cost of downtime is high, or
  • the data is mostly read locally and written locally.

It is not worth it for a product whose whole value is real-time collaboration across locations, or where every site has solid fibre and the app is a thin client over a shared dataset. Use the boring cloud setup there.

The takeaway

Offline-first is not a library you install. It is deciding, before you write the first migration, that the network is optional. Put the data next to the user, keep the client light, sync on a schedule, and pick the simplest conflict rule you can defend. The result is software that loads instantly whether or not the building has a working internet connection that day — which, if you build for the places that need it most, is the entire point.