Saved vs Synced in Offline Apps
Offline forms need three distinct states: saved on the device, queued for upload, and confirmed by the server. Treating them as one status is what causes lost work and duplicate records. The practical fix is to show each state separately, store drafts locally, queue uploads, and only mark a record as synced after the server returns a durable identifier.
Why one status is not enough
A form that says saved after writing to local storage tells the user nothing about whether the office has the record. If the device is lost, damaged, or wiped before upload, the work is gone. If the app retries an upload it already completed, the server may create a duplicate. Users then lose trust in the app and start keeping paper copies, which defeats the purpose.
Separate the states in the interface and in the data model. Local draft means the content exists only on the device. Pending upload means it is queued and will be sent when connectivity returns. Confirmed means the server has stored it and returned an identifier the app can rely on. A conflict state is also needed when the server already holds a different version of the same record.
Designing the local draft
Write drafts to durable storage as the user types or at short intervals, not only when they tap save. Include a client-generated identifier so the same draft can be recognized across retries. Keep the draft until the server confirms the record, then remove it or mark it as archived.
Show the draft state plainly in the form header and in any list of records. A field technician who sees draft knows the note has not left the phone. That single word prevents the assumption that the office already has the information.
Queueing uploads and retrying
A pending upload should be a durable queue entry, not an in-memory task. Store the payload, the client identifier, and a retry count. On reconnect, send entries in order and handle partial failures. If the server returns a validation error, do not retry blindly; surface the problem to the user and keep the draft.
Retries need backoff and a cap. A record that fails repeatedly should be flagged for attention rather than retried forever in the background. The user should be able to see what is pending and trigger a manual attempt.
Confirming the server save
Only mark a record as synced after the server responds with a durable identifier and, ideally, a version or timestamp. The client should store that identifier so later edits can reference the same record. Without it, a retry after a timeout can create a second copy.
Suppose a field-service app sends a job note and the connection drops after the server has written the record but before the response arrives. On the next attempt, the client sends the same client identifier. If the server treats that identifier as unique, it returns the existing record instead of creating a duplicate. That is a decision about idempotency, and it is worth testing before release.
Conflicts and device loss
A conflict occurs when the server has a newer version than the one the client is trying to upload. The app should not silently overwrite either side. Present both versions, or at least the differing fields, and let the user choose. Record the resolution so the history is clear.
Device loss is a separate risk. Local drafts are only as safe as the device. If the work matters, consider encrypting local storage and requiring a passcode. For high-value records, a periodic background upload when connectivity is available reduces the window of loss. None of this removes the need to tell the user which state a record is in.
A practical acceptance test
Before release, run this test on a real device. Create a note, then enable airplane mode and confirm the app shows draft, not synced. Close and reopen the app and confirm the draft is still there. Disable airplane mode and confirm the state changes to pending, then to confirmed only after the server responds. Then repeat the upload with the same client identifier and confirm the server does not create a second record.
The test passes only if the confirmed state appears after the server response and the duplicate check holds. If the app shows confirmed while offline, or if a retry creates two records, the implementation is not ready. Web and native implementations differ in how they store drafts and schedule background work, so verify the behavior on the target devices rather than assuming one approach covers all.
Continue reading
A practical next step
Explore the related Devign work, then discuss your requirements before committing to a project scope.