Mobile field readiness
A desktop browser at a desk on wifi is the least representative test environment
available. The failures below do not appear there at all.
1. Secure context first, because it silently gates everything
Camera, microphone, geolocation, service workers and notifications require
HTTPS. localhost is exempt, which is exactly why this gets discovered late:
everything works on the dev machine and nothing works the moment you open the
LAN address on a real phone.
http://localhost:5173 camera works (localhost exemption)
http://192.168.1.42:5173 camera silently unavailable
https://<tunnel>.trycloudflare.com camera works
Set up a real HTTPS path for device testing on day one — a local certificate
tool or a tunnel — and put the command in the README. Otherwise every person who
joins loses an afternoon to it.
2. Test on a real device early, and on a cheap one
Desktop responsive mode gets you layout and nothing else. It does not reproduce:
- Touch versus mouse, and the absence of hover.
- The on-screen keyboard covering the field being typed into, and the viewport
resize that follows.
- Safe areas: notches, rounded corners, the home indicator, the URL bar that
appears and disappears as you scroll.
- Real camera latency and real photo file sizes.
- Memory pressure — a mid-range Android will kill a tab that a laptop runs
comfortably.
- Sunlight, which makes low-contrast greys invisible.
Test on the worst device your users actually have, not the newest one you own.
3. Every permission has a denied path, and it is permanent
Camera, location and notifications can be denied, denied permanently, or
restricted by policy. On iOS a second prompt is often not shown at all.
For each one:
- Explain before prompting why you need it, in context, at the moment it is
needed. A prompt on first launch with no explanation gets denied.
- Handle denial with something usable — upload from the gallery instead of the
camera, type an address instead of using GPS.
- When denied permanently, say so and point at the OS settings. "Location
unavailable" leaves the user stuck; "Location is off for this site — turn it
on in Settings > Safari > Location" does not.
- Never block the whole flow on an optional permission.
4. GPS is approximate, and you must decide how approximate is acceptable
A position comes with an accuracy radius, and it is routinely 20–50 m
outdoors and much worse indoors, near tall buildings, or on a device with wifi
positioning only.
- Read
accuracy and use it. A check-in inside a 100 m geofence with 500 m
accuracy has not been verified by anything.
- Size the geofence for the site, not for the map pin. Loading docks, car parks
and multi-building sites need a bigger radius than feels right.
- Take the first fix, then watch for a better one — the first is often the
coarse network fix, with the GPS fix arriving seconds later.
- Verify on the server. Coordinates from a client are a claim, not a fact;
any of them can be spoofed. Decide whether that matters for your use, and if
it does, treat GPS as one signal among several rather than proof.
- Provide a manual override with a reason, and log it. Otherwise a genuine
failure leaves a worker unable to do their job.
5. Service worker updates are the top support issue
A cached shell that will not update produces "I see the old version" reports that
are impossible to reproduce, because the reporter's device is the only one
affected.
- Serve the entry document and the service worker with no-cache; serve hashed
assets with long cache. Backwards, and users are pinned on a broken build.
- Have an update path: detect the new worker, then either prompt ("A new
version is ready — reload") or skip waiting and reload, but choose one and
test it.
- Never cache API responses in the same policy as the shell.
- Be able to answer "which build is this user on?" — put the build id in the UI
and in error reports.
- Test the upgrade, not just the install: load the old version, deploy, reload,
confirm the new one arrives.
6. Design for one hand, outdoors, in a hurry
- Primary actions in the bottom third, where a thumb reaches.
- Touch targets at least 44×44 px with real spacing. Two destructive actions
should never be adjacent.
- High contrast; do not rely on colour alone for state.
- Never lose typed input on rotation, backgrounding, or an incoming call.
- Confirm destructive actions, and make them undoable where you can.
- Keep the primary flow reachable in as few taps as possible. Count them.
7. Throttle rather than disconnect
The field is rarely cleanly offline. It is 2G, a half-open connection, a
captive portal, or a connection that accepts the request and never answers.
- Test at "slow 3G" throttling, not just offline.
- Set request timeouts. A hung fetch with no timeout is an app that appears
frozen.
- Show progress for anything over a second, and make it cancellable.
- Retry with backoff; give up and tell the user rather than spinning forever.
If the flow must survive a real loss of coverage, that is a design decision on
its own — see offline-first-sync.
8. Battery, data and storage are the user's, not yours
- Continuous GPS drains a battery in hours. Sample, do not stream, and stop
when backgrounded.
- Downscale photos on capture. A modern phone camera produces files that are
both slow to upload on 3G and expensive on a metered plan.
- Cap local caches and evict.
- Release the camera and any wake lock the moment the screen is left.
Checklist
1---2name: mobile-field-readiness3description: Take a mobile app or PWA feature from "works on my laptop" to "works on a phone in the field" — camera, GPS and geofencing, permissions, install and update, poor signal, one-handed use. Use when building or reviewing anything a person uses standing up, outdoors, on their own device.4---56# Mobile field readiness78A desktop browser at a desk on wifi is the least representative test environment9available. The failures below do not appear there at all.1011## 1. Secure context first, because it silently gates everything1213Camera, microphone, geolocation, service workers and notifications require14HTTPS. `localhost` is exempt, which is exactly why this gets discovered late:15everything works on the dev machine and nothing works the moment you open the16LAN address on a real phone.1718```19http://localhost:5173 camera works (localhost exemption)20http://192.168.1.42:5173 camera silently unavailable21https://<tunnel>.trycloudflare.com camera works22```2324Set up a real HTTPS path for device testing on day one — a local certificate25tool or a tunnel — and put the command in the README. Otherwise every person who26joins loses an afternoon to it.2728## 2. Test on a real device early, and on a cheap one2930Desktop responsive mode gets you layout and nothing else. It does not reproduce:3132- Touch versus mouse, and the absence of hover.33- The on-screen keyboard covering the field being typed into, and the viewport34 resize that follows.35- Safe areas: notches, rounded corners, the home indicator, the URL bar that36 appears and disappears as you scroll.37- Real camera latency and real photo file sizes.38- Memory pressure — a mid-range Android will kill a tab that a laptop runs39 comfortably.40- Sunlight, which makes low-contrast greys invisible.4142Test on the worst device your users actually have, not the newest one you own.4344## 3. Every permission has a denied path, and it is permanent4546Camera, location and notifications can be denied, denied permanently, or47restricted by policy. On iOS a second prompt is often not shown at all.4849For each one:5051- Explain **before** prompting why you need it, in context, at the moment it is52 needed. A prompt on first launch with no explanation gets denied.53- Handle denial with something usable — upload from the gallery instead of the54 camera, type an address instead of using GPS.55- When denied permanently, say so and point at the OS settings. "Location56 unavailable" leaves the user stuck; "Location is off for this site — turn it57 on in Settings > Safari > Location" does not.58- Never block the whole flow on an optional permission.5960## 4. GPS is approximate, and you must decide how approximate is acceptable6162A position comes with an **accuracy radius**, and it is routinely 20–50 m63outdoors and much worse indoors, near tall buildings, or on a device with wifi64positioning only.6566- Read `accuracy` and use it. A check-in inside a 100 m geofence with 500 m67 accuracy has not been verified by anything.68- Size the geofence for the site, not for the map pin. Loading docks, car parks69 and multi-building sites need a bigger radius than feels right.70- Take the first fix, then watch for a better one — the first is often the71 coarse network fix, with the GPS fix arriving seconds later.72- **Verify on the server.** Coordinates from a client are a claim, not a fact;73 any of them can be spoofed. Decide whether that matters for your use, and if74 it does, treat GPS as one signal among several rather than proof.75- Provide a manual override with a reason, and log it. Otherwise a genuine76 failure leaves a worker unable to do their job.7778## 5. Service worker updates are the top support issue7980A cached shell that will not update produces "I see the old version" reports that81are impossible to reproduce, because the reporter's device is the only one82affected.8384- Serve the entry document and the service worker with no-cache; serve hashed85 assets with long cache. Backwards, and users are pinned on a broken build.86- Have an update path: detect the new worker, then either prompt ("A new87 version is ready — reload") or skip waiting and reload, but choose one and88 test it.89- Never cache API responses in the same policy as the shell.90- Be able to answer "which build is this user on?" — put the build id in the UI91 and in error reports.92- Test the upgrade, not just the install: load the old version, deploy, reload,93 confirm the new one arrives.9495## 6. Design for one hand, outdoors, in a hurry9697- Primary actions in the bottom third, where a thumb reaches.98- Touch targets at least 44×44 px with real spacing. Two destructive actions99 should never be adjacent.100- High contrast; do not rely on colour alone for state.101- Never lose typed input on rotation, backgrounding, or an incoming call.102- Confirm destructive actions, and make them undoable where you can.103- Keep the primary flow reachable in as few taps as possible. Count them.104105## 7. Throttle rather than disconnect106107The field is rarely cleanly offline. It is 2G, a half-open connection, a108captive portal, or a connection that accepts the request and never answers.109110- Test at "slow 3G" throttling, not just offline.111- Set request timeouts. A hung fetch with no timeout is an app that appears112 frozen.113- Show progress for anything over a second, and make it cancellable.114- Retry with backoff; give up and tell the user rather than spinning forever.115116If the flow must survive a real loss of coverage, that is a design decision on117its own — see `offline-first-sync`.118119## 8. Battery, data and storage are the user's, not yours120121- Continuous GPS drains a battery in hours. Sample, do not stream, and stop122 when backgrounded.123- Downscale photos on capture. A modern phone camera produces files that are124 both slow to upload on 3G and expensive on a metered plan.125- Cap local caches and evict.126- Release the camera and any wake lock the moment the screen is left.127128## Checklist129130- [ ] HTTPS available for device testing, documented in the README131- [ ] Tested on a real, low-end device132- [ ] Every permission has an explanation, a denied path and a permanent-denial message133- [ ] GPS accuracy read and acted on; geofence sized for the site; server-side check; manual override logged134- [ ] Service worker update path tested by upgrading, not installing135- [ ] Build id visible in the UI136- [ ] Reachable one-handed; 44 px targets; no input lost on rotate or background137- [ ] Tested throttled, not only offline; timeouts on every request138- [ ] Photos downscaled; GPS sampled; caches capped