TL;DR
"Works offline" is four separate decisions, and vendors usually ship one of them and call it done. This is what each one costs, what this field app does, and the one place where it deliberately does less than it could — because doing more would show one company's work to another.
First: it is a PWA, not a native app
The field app installs from the browser to the home screen. It is not in the App Store or on Google Play.
That is a real limitation and worth stating before anything else. A PWA on iOS cannot read position in the background, cannot use every native sensor, and depends on the browser engine underneath it. If your requirement is a native app, this is not one.
What it gets you instead: one codebase, no store review between a bug and its fix, no per-device install procedure for a seasonal crew that changes in April, and a link that works on whatever phone somebody already has. It opens full-screen in portrait and starts on the jobs screen rather than the office one.
Second: the shell is cached, the data is not
A service worker precaches the shell itself: the entry document, the icon and the manifest. Everything else the build emits is content-hashed and named at build time, which a hand-written worker cannot know in advance, so those files are cached as they are requested. It costs one online visit before the app opens offline, and every field phone has had one by the time it reaches a customer.
Because the build output is content-hashed, a cached copy can never be the wrong version of anything, so assets are served from the cache and refreshed in the background. A navigation tries the network first — so a deploy is picked up — and falls back to the cached shell, which is what makes the app open in a cellar.
Then the rule that matters more than any caching strategy: nothing from the API is ever cached.
Every API response is scoped to one organization and one signed-in user. A cached copy outlives both. Serving yesterday's response to somebody who has since switched between two offices — or signed out, or been removed from the company — would show one customer's work to another, and it would do it from a layer that no amount of correct server-side scoping can reach.
So being honest about what "offline" means here: the app opens without a signal, and the worker's clock-ins and clock-outs are queued. A cold start with no connection gives you the app, not yesterday's job list. That is a deliberate trade, and the security reason is the whole of it.
The other service-worker decision worth copying: a new version does not take over a running tab on its own. Swapping the assets under a half-filled form is how somebody loses a completion — and on this app, that form may have a customer standing next to it waiting to sign. The new worker takes over on the next load, or when the page decides the moment is safe.
Third: the outbox
Clocking in and out goes through a queue: written to disk first, sent when there is a connection, removed only once the server has acknowledged it.
IndexedDB, not localStorage. It is the only web storage that reliably survives an iOS home-screen app being evicted from memory mid-shift, and it is not limited to the few megabytes of strings that localStorage gives you.
Written before sending, always — even on a good connection. Sending first and queueing on failure leaves a window: the request is in flight, the app is killed, the work is gone. That window opens exactly when the network is bad, which is exactly when field work happens. Writing first costs a few milliseconds and closes it. It also buys one ordering guarantee for free: a clock-out can never overtake the clock-in it closes, because both leave through the same queue.
The id and the timestamp are made on the device, at the moment of the act. Not when the entry is finally sent. An hour spent in a basement must not be recorded as an hour that began when the van reached the main road. The server accepts the phone's time within sanity limits — not more than five minutes in the future, not more than a week back — keeps the time the entry actually arrived beside it, and the timesheet flags any stretch that synced more than 15 minutes late. Trusting the phone's clock is the point; trusting it blindly is not.
The position travels with the entry. Taken at the tap, sent with the request whenever it drains. Taking it when the queue drains would record where the van was when it found a signal — a different place, and a worse lie than no position at all.
Fourth: how the queue drains, and when it gives up
This is where most offline implementations quietly go wrong.
Serial and ordered. Entries are sorted by when they happened and sent one at a time. These are not independent events: a clock-out closes a specific clock-in. Sending them in parallel would be faster and would arrive out of order often enough to matter.
Stops at the first real failure, rather than skipping it. Whatever failed is almost certainly the connection. Skipping past it would deliver later work while the thing it depends on is still sitting in the queue.
Three statuses mean "done with, one way or another". A 409 from a clock-in means the server already has it: the request landed and the reply was lost, which is precisely what a flaky connection does. A 422 is a payload the server refuses once and will refuse every time. A 404 names a job that is gone. None of the three gets better by being sent again, and retrying forever would pin the queue behind an entry that can never succeed — with every clock-out behind it stuck too. All three are removed.
Everything else is retried. A timeout, a 500, no network at all. The attempt count and the last error stay on the entry, so a queue that is genuinely stuck is visible rather than silent.
What is queued today, and what is not
Being exact, because "works offline" is the phrase people buy on.
Clock-in and clock-out go through the queue. Those are the two acts that happen eight times a day in places with no signal, and they are the ones whose timestamps matter most.
The queue has slots defined for checklist ticks, photographs and signatures. Nothing enqueues them yet, and the sending code for those kinds deliberately throws rather than quietly resolving — a silent success there would report a customer's signature as delivered and then drop it, which is the one outcome worth writing code to prevent. The checklist, photos, materials and the customer's signature are sent together with the completion, and job completion today needs a connection at the moment it is sent.
If your crews finish jobs in places with no signal at all, know that before you buy, not after.
What to tell the crew
Three sentences, which is about as much as anybody will remember:
- Open the app on wifi once before the first job. After that it opens anywhere.
- Clock in and out when it happens, not when you get a signal. The time is taken from your phone at the moment you tap.
- The number next to the clock is what is waiting to be sent. It clears itself when you get coverage. If it does not clear for a whole day, tell the office.