Shift Handover Data Sync in Offline-First Guard Apps
Shift handover is the most data-sensitive moment in a security guard's workflow. The outgoing guard has hours of accumulated patrol data, checkpoint scans, and possibly unsynced incident reports on their device. The incoming guard needs to see the current state of the site: open incidents, missed checkpoints, and notes from the previous shift. If the app handles this transition poorly, data is lost, duplicated, or delivered out of order. In an offline-first architecture, where both devices may have been disconnected for hours, the sync challenge during handover is significant.
This post covers the technical strategies for making shift handover sync reliable, from conflict resolution at the data layer to bandwidth optimization for sites with limited connectivity.
What Needs to Sync at Shift Boundaries
Not all data needs to sync at the same time or with the same priority. Breaking the sync into distinct phases prevents large payloads from blocking critical information.
At Shift End (Outgoing Guard)
The outgoing guard's device should prioritize uploading in this order:
- Pending incident reports (text and metadata only, without media). These are the highest-value records and the smallest payloads. A typical incident report without attachments is under 2KB.
- Checkpoint scan records. A full shift of checkpoint data for a 20-checkpoint site is roughly 10-15KB.
- Shift summary and handover notes. Free-text notes the outgoing guard writes for the incoming guard. Under 1KB.
- Incident media files. Photos and video associated with reports. These can be megabytes each and should sync in the background after the critical data is uploaded.
- GPS trace data. The continuous location log from the shift. This can be 50-200KB depending on polling interval and shift duration, and is lowest priority.
At Shift Start (Incoming Guard)
The incoming guard's device needs to pull:
- The current site configuration: checkpoint locations, patrol routes, and any schedule changes.
- Open incidents from the previous shift that require follow-up.
- Handover notes from the outgoing guard.
- The guard's own assignment details: which zones to cover, expected patrol frequency, supervisor contact info.
This pull should be a single API call that returns a bundled JSON payload, typically under 50KB for a standard site. The app stores this in the local database and is ready for the shift to begin, regardless of what happens to connectivity afterward.
CRDT-Based Conflict Resolution
During shift handover, it is common for multiple people to touch the same data. A supervisor might update an incident's priority on the web dashboard while the outgoing guard adds a final note, and the incoming guard changes the status to "acknowledged." Without a conflict resolution strategy, whichever write reaches the server last silently overwrites the others.
Field-Level Last-Write-Wins
The simplest practical approach is last-write-wins applied at the field level rather than the record level. Each field in a record carries its own updated_at timestamp. When the server receives an update, it compares each field's timestamp against the current value. Fields with a newer timestamp are accepted; fields with an older timestamp are ignored. This means the supervisor's priority change, the guard's description update, and the incoming guard's status change all merge correctly because they modify different fields.
Implementation requires storing a vector of timestamps alongside each record. In the local database, this can be a JSON column or a separate metadata table. On sync, the client sends the full record with per-field timestamps, and the server returns the merged result.
Grow-Only Sets for Tags and Flags
Some fields are additive by nature. Tags on an incident report ("police notified," "client contacted," "maintenance dispatched") should never be removed by a concurrent edit. A Grow-Only Set (G-Set) is the simplest CRDT that handles this. Each device can add tags locally, and when sets from different devices are merged, the union is taken. No tag is ever lost due to a conflict.
For fields that need both add and remove operations, use a Two-Phase Set (2P-Set) or an Observed-Remove Set (OR-Set). In practice, most guard tour applications only need G-Sets because removing a tag from an incident report is rare and can be handled through a separate "corrections" workflow that requires supervisor approval.
When Full CRDTs Are Overkill
Libraries like Automerge and Yjs provide sophisticated CRDT implementations that handle arbitrary text editing, nested objects, and complex merge scenarios. For guard tour apps, this level of complexity is rarely justified. The data model is primarily append-only (new scans, new reports, new notes), and the few editable fields (incident description, priority, status) are well served by field-level LWW. Introducing a full CRDT framework adds bundle size, memory overhead, and a learning curve for the development team without a proportional benefit.
Pending Report Queues and Ordering
When the outgoing guard's device reconnects during handover, it may have dozens of pending records spanning multiple hours. The sync worker must upload these in the correct order to maintain referential integrity on the server.
Checkpoint scans are independent and can be uploaded in any order. Incident reports, however, may reference a specific checkpoint scan ("incident occurred during scan of checkpoint 7"). The scan record must reach the server before the incident report that references it, or the server will reject the foreign key reference.
The solution is a dependency-aware upload queue. Each record declares its dependencies (if any) as foreign key references. The sync worker topologically sorts the pending queue so that dependencies are uploaded first. In practice, this means checkpoint scans upload before incident reports, and incident report text uploads before media attachments.
If a dependency fails to upload, all records that depend on it remain in the queue rather than being sent out of order. The worker retries the failed dependency with backoff, and downstream records follow once it succeeds.
Bandwidth Optimization for Constrained Sites
Many guard sites have severely limited bandwidth. Underground parking structures rely on weak cellular signals. Rural industrial sites may have satellite internet with 600ms latency and 1 Mbps throughput. Remote mining operations sometimes have connectivity windows of only 30 minutes per day when a vehicle passes through a coverage zone.
Delta Sync
Instead of syncing full records, send only changed fields. The client tracks which fields have been modified since the last successful sync and sends a patch payload containing just those fields with their timestamps. For a 2KB incident report where only the status changed, the delta payload might be 50 bytes. Over a shift with hundreds of records, this reduces total sync volume by 80-90%.
Compression
All sync payloads should be gzip-compressed. JSON compresses well, typically achieving 70-85% reduction. On Android, OkHttp handles gzip transparently when the server supports it. On iOS, URLSession does the same. For the media upload path, images should be compressed before upload (see our post on photo and video evidence in low-bandwidth environments), and video should use chunked upload with resume support.
Sync Windows
For sites with intermittent connectivity, the app should detect when bandwidth is available and aggressively sync pending data during those windows. Android's WorkManager supports constraints like NetworkType.CONNECTED combined with NetworkType.UNMETERED to prefer Wi-Fi. On iOS, BGProcessingTask with requiresNetworkConnectivity provides similar behavior. The key is that the sync worker runs automatically whenever conditions are met, without the guard needing to take any action.
Handling Merge Conflicts in Incident Reports
Despite field-level LWW and CRDTs, there are cases where automated merging is not appropriate. If two people edit the same free-text description field concurrently, field-level LWW will silently discard one version. For low-stakes fields like internal notes, this is acceptable. For the primary incident description that may be used in legal proceedings, it is not.
The pragmatic approach is to detect these conflicts and surface them to a supervisor for resolution. When the server receives an update to a text field and the incoming base_version (the version the client started editing from) does not match the current server version, the server stores both versions and marks the field as conflicted. The supervisor's dashboard shows a diff view with both versions and lets them produce a merged result.
This conflict detection requires versioning each field. A simple incrementing integer per field, stored alongside the timestamp, is sufficient. The client sends its base_version with each update. If base_version matches the server's current version, the update is clean. If not, it is a conflict.
Shared vs. Personal Devices
Shift handover introduces a complication when guards share devices. If the outgoing guard logs out and the incoming guard logs in on the same phone, the local database contains the previous guard's data. The app must handle this gracefully: pending sync records from the previous guard's session should continue to sync in the background even after the new guard logs in. The new guard's session should not have access to the previous guard's unsynced data in the UI, but the sync worker should process it transparently.
Implement this by tagging all local records with a session_id tied to the guard's authentication token. The UI filters by the current session. The sync worker processes all pending records regardless of session.
DEVSFLOW Guarding builds reliable shift handover sync for offline-first guard tour apps. If your guards are losing data between shifts, let's fix that.