When the signal is optional but the work isn't
Most software quietly assumes a live connection. That assumption holds up fine in an office, and falls apart the moment your users are somewhere real. On the builds we work on, "somewhere real" means a basement three levels below the street, a lift shaft in a steel-framed tower, or a plant room where the concrete blocks every bar of reception. The connectivity is optional. The work is not.
Offline-first is our answer to that gap. It is not a graceful-degradation afterthought bolted onto an online app; it is a deliberate architecture where the local device is the source of truth and the network is treated as an occasional, unreliable luxury. We built IssuesID, our construction defect and quality-management platform, exactly this way, and it is the clearest worked example we have for how the pieces fit together.
What a PWA actually is
A Progressive Web App is a website that behaves like an installed application. It runs in the browser, but with a manifest and a service worker it can install to the home screen, launch full-screen without browser chrome, and keep working when the network drops. No app store, no review queue, no separate iOS and Android builds to maintain.
That last point matters more than it sounds. For a field tool, "install from the App Store" is friction: an account, a device policy, a download over whatever signal exists on site. A PWA installs from a URL. A site manager opens a link, taps "Add to Home Screen", and the app is on the device in seconds. You ship once and update everywhere, instantly, because it is still just a web deployment underneath.
Online-with-cache is not offline-first
There is an important distinction people collapse, and it is worth being precise about it.
Online-with-cache means the app fundamentally needs the network, but caches responses so it survives a flaky connection or loads faster on the second visit. Lose signal for an hour and the app is a museum: you can look at what it cached, but you can't do anything new.
Offline-first inverts the relationship. Every action writes to a local store first and returns immediately. The UI never waits on the network, because it never asks the network for permission. Syncing to the server is a background concern that happens whenever connectivity returns. To the person on site, the app feels identical whether they have five bars or none, and that consistency is the entire point.
For a defect-capture tool, only the second model is acceptable. An inspector walking a lift shaft cannot have the app freeze because a photo upload is pending. They tag the defect, annotate the drawing, move on. The sync is our problem, not theirs.
The machinery
Offline-first has a fairly settled toolkit in 2026, and it is less exotic than it looks.
- A service worker sits between the app and the network as a programmable proxy. It intercepts requests and decides what to serve from cache and what to fetch. We manage ours with Workbox, which turns cache strategy into a few lines of configuration rather than hand-rolled
fetchhandlers. The app shell — HTML, JS, CSS — goes cache-first so the app launches instantly and offline; genuinely dynamic reads use network-first with a cache fallback. - A local database holds the real data.
localStorageis far too small and synchronous; the right tool is IndexedDB, which handles structured records and large blobs like photos. We use Dexie as a wrapper because the raw IndexedDB API is genuinely unpleasant, and Dexie gives it a clean, promise-based query interface. - Queued writes are the heart of it. Every create, edit and photo becomes a record in the local store, plus an entry in an outbound queue. The user's action completes the instant it hits IndexedDB.
- Background sync drains that queue. When the device is online — including after the app has been closed — the service worker replays queued writes to the server and pulls down anything new.
Registering the service worker is the one piece worth seeing, because everything above hangs off it:
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/sw.js").then((reg) => {
// Ask the browser to replay our queue whenever it regains connectivity
return reg.sync.register("sync-outbound-queue");
});
});
}
The genuinely hard part: sync and conflict resolution
Caching is a solved problem. Sync is where offline-first earns its reputation, and where most of the real engineering goes.
The trouble is time. A device can be offline for hours. When it reconnects, it is not replaying one clean change against a pristine server — it is merging a batch of local edits against a server that other people have been editing the whole time. Two inspectors can update the status of the same defect while neither has signal. When they both reconnect, someone has to decide what the truth is.
There is no universal right answer, only a policy you choose deliberately:
- Last-write-wins is the simplest — the most recent timestamp takes the field. It is fine for low-contention data and quietly loses edits under contention.
- Field-level merging treats a record as independent fields, so two people editing different attributes of the same defect both succeed. This handles the common case well and is where we land most often.
- Append-only logs sidestep merging entirely for the right data shapes. New photos, comments and audit entries never conflict, because nothing is overwritten — you are only ever adding.
IssuesID leans hard on that last idea. Its tamper-proof audit trail is append-only by design: revisions are immutable, records are withdrawn rather than deleted, and every change is logged. That is a compliance and handover feature first, but it is also what makes offline sync tractable — an append-only history has almost nothing to merge. We reserve genuine conflict resolution for the small set of mutable fields where it actually arises, and surface those to a human when the automatic policy would be guessing.
When it's worth the cost — and when it isn't
Offline-first is more expensive to build and considerably more expensive to test. It is not a default; it is a decision.
It pays for itself when the work happens where the network doesn't: field and site apps, logistics, agriculture, mining, inspection and maintenance — anywhere users are routinely beyond reliable coverage and cannot simply "try again later". If losing connection means losing work, or means a person standing idle, offline-first is the correct architecture rather than a nice-to-have.
It is over-engineering for the opposite case. An internal admin dashboard, a marketing site, a back-office tool used at a desk on office wifi — building full offline sync into those is cost with no return. A sensible service-worker cache for resilience and speed is plenty; you do not need a local database and a conflict-resolution policy to survive a two-second network blip.
The trade-offs are real even when the decision is right. Browser storage has limits and can be evicted under pressure, so you manage what you keep on-device and prune synced data. Testing multiplies: you are now validating offline states, partial syncs, interrupted syncs and conflict paths, not just the happy online flow. And sync logic, once you have it, tends to be the part of the system you revisit most.
The pragmatic take
Offline-first is not about never touching the network. It is about refusing to let the network dictate whether people can do their jobs. Get the local store, the queue and the sync policy right, and the app simply works — signal or no signal — and the reconnection sorts itself out quietly in the background.
That is the bet we made with IssuesID, because basements and lift shafts don't get better reception to suit our stack. If you want to see offline-first capture and sync working on a real site, request a demo. If you are weighing whether your own build genuinely needs it, get in touch — it is exactly the kind of trade-off we like talking through before a line of code is written.


