Offline tracking and sync
Background Location Pro is offline first. Every accepted point is written to storage on the device before it is emitted to JS or uploaded. A phone can go through a tunnel, a flight or a dead zone for hours and nothing is lost. The queue drains automatically when connectivity returns.
How delivery works
- Sync tick: every 30 seconds, or every 5 seconds with
setDemoMode(true). You can force one withsyncNow(). - Offline: no requests are sent while
onlineisfalse. Points accumulate inpending. - Batches: up to
batchSizepoints per request, oldest first. Configure withsetSyncPolicy(). - Idempotent: a batch keeps the same
batchIdacross retries, so your server can safely de-duplicate. - Durable: pending points and retry state survive process death and reboots.
Backend contract
The tracker sends:
POST {serverUrl}/api/location/track
Content-Type: application/json; charset=utf-8
Idempotency-Key: <batchId>
X-Device-Id: <deviceId>
{
"batchId": "6f1c2a7e-...",
"deviceId": "a3b9...",
"points": [
{
"id": "c0ffee-...",
"lat": 12.97194,
"lng": 77.59369,
"accuracy": 8.5,
"speed": 1.4,
"bearing": 92.0,
"altitude": 914.2,
"timestamp": 1762325460000
}
]
}
| Field | Meaning |
|---|---|
batchId | Stable across retries of the same batch. Use it for de-duplication. |
deviceId | Stable per install. Also available as getStatus().deviceId. |
points[].id | Unique per point. Matches LocationEvent.id emitted to JS. |
lat, lng | Coordinates in degrees. |
accuracy | Horizontal accuracy radius in meters. |
speed | Meters per second. |
bearing, altitude | Degrees and meters. |
timestamp | Capture time in epoch milliseconds. |
How your server should respond
| Response | Tracker behavior |
|---|---|
2xx | The batch is acknowledged. Points leave the queue and count toward synced. |
5xx, 429, network error, timeout | Retried with jittered exponential backoff, capped at maxBackoffMs. A Retry-After header is honored. |
Other 4xx | Counted as a rejection. After maxAttempts rejections the batch moves to the dead-letter state and counts toward rejected. |
Store batchId (or each point id) with a unique constraint and return 2xx for duplicates. A batch can be delivered more than once if the response is lost on a flaky network. Idempotency makes that harmless.
A minimal Express handler:
app.post('/api/location/track', async (req, res) => {
const {batchId, deviceId, points} = req.body;
if (await db.batches.exists(batchId)) {
return res.status(200).json({ok: true, duplicate: true});
}
await db.transaction(async tx => {
await tx.batches.insert({batchId, deviceId});
await tx.points.insertMany(points.map(p => ({...p, deviceId})));
});
res.status(200).json({ok: true});
});
Authentication
startTracking() takes only a base URL, and the Evaluation edition does not attach custom headers. For evaluation, use an unguessable URL path or an IP allowlist. The licensed editions include the full source, so you can add auth headers, tokens or request signing.
Observing the queue from JS
LocationTracker.onSyncStatus(e => {
console.log({
online: e.online,
pending: e.pending, // on device, not yet acknowledged
synced: e.synced, // acknowledged by your server
retryAttempt: e.retryAttempt, // 0 when healthy
nextRetryAt: e.nextRetryAt, // epoch ms, 0 when nothing scheduled
message: e.message,
});
});
getStatus() adds rejected (dead-lettered points) and lastSyncError (for example http_503 or TimedOut).
Tuning the sync policy
// batchSize, maxBackoffMs, maxAttempts
LocationTracker.setSyncPolicy(100, 5 * 60_000, 5);
| Goal | Suggested policy |
|---|---|
| Live map with a fast backend | setDemoMode(true), batchSize 25 to 50 |
| Battery-friendly fleet tracking | Demo mode off, batchSize 100 to 200, maxBackoffMs 5 to 15 minutes |
| Never drop data, even on persistent 4xx | maxAttempts = 0 (keep retrying forever) |
The policy is persisted and applies to the next batch.