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 everyconfigure(). 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.