ADR-0043: FieldForce background location — flutter_background_geolocation, three-gate activation, telemetry routed around PowerSync

Status

Accepted (pending FieldForce Phase 1 implementation)

Tags

fieldforce, mobile, flutter, location, background-execution, feature-flags, privacy, consent, telemetry

Decision

FieldForce includes a background-location capability in its own core_location package, using flutter_background_geolocation as the engine. Digital Worker does not depend on it. Three-gate activation — tracking runs only when ALL three are open:
  1. Feature flag (server-driven) — is tracking allowed for this user/role at all? Also the remote kill-switch.
  2. OS permission (“Always” location) — granted by the user.
  3. Shift / runtime state — the worker is on-the-clock / on an active job.
  • Flipping the flag off cleanly tears down the foreground service, stops GPS, and removes the tracking notification — not merely stops uploading.
  • The flag value is cached locally and evaluated from cache when offline (last-known value; never default-on when unknown).
  • The flag reaches core_location as a watchable value (Stream<bool>/listenable), never a one-shot boolean read at startup — the kill-switch must take effect mid-session (see CLAUDE.md §16 for flag delivery).
  • Location telemetry uploads separately from PowerSync — the engine’s own offline queue posts batches to a dedicated Go endpoint (append-only telemetry). It does not flow through PowerSync buckets.
  • Tracking is shift-scoped by design — only while on-the-clock — for battery and consent legitimacy, with explicit consent + disclosure.

Why

Background location is the most OS-restricted, battery- and privacy-sensitive capability in the product. It needs a foreground service + visible notification on Android, “Always” permission + background mode on iOS, and must survive OEM battery-killers on the budget Android hardware field workers often carry. flutter_background_geolocation is the battle-tested standard that solves motion-based sampling, OEM survival, and resilient offline upload — hand-rolling these reproduces solved, bug-prone work against the project’s “least debugging” priority (ADR-0037). Two must-record nuances:
  1. The three-gate model and the kill-switch teardown. A feature flag controls the app’s behavior, not the OS permission — both are independent gates, and shift state is a third runtime gate. Tracking requires all three open. Critically, flipping the flag off must tear down the service (stop GPS, drop the notification), not just stop uploading — otherwise the app drains battery for a disabled, highest-blast-radius feature. The flag doubles as a remote kill-switch (mirrors the backend’s two-tier kill-switch instinct, ADR-0016).
  2. Telemetry routes around PowerSync. GPS pings are high-volume, append-only, and would bloat PowerSync buckets past their sweet spot (thousands–tens of thousands of rows, not millions — ADR-0040). Location is a distinct bounded concern with different data characteristics, so it gets its own offline queue → dedicated Go endpoint, keeping the sync engine for interactive entities only.
The offline-default rule matters too: server-driven flags need connectivity to refresh, but field devices go offline. The flag is cached and evaluated from last-known value offline, and never defaults on when unknown — privacy-safer to keep tracking off than to start it on an unverified assumption. Rejected alternatives:
  • geolocator + hand-built foreground service (flutter_background_service). Free, but reimplements battery-optimization, motion-detection, and OEM-survival logic. Rejected — the paid engine’s resilience is worth it for production field use.
  • Route location through PowerSync as a synced table. Architecturally uniform, but floods buckets with a GPS firehose and duplicates the engine’s own queue. Rejected — telemetry uses its own path.
  • Always-on tracking (not shift-scoped). Worse battery, weaker consent legitimacy. Rejected — track only on-the-clock.
  • Default-on when flag state is unknown offline. Privacy risk. Rejected — last-known-or-off.
  • No feature-flag gate (compile-time only). Loses the remote kill-switch on the highest-blast-radius capability. Rejected.

Consequences

  • flutter_background_geolocation requires a paid production license — a budgeted project cost.
  • Android shows a persistent tracking notification while active (required); iOS shows the location indicator — both are acceptable and aid transparency.
  • Field-device-specific tuning is expected (OEM battery-killers) — an OS/hardware reality, not a code defect; mitigated, not eliminated.
  • The location path is a separate Go endpoint + schema from the PowerSync sync path — two ingestion routes to maintain, by design.
  • Consent/disclosure flows and shift-state wiring become product requirements.

Rules for agents

  • Background location lives ONLY in core_location, FieldForce-only. Digital Worker must not depend on it.
  • Tracking runs only when ALL three gates are open: feature flag → OS permission → shift state.
  • core_location receives the flag as a watchable stream and reacts to changes mid-session; a one-shot boolean resolved at startup is a design break (it cannot honor the kill-switch).
  • Flag-off MUST cleanly tear down the foreground service, stop GPS, and remove the notification — not merely stop uploading.
  • Cache the flag locally; evaluate from last-known value offline; NEVER default-on when unknown.
  • Upload location via the engine’s own queue → dedicated Go endpoint. Do NOT route GPS telemetry through PowerSync buckets.
  • Track shift-scoped only, with explicit consent + disclosure.