Field data 13 min read

Offline field data collection patterns: Field Maps, QField and custom apps

Crews edit without a signal, the office edits the same features, and everything has to reconcile at the end of the day. The three sync architectures we deploy, how each handles conflicts, and the design rules that keep field data trustworthy.

On this page
  1. Why offline is hard
  2. The sync cycle, animated
  3. Pattern 1: Field Maps + sync-enabled services
  4. Pattern 2: QGIS project + QField
  5. Pattern 3: custom app with a change log
  6. Conflict policies
  7. Design rules that survive the field
  8. Choosing a pattern
  9. Takeaways

Field data collection fails in the same three places on every platform: the moment a device goes offline, the moment two people edit the same feature, and the moment someone trusts a timestamp set by a phone. The tools differ — ArcGIS Field Maps, QField, a custom app on the Maps SDKs or MapLibre — but the architecture underneath has to answer the same questions. Here are the three patterns we deploy and how each one answers them.

Why offline is hard

Online editing is easy because the server is the single source of truth and every edit is a round trip. Offline editing gives every device its own copy of the truth for hours or days. Reconciling those copies means dealing with:

  • Identity of features. New features created offline have no server ID yet. Two devices can create “the same” hydrant.
  • Concurrent edits. The office changes a valve while a crew changes the same valve. Somebody has to lose, or the two edits have to merge.
  • Attachments. Photos are large, arrive later than rows, and must not be orphaned when a row is rejected.
  • Time. Device clocks drift and get changed. Ordering edits by client time is a trap.
  • Partial failure. Uploads stop half-way. The protocol must be resumable and idempotent, or crews resend and duplicate.

The sync cycle, animated

Whatever the product, a well-behaved offline workflow goes through the same six states. Hover to pause, or pick a phase.

Device (offline replica)

  • hydrant-88 · flow 1200
  • valve-12 · open
createReplica → .geodatabase / .gpkg

Server (feature service)

  • hydrant-88 · flow 1200
  • valve-12 · open

1 · Take offline

The app requests a replica for the work area. The server packages features, attachments and a replica ID; the device stores them locally.

Replica → offline edits → concurrent server edit → upload deltas → download and detect conflicts → consistent. The conflict in step 5 is resolved by policy, never by accident.

Pattern 1: Field Maps + sync-enabled feature services

The default for organisations on ArcGIS. Enable Sync on a feature layer (hosted, or an enterprise geodatabase layer with GlobalIDs and archiving or branch versioning), define preplanned offline map areas, and Field Maps handles replica creation, local editing and synchronisation. Survey123 and QuickCapture use the same service capability.

create-replica.shShell
1# Ask the feature service for an offline replica of two layers within an extent
2curl -s "$SERVICE/FeatureServer/createReplica" \
3 -d f=json -d token="$TOKEN" \
4 -d replicaName="crew-07-north" \
5 -d layers="0,3" \
6 -d geometryType=esriGeometryEnvelope \
7 -d geometry='{"xmin":8022000,"ymin":2640000,"xmax":8031000,"ymax":2649000,"spatialReference":{"wkid":102100}}' \
8 -d returnAttachments=true \
9 -d attachmentsSyncDirection=bidirectional \
10 -d syncModel=perLayer \
11 -d dataFormat=sqlite \
12 -d async=true
13# → { "statusUrl": ".../jobs/<id>" } → poll until "Completed", then download the .geodatabase
The REST call Field Maps makes for you. Useful to script preplanned areas or to debug what the device is actually asking for.

Conflict handling depends on where the data lives. Hosted feature layers apply edits as they arrive: for a row edited on both sides, the last synchronisation to reach the server wins, field by field is not attempted. Enterprise geodatabase layers using versioning give each replica its own version; reconcile and post can detect row-level conflicts and, with branch versioning, surface them to a reviewer before they reach the default version. Editor tracking (creator, created, editor, edited) is set by the server at apply time — use those fields, not device time.

Pattern 2: QGIS project + QField

For teams on QGIS, the field app is QField and the project is the contract: forms, constraints, default values and offline layers are designed in QGIS and shipped to the device. Two synchronisation routes exist.

  1. 01

    QFieldCloud

    The project and its GeoPackage layers are hosted in QFieldCloud (SaaS or self-hosted). Devices push delta files — ordered lists of feature changes — which the server applies to the master layers in sequence. Deltas that cannot be applied are flagged for review rather than dropped.
  2. 02

    Direct to PostGIS with an offline copy

    Layers stay in PostGIS; the QGIS Offline Editing plugin (or the QFieldSync packaging step) copies them into a GeoPackage, the crew edits, and synchronisation writes the changes back with the plugin's change log. Simple, self-contained, but conflict detection is coarse and the process runs on a desk QGIS rather than on the device.
package_for_qfield.pyPython
1from qgis.core import QgsProject, QgsOfflineEditing, QgsRectangle
2
3project = QgsProject.instance()
4project.read("/data/water_network.qgz")
5
6# Layers that go offline; basemap layers stay as XYZ/WMTS and are cached separately
7offline_ids = [l.id() for l in project.mapLayers().values()
8 if l.customProperty("QFieldSync/action") == "offline"]
9
10area = QgsRectangle(8022000, 2640000, 8031000, 2649000) # crew 07, north sector
11ok = QgsOfflineEditing().convertToOfflineProject(
12 "/export/crew-07", "field.gpkg", offline_ids,
13 onlySelected=False, containerType=QgsOfflineEditing.GPKG,
14 layerNameSuffix="")
15print("packaged" if ok else "failed", len(offline_ids), "layers")
Packaging a project for offline use from PyQGIS — the same steps the QFieldSync plugin performs from its dialog.

Pattern 3: custom app with a change log

When forms, workflows or licensing do not fit the off-the-shelf apps, we build the collection app — Swift or Kotlin native, Flutter or React Native — and own the sync protocol. Two sub-patterns:

  • ArcGIS Maps SDKs for Native Apps. A mobile geodatabase, GeodatabaseSyncTask and the same sync-enabled feature service as Pattern 1. You get Esri's replica protocol and conflict semantics for free and design only the UI.
  • Your own API. SQLite/GeoPackage on the device, PostGIS on the server, and an append-only change log in between. More work, complete control — including field-level merges and a review queue.

The change log is the heart of the second option. Every edit is a row that can be replayed, rejected or merged, and every batch is idempotent:

sync/types.tsTypeScript
1export type Change = {
2 id: string; // ULID, generated on the device
3 deviceId: string; // stable per install
4 layer: "hydrant" | "valve" | "inspection";
5 featureId: string; // UUID (GlobalID); new features mint one offline
6 op: "insert" | "update" | "delete";
7 baseVersion: number; // server row version the edit was made against
8 fields: Record<string, unknown>; // changed fields only, never the full row
9 attachments?: { id: string; sha256: string; bytes: number }[];
10};
11
12export type SyncRequest = { replicaId: string; sinceSeq: number; changes: Change[] };
13
14export type SyncResponse = {
15 applied: string[]; // change ids accepted
16 conflicts: { changeId: string; serverRow: Record<string, unknown>; policy: "server-wins" | "merged" | "review" }[];
17 serverChanges: Change[]; // everything since `sinceSeq`, in server order
18 seq: number; // new watermark to store on the device
19};
A minimal change-log schema. ULIDs sort by device time but are only used for ordering within one device; the server assigns the authoritative sequence.
apply_change.sqlSQL
1UPDATE valve
2 SET status = COALESCE(:status, status),
3 pressure = COALESCE(:pressure, pressure),
4 row_version = row_version + 1,
5 edited_by = :user_id,
6 edited_at = now() -- server clock, never the device's
7 WHERE global_id = :feature_id
8 AND row_version = :base_version
9RETURNING row_version;
Optimistic concurrency in one statement: the update only lands if the row is still at the version the device edited. Zero rows updated means a conflict.

Conflict policies

A conflict is a row edited on the device and on the server between replica creation and sync. There are only four honest ways to handle one:

PolicyWhat happensUse when
Server winsThe field edit is rejected and returned to the device with the server row.Office data is authoritative (asset registers, network topology).
Client winsThe field edit overwrites the server row.The field observation is the whole point (inspections, condition scores).
Field-level mergeNon-overlapping fields are combined; overlapping ones fall back to another policy.Wide rows edited by different roles (crew fills condition, office fills work order).
Review queueBoth versions are kept; a person picks. Nothing is applied until they do.Regulated data, or when the two edits disagree on geometry.
Whichever policy you pick, log the losing version. Silent overwrites are how field programmes lose trust.

Design rules that survive the field

  1. GlobalIDs everywhere. Never let a device depend on an OBJECTID; they are assigned on apply and differ between replicas.
  2. Server time is the only time. Store the device timestamp as an attribute if you must, but order and audit with server-set fields.
  3. Send deltas, not rows. Changed fields only, so that two people editing different columns never conflict at all.
  4. Idempotent batches. Every change has an ID; re-sending a batch after a dropped connection must be a no-op on the server.
  5. Attachments after rows, by hash. Upload the row, then the photo, keyed by content hash — duplicates cost nothing and orphans are detectable.
  6. Scope the replica. A crew's sector, not the whole city. Smaller replicas sync faster and conflict less.
  7. Test with two devices and a clock set wrong. If the workflow survives that, it survives the field.

Choosing a pattern

Field Maps + sync servicesQGIS + QFieldCustom app
PlatformArcGIS Online / EnterpriseQGIS, QFieldCloud or PostGISAny backend; Esri SDKs or your own API
Time to first crewDaysDaysWeeks to months
Forms and rulesSmart forms, ArcadeQGIS widgets and constraintsAnything you can build
Conflict handlingLast sync wins (hosted) or versioned reconcile (enterprise gdb)Ordered deltas with review (QFieldCloud)Your policy, incl. field-level merge and review queue
LicensingNamed usersOpen source (+ QFieldCloud plan if hosted)Your choice
Best forEsri shops that want a supported, configurable appOpen-source shops, budget-constrained programmesBespoke workflows, offline-first products, regulated data

Our own Geo Data Collector app on iOS is the third pattern in product form: an in-app form designer, fully offline capture and export to GeoJSON or GeoPackage — built because a client's workflow fitted none of the configurable apps.

ArcGIS Field Apps: Field Maps, Survey123, QuickCaptureComplete field workflows on the Esri stack — forms, offline areas, tracking and the dashboards on top.QField mobile projects on QGISProject design, offline layers, QFieldCloud or PostGIS synchronisation.Geo Data Collector (iOS)Our offline field data collection app with a built-in form designer.

Takeaways

Takeaways

  • 01The protocol is the product. Replica, deltas, conflict detection and an idempotent apply step — every good offline app has them, whatever the logo.
  • 02Pick a conflict policy per layer and log the loser. Server-wins for assets, client-wins for observations, review for regulated data.
  • 03GlobalIDs and server time are non-negotiable; device IDs and device clocks are hints.
  • 04Configurable first, custom when the workflow demands it. Field Maps or QField cover most programmes; build when forms, licensing or offline-first UX say otherwise.

Start a project

Have a project in mind? Let's build it.

Share your requirement and get an obligation-free consultation, a technology recommendation, and a quote within 48 business hours.

Chat on WhatsApp