Skip to content

Data pipeline

This page follows one fix from the moment the OS produces it to the moment it lands on your server. It’s the companion to Tracking lifecycle: that page explains when GPS runs (moving vs. stationary); this one explains what happens to a fix once the OS hands one over, regardless of motion state.

flowchart LR
    fix[OS fix] --> acc[Accuracy gate]
    acc --> teleport[Teleport check]
    teleport --> kalman[Kalman smoothing]
    kalman --> accepted[Accepted fix]
    accepted --> queue[(bgeo.db queue)]
    accepted -.-> onloc[onLocation — your Swift]
    accepted -.-> odo[Odometer gate]
    queue --> uploader[Uploader]
    uploader --> server[(Your server)]

    fix -.-> logger[Native logger]
    logger -.-> logentries[log_entries]
    logentries -.-> logurl[(logUrl)]

Every solid-arrow stage above runs natively, entirely inside the engine — none of your Swift is involved, and none of it pauses if your app is backgrounded or killed. The dotted branches are where your code and a separate logging lane hook in.

Accuracy gate

Every raw fix is checked against locationFilterMaxAccuracy first — fixes worse than this are rejected outright, in every locationFilterPolicy including 'PassThrough'. disableLocationFilter bypasses this (and everything below) entirely, accepting every raw fix as-is. No event fires for a rejected fix — it simply never reaches the stages below.

Teleport check

A fix that passes the accuracy gate is checked against locationFilterMaxSpeed: does the implied speed from the previous fix look like a real GPS jump? locationFilterPolicy decides what happens next:

  • 'Conservative' (default) — a violating fix is dropped.
  • 'Adjust' — a violating fix is capped to the speed limit instead of dropped.
  • 'PassThrough' — teleport rejection (and Kalman smoothing, next) is skipped entirely; only the accuracy gate applies.

Like kalmanProfile below, locationFilterPolicy is only applied when the filter is rebuilt — tracking start/stop — not live on setConfig().

Kalman smoothing

A fix that clears the teleport check is smoothed by a Kalman filter tuned by kalmanProfile: 'DEFAULT', 'AGGRESSIVE' (reacts faster, noisier path), or 'CONSERVATIVE' (more smoothing, more lag). Same rebuild-only rule as locationFilterPolicy above.

Odometer gate

A fix that survives filtering is accepted — it advances to persistence and your listeners regardless of what happens next — but odometerAccuracyThreshold decides whether it also advances the running odometer total. 0 (default) disables this gate, so every accepted fix counts; a positive value excludes noisier-but-still-filter-passing fixes from distance accounting only. Read the running total with getOdometer().

Reaching your code: onLocation

An accepted fix is delivered through the onLocation event — but your app is a UI-only observer here, not a step in the upload path: nothing downstream (persistence, upload) waits on or depends on a listener being attached. The native queue below keeps persisting and uploading fixes taken while your engine isn’t running either way.

Persistence: the SQLite queue

Every accepted fix (independent of the odometer gate) is written to the location_events table in a single SQLite database, bgeo.db, on both platforms. The queue drains oldest first, ordered by row id. Geofence ENTER/EXIT/DWELL events ride this same queue alongside location records, so they reach your server even if the transition happened while the app was killed.

Two caps keep the queue bounded:

  • maxRecordsToPersist — count cap; the oldest rows are evicted once exceeded (default -1, unlimited).
  • maxDaysToPersist — age-based pruning, run unconditionally on every configure(). The engine hardcodes a 2-day retention default; setting this key only takes effect when the value is positive, in which case it replaces that default (raising or lowering it). There is no way to disable age-based pruning entirely.

Inspect or manage the queue directly with getCount(), getLocations(), destroyLocations(), or enqueue a record manually with insertLocation().

Upload

The uploader drains the queue to url — leaving url unset puts the SDK in persist-only mode (fixes still queue and still reach onLocation, nothing uploads). Default body shape is a single record, { "location": {…} }; enabling batchSync sends an array instead, { "location": [{…}, {…}] }, capped at maxBatchSize. See the HTTP guide’s Shaping the body section for the full request-shaping options (params, extras, httpRootProperty).

A failed upload backs off exponentially before the next attempt. Within that, one rule isolates bad data from a slow server: a permanent 4xx response (malformed or gone) is treated as a poison record and dropped, so a single bad row can’t wedge the queue behind it forever — but a 429 (server throttling) is explicitly carved out of that rule and retried with backoff like a 5xx or network error, since the batch itself was fine.

A cellular→Wi-Fi handoff with continuous internet the whole time drains the queue without ever firing onConnectivityChange — because that drain and that event watch two different signals. disableAutoSyncOnCellular defers auto-sync while the active network is cellular and drains the backlog the moment a non-cellular network becomes available; that drain is driven by the native network-capabilities callback, which re-checks on every capability change while on a non-cellular transport (deliberately level-triggered, not edge-only, so a missed transition can’t strand the queue). onConnectivityChange, by contrast, fires only when validated internet flips true/false — a handoff between two already-connected networks never trips it.

Uploads continue natively while the app is killed or backgrounded, including a 401/403 triggering a authorization token refresh-and-retry with none of your code required — see the HTTP guide’s JWT refresh section for the full request/response contract. Every completed location-sync request (not log uploads — see below) fires onHttp: status is 0 for a network error, and a refresh-triggering 401/403 produces two events — one for the original request, one for the retry.

Log lane

Logging runs as its own lane, parallel to the location pipeline: every log line mirrors unconditionally to the native console (os_log), then — only when logLevel admits it — is also persisted into log_entries in bgeo.db. From there it’s uploaded in batches to logUrl using the same headers/authorization/token-refresh machinery as location uploads. See the Logging & debugging guide for retention and flush-trigger detail.