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, telemetryDecision
FieldForce includes a background-location capability in its owncore_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:
- Feature flag (server-driven) — is tracking allowed for this user/role at all? Also the remote kill-switch.
- OS permission (“Always” location) — granted by the user.
- 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_locationas 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:
- 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).
- 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.
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_geolocationrequires 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_locationreceives 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.