Offline field service: slow first sync, lost notes, leaked channels
Field-service app: Couchbase Lite 3.x on Android and iOS, Sync Gateway 3.x, 350 technicians who are offline 6–8 hours a day. A job document is ~4 KB plus three photos of ~2 MB each, stored as blobs. The sync function assigns channels region.<region> from a region field on the document and tech.<techId> from assignedTo. The Sync Gateway database holds 180,000 job documents and 40 GB of attachments; delta sync is enabled. Three symptoms: (1) after a technician reconnects at the end of the day, the first pull takes 12–18 minutes before the day's new jobs appear, over hotel Wi-Fi; (2) two technicians edited the same job's notes field and one set of notes vanished with no error shown; (3) an audit found a technician holding 300 documents from a region they had never worked in.
1. Diagnose the 12–18 minute first sync and propose changes that cut it down.
Read the worked answer
Diagnose in this order:
- Attachments dominate. 120 jobs × 3 photos × 2 MB ≈ 720 MB per technician per day. Over hotel Wi-Fi that alone is the 12–18 minutes; the ~500 KB of JSON is noise. Check Sync Gateway logs for attachment bytes versus document bytes - that number usually ends the discussion.
- Channel breadth. If
tech.<id>also pulls every job in the region, or every unassigned job, the initial pull is thousands of documents, most irrelevant to that technician. - Checkpoint loss. After hours offline the replicator resumes from its checkpoint. If the local database was purged, or the checkpoint was lost after an app upgrade, it re-pulls the channel from the beginning.
Fixes, highest impact first:
- Do not sync photos through Sync Gateway. Upload them directly to object storage on a background queue and sync only the URL and a thumbnail. This is the single biggest win and removes 40 GB from the sync path.
- If photos must sync, put them in separate
photo::<jobId>::<n>documents so the job document syncs without them, and pull photo documents on demand rather than as part of the day's job pull. - Subscribe to the narrowest channel set:
tech.<id>for assignments,region.<region>only for reference data that genuinely needs to be offline. - Confirm
deltaSyncis enabled and that revision history settings are not forcing full re-pulls; verify with a measured pull before and after each change rather than changing three things at once.
2. Explain why one technician's notes disappeared and choose between a custom conflict resolver and a data-model change.
Read the worked answer
Why notes vanish: Couchbase Lite and Sync Gateway resolve concurrent edits deterministically, not by wall clock. When two revisions of the same document are pushed, a winner is chosen by the revision algorithm - effectively arbitrary from the user's point of view. The losing revision is not merged; it remains as a conflicting revision in the tree. The technician whose notes lost gets no error, because their write succeeded locally.
Diagnose: log conflicts in the app (a resolver that records both revisions), and inspect the document's revision tree on the server. Confirm these are real concurrent edits (push without an intervening pull) rather than one client overwriting after pulling.
Options:
- Custom conflict resolver in Couchbase Lite. Merge at field level - take
notesfrom the revision with the laterupdatedAt, or concatenate both with attribution. The resolver runs on the client that detects the conflict during replication, so every technician's app must ship identical logic, and the merged result must itself be written back as a new revision. - Design the conflict out. Make notes append-only: each note is its own document (
note::<jobId>::<techId>::<uuid>) in the same channel. Two technicians writing notes never touch the same document, and the UI merges them by timestamp. - Server-side validation is not conflict resolution. The sync function can reject malformed writes but cannot merge two revisions.
Recommendation: append-only note documents for the shared human-authored field, with a custom resolver as a safety net for the remaining shared fields and the merge rule documented and tested. Resolvers handle exceptions; the model should prevent the common case.
3. Find the access-control defect behind the leaked region documents and describe the corrected sync function.
Read the worked answer
Root cause: the sync function derives region.<region> from a field on the document, and documents are writable by clients. Either a technician edited region on a job, or a job was created with the wrong region - either way the server used client-supplied content to make an authorization decision. Channels are an access boundary; never compute them from unverified data.
Corrected pattern:
function sync(doc, oldDoc) {
requireUser(); // authenticated, not guest
if (doc.type !== "job") throw({forbidden: "reserved type"});
if (oldDoc && oldDoc.region !== doc.region)
throw({forbidden: "region is immutable"});
requireAccess("region." + doc.region); // caller must already belong
channel("region." + doc.region);
channel("tech." + doc.assignedTo);
}
requireAccess(channel) fails the write unless the authenticated user already has access to that channel, so a client cannot place a document into a region it does not belong to. requireRole("dispatcher") around reassignment restricts who may change assignedTo; otherwise a technician could reassign a job into their own tech. channel.
Operational follow-up: grant channel access through the Admin API when accounts are provisioned, tied to roster data - never to a document field. Periodically reconcile each user's channel set against the roster, and alert on bursts of 403s from the sync function: they indicate either a client bug or someone probing the boundary.