Consider a registration desk on load-in day when the venue Wi-Fi drops. The check-in app keeps accepting taps, but every badge prints from a cached record. Two attendees with the same last name get checked in twice. A sponsor’s lead-capture tablet shows scans that never reach the CRM. The app does not crash. It goes blind, and nobody notices until a speaker asks why the session roster is missing names.
This is not a story about bad Wi-Fi. It is a story about a registration system that treated offline as an error state instead of a first-class mode. The fix is not “better Wi-Fi.” The fix is a sync contract: what the device owns, what the server owns, how conflicts resolve, and how a human can audit the result before doors open.
What actually failed
Three distinct failures compounded:
- No local write path. The app read from a memory cache but wrote only to the API. When the API became unreachable, check-ins were dropped silently after a 5-second timeout.
- No conflict rule. Two desk tablets edited the same attendee record — one changed the badge name, the other added a dietary flag. The server accepted whichever request arrived last, and the earlier edit vanished.
- No reconciliation surface. There was no queue view, no per-device log, and no way for the registration lead to see what had been captured offline versus what had synced.
Each of these is a design choice, not an accident. And each has a documented, reversible fix.
Offline mode is a storage decision first
If the check-in app cannot write to durable local storage, offline mode is theater. The platform you build on determines what “durable” means.
On Android, the Room persistence library provides an abstraction over SQLite with compile-time verification of queries and streamlined migration paths. The documentation is explicit that the common use case is caching structured data so users can browse while offline — but for check-in you need more than browse. You need insert, update, and a queue table that survives process death.
On Apple platforms, Core Data is the equivalent persistence layer; the Core Data documentation is the starting point for the managed object model and persistent store configuration. The operational point is the same: the local store must be the source of truth for the duration of the offline window, not a mirror of a server the device cannot reach.
On the web, IndexedDB is a transactional, asynchronous, object-oriented database suitable for significant amounts of structured data. It follows a same-origin policy, which matters if your check-in UI is served from a different origin than your API. It also has browser-specific storage quotas and eviction behavior — the MDN page links to the quota and eviction criteria, and you should read them before you assume a 200 MB local cache will survive a two-day event.
If you are using a managed backend, know its offline semantics before you promise them to your registration lead. Cloud Firestore, for example, supports offline persistence on Android, Apple, and web, caches documents the app is actively using, and synchronizes local changes when the device reconnects. The same page states plainly: “For multiple changes to the same document, it’s last write wins.” That is a documented behavior, not a bug — and it is the wrong default for registration data where two operators may edit the same attendee.
The conflict rules you actually need
Registration data has a property that generic sync frameworks do not assume: different fields have different owners. A badge name is owned by the registration desk. A dietary flag is owned by the attendee. A sponsor scan is owned by the sponsor integration. A session seat assignment is owned by the program team. If you sync at the record level, you will lose edits. If you sync at the field level, you can define a rule per field.
Three rules cover most conference check-in scenarios:
1. Field-level last-write-wins with a server timestamp
For fields where any operator’s edit is equally valid — check-in status, badge printed flag, arrival time — last-write-wins is acceptable if the write carries a server-assigned timestamp and the device clock is not trusted. The Firestore documentation above confirms last-write-wins is the default for same-document changes; if you use it, make the granularity a field, not a document.
2. Append-only for events that must not be lost
Check-in events, sponsor scans, and session attendance should be append-only records with a device-generated UUID and a monotonic local sequence number. Two devices checking in the same attendee produce two events, not one overwritten record. The reconciliation step deduplicates by attendee ID and keeps the earliest timestamp. This is the same principle behind the Compensating Transaction pattern: when you cannot roll back, you record forward and reconcile later. Microsoft’s guidance is explicit that compensating transactions are application-specific and that steps should be idempotent so they can be retried safely.
3. Explicit conflict queue for ambiguous fields
For fields where two edits genuinely conflict — legal name, company, title — do not auto-resolve. Write both versions to a conflict table with device ID, operator ID, local timestamp, and server receipt timestamp. Surface the queue to the registration lead at a defined checkpoint (see below). A human decides. This is the “human in the decision-making process” case that the Compensating Transaction guidance calls out for high-impact or hard-to-automate decisions.
Use a patch format, not a full-record overwrite
The reason record-level sync loses data is that each device sends the whole record. If device A sends a record without the dietary flag and device B sends a record with it, the server cannot tell whether the flag was removed or simply not known to device A.
Send patches instead. RFC 6902 (JSON Patch) defines a JSON document structure for expressing a sequence of operations — add, remove, replace, move, copy, test — to apply to a target document. Two properties matter for check-in:
- The
testoperation lets you assert the current server value before applying a change. If the test fails, the patch is rejected and the device knows it must re-read. - RFC 6902 states that when used with HTTP PATCH, the method is atomic: if any operation in the patch fails, no changes are made. That gives you a clean all-or-nothing write per field group.
A practical patch for a check-in event looks like this:
[
{ "op": "test", "path": "/attendees/4821/checked_in", "value": false },
{ "op": "replace", "path": "/attendees/4821/checked_in", "value": true },
{ "op": "add", "path": "/attendees/4821/checkin_events/-", "value": {
"device_id": "desk-03",
"operator_id": "nrook",
"local_seq": 117,
"local_ts": "2026-09-29T08:14:02Z"
}}
]
If the attendee was already checked in by another device, the test fails, the patch is rejected atomically, and the device logs a conflict instead of silently overwriting.
Detecting blindness before it matters
The app in the opening scenario did not know it was offline. That is the failure to fix first. Three signals, checked every 10 seconds during doors-open hours:
- Reachability, not association. A device can be associated with an access point and still have no route to the API. Probe the API health endpoint, not the Wi-Fi SSID.
- Write acknowledgment latency. If the p95 write acknowledgment exceeds 2 seconds for three consecutive attempts, switch the UI to offline mode explicitly. Do not wait for a timeout.
- Queue depth. If the local outbound queue exceeds a threshold you set — 50 events is a reasonable starting point for a single desk — surface a visible indicator to the operator. The operator should never have to guess whether their taps are reaching the server.
Firestore’s fromCache metadata property is one example of a platform-level signal: the documentation notes that when fromCache is true, the data came from the cache and may be stale or incomplete. If your platform exposes an equivalent, use it. If it does not, build one.
The reconciliation checkpoint
Offline mode is not finished when the network returns. It is finished when a human has reviewed the conflict queue and confirmed the attendee count. Define a checkpoint:
- T+15 minutes after network restoration: all devices must report queue depth zero or a non-zero conflict count.
- T+30 minutes: registration lead reviews the conflict queue. Each conflict shows both values, both device IDs, both timestamps, and a one-click resolution.
- T+60 minutes: attendee count from the server is compared against the sum of unique check-in events. A variance greater than zero is investigated before the next session block.
This is the same discipline the Compensating Transaction guidance describes: record progress so the process can resume from the point of failure, correlate the original operation and its compensation end-to-end, and raise an alert with detailed failure information when manual intervention is required.
Sponsor integrations: read-only by default
The sponsor tablet that showed unsynced scans is a separate problem with the same root cause. Sponsor integrations should not write to the attendee record during the event. They should append scan events to a sponsor-specific queue, tagged with the sponsor ID and the attendee ID, and sync on the same schedule as check-in events. The attendee record is not modified. The sponsor gets a lead list after reconciliation, not during.
This respects attendees because it limits what the sponsor integration can change. It also eliminates an entire class of sync conflicts: a sponsor scan can never overwrite a registration desk edit if it never writes to the same fields.
What to test before doors open
Every fix above is reversible and testable. Run these four tests during load-in, not on the morning of the event:
- Airplane mode check-in. Put one desk tablet in airplane mode. Check in 20 attendees. Restore network. Confirm 20 events arrive, zero duplicates, zero lost.
- Concurrent edit. Two tablets edit the same attendee’s badge name within 30 seconds, both offline. Restore network. Confirm the conflict queue shows both edits and neither was silently dropped.
- Queue depth alarm. Disconnect the API (not the Wi-Fi). Confirm the operator-facing indicator appears within 10 seconds and the queue depth is visible.
- Reconciliation drill. Have the registration lead walk through the conflict queue with a test conflict. Time it. If it takes more than 2 minutes per conflict, the UI is not ready.
If any test fails, you have a reversible fix: revert the sync rule, revert the patch format, or revert the offline mode toggle. None of these require a code deploy during the event if you have feature flags.
FAQ
Can we just use last-write-wins for everything?
No. Last-write-wins is documented behavior in Firestore for same-document changes, and it is acceptable for fields where any operator’s edit is equally valid. It is not acceptable for fields where two operators may legitimately disagree, or for events that must not be lost. Use field-level last-write-wins for status fields, append-only events for check-ins and scans, and a conflict queue for identity fields.
Do we need a CRDT or operational transform library?
For most conference check-in workloads, no. CRDTs and operational transforms solve collaborative text editing and similar problems where every keystroke must merge. Registration data is field-oriented and event-oriented. A patch format with a test operation, plus an append-only event log, covers the realistic conflict surface without the operational complexity of a CRDT.
How much local storage do we need?
Budget for the full attendee list plus a 24-hour event queue. A 5,000-attendee event with 10 fields per record is roughly 2–5 MB of structured data. The queue is smaller. The constraint is not size; it is eviction. Read your platform’s storage quota and eviction documentation — IndexedDB’s behavior differs by browser, and Firestore’s default cache threshold is 100 MB with periodic cleanup of older, unused documents.
What if the venue Wi-Fi never comes back?
That is a different failure mode with a different fix: a local-only mode where the device never attempts to sync, and reconciliation happens after the event from exported device logs. The sync contract above still applies — you still need append-only events and a conflict queue — but the checkpoint moves to post-event. Decide the trigger for local-only mode before doors open: for example, if the API is unreachable for 30 continuous minutes, switch to local-only and notify the registration lead.
How does this connect to the room-to-chat handoff?
The same offline and sync-conflict rules apply when session attendance data moves from the room to the chat platform. If the handoff is not idempotent, attendees appear twice or not at all. The room-to-chat handoff failure analysis covers the specific failure modes at that boundary; the reconciliation checkpoint described here is the upstream control that makes that handoff reliable.
Copyable artifact: offline sync contract for the registration lead
Paste this into the registration runbook. It is written for the registration lead, not for engineering.
OFFLINE SYNC CONTRACT — REGISTRATION DESK
Event: ______________ Date: ______________
Registration lead: ______________
1. OFFLINE TRIGGER
- API health probe fails 3 consecutive times (10s interval) → offline mode ON
- Operator-facing indicator must appear within 10 seconds
- Queue depth visible to operator at all times
2. WRITE RULES
- Check-in status: field-level last-write-wins, server timestamp
- Check-in events: append-only, device UUID + local sequence number
- Identity fields (name, company, title): conflict queue, no auto-resolve
- Sponsor scans: append-only to sponsor queue, never write attendee record
3. CONFLICT QUEUE
- Shows: field, device A value, device B value, device IDs, timestamps
- Resolution: one click per conflict, logged with operator ID
- Target: under 2 minutes per conflict
4. RECONCILIATION CHECKPOINT
- T+15 min after network restore: all devices report queue depth
- T+30 min: registration lead reviews conflict queue
- T+60 min: server attendee count vs. unique check-in events
- Variance > 0: investigate before next session block
5. LOCAL-ONLY MODE
- Trigger: API unreachable 30 continuous minutes
- Action: notify registration lead, switch to local-only, export device logs post-event
6. PRE-DOORS TESTS (all four must pass)
[ ] Airplane mode: 20 check-ins, restore, zero loss, zero duplicates
[ ] Concurrent edit: two offline edits, conflict queue shows both
[ ] Queue depth alarm: API down, indicator within 10 seconds
[ ] Reconciliation drill: registration lead resolves test conflict under 2 minutes
Signed off by: ______________ Time: ______________
The point of the contract is not the document. It is that the registration lead can answer three questions at any moment during doors-open: Is the app online? How many events are queued? Who resolves conflicts? If any of those answers is “I don’t know,” the app is blind, and the fix is not more Wi-Fi.