Engineering notes on building a sign-in kiosk that keeps working when the network does not. Written while building an open-house sign-in app for iPad, but the patterns apply to any unattended capture form.
An offline-tolerant app tries the network first and falls back to local storage when the request fails. That sounds equivalent, and it is not: it makes the failure path the least-exercised code in the product. It runs only when something is already going wrong, which is exactly when you cannot afford a bug.
An offline-first app inverts the order. The local write is the primary action and always succeeds; sync is a separate background concern. The submission is durable the moment the user taps the button, network or not.
A cache is allowed to be evicted. Captured leads are not. On iOS we use GRDB over SQLite with WAL journaling, so writes are atomic and survive a hard kill mid-transaction — which, on a kiosk device that gets closed by a stranger, happens more than you would like.
try db.write { db in
try Visitor(name: name, email: email, capturedAt: .now).insert(db)
}
// durable here, regardless of connectivity
Once the same record can change in two places, you need a resolution rule chosen up front. Options, roughly in order of how much work they are:
Pick the weakest one that is actually correct for your data. For capture forms that is almost always append-only.
The single most useful test: put the device in airplane mode, complete the form, force-quit the app, reopen it, and confirm the entry is still there. Then restore the network and confirm exactly one copy reaches the server. Most tools that claim offline support fail one of those two halves.
Retries are guaranteed, so the server must dedupe. Generate a UUID on the device at capture time and treat it as the idempotency key. Do not key on (email, listing) — the same visitor legitimately signs in at two open houses.