Persistence & history
Every gated position is persisted to a SQLite database that the native library owns. The schema, retention rules, and write semantics are identical across platforms — only the engine differs (Room on Android, GRDB on iOS).
The non-crossing invariant
Section titled “The non-crossing invariant”This is the rule that shapes everything else: the persistence write path is invoked from the platform’s native location callback and never crosses into Dart or JavaScript. In background, those runtimes are not guaranteed to be running — a missed write would be a lost point. So:
- Android: write happens inside the foreground service, on a background dispatcher, fed from the
FusedLocationProviderClientcallback. - iOS: write happens synchronously from the
CLLocationUpdate.liveUpdates(or legacy delegate) callback insideStateHolder.deliverLocation. - Flutter / RN bridges expose
history(from, to)for reads only. They never participate in writes.
Schema
Section titled “Schema”Each row corresponds to one emitted (gated) position.
| Column | Type | Meaning |
|---|---|---|
ts | INTEGER | Unix milliseconds (timestamp from the OS provider, not the time we received it) |
lat | REAL | Degrees |
lng | REAL | Degrees |
accuracy | REAL | Metres, horizontal 1σ |
speed | REAL | Metres / second |
bearing | REAL | Degrees, 0–360 (true north) |
altitude | REAL | Metres above WGS-84 ellipsoid |
activity_hint | TEXT (nullable) | Reserved for future activity recognition (always NULL in v1) |
There’s an index on ts ASC for range queries. Both platforms keep the schema in lockstep — migrations bump version on both adapters together.
Database location
Section titled “Database location”| Platform | Path |
|---|---|
| Android | App-private data (Room default), inside the foreground-service process |
| iOS | Library/Application Support/beekon/beekon.db, marked isExcludedFromBackup = true |
iOS deliberately excludes the DB from iCloud — silent sync of a 100K-row trip database isn’t user-friendly behaviour.
Retention
Section titled “Retention”A two-axis policy that runs on every write batch:
TTL: 7 days Cap: 100,000 rowsAfter each write, the adapter prunes rows older than 7 days and trims to 100K rows from the oldest end. Both bounds are normative — same on every platform.
If you’re deleting historical user data on demand (GDPR-style “delete my account”), wipe the DB at the OS level (uninstall the app, or clear app data on Android). Beekon doesn’t expose a clearHistory() API in v1.
Reading history
Section titled “Reading history”history(from, to) returns positions inclusive of both bounds.
val now = Instant.now()val hourAgo = now.minus(1, ChronoUnit.HOURS)
// suspend, throws BeekonError.InternalError on DB failureval points: List<Position> = Beekon.history(from = hourAgo, to = now)Returned list is sorted ascending by timestamp.
let now = Date()let hourAgo = now.addingTimeInterval(-3600)
// async throwslet points = try await Beekon.shared.history(from: hourAgo, to: now)Returned array is sorted ascending by timestamp.
final now = DateTime.now();final hourAgo = now.subtract(const Duration(hours: 1));
final points = await Beekon.instance.history(from: hourAgo, to: now);Returned List<Position> is sorted ascending by timestamp.
Reads while tracking
Section titled “Reads while tracking”Reads and writes share the same database. Both engines (Room, GRDB) use WAL mode, so concurrent readers don’t block the writer and vice versa. You can call history while Beekon.state == Tracking without affecting emission cadence.
What persistence does not do
Section titled “What persistence does not do”- It doesn’t sync to a cloud — that’s your application’s job. Beekon’s console test-rig demonstrates an ingest endpoint, but it’s not part of the SDK contract.
- It doesn’t expose the underlying SQLite handle. If you need to run custom queries, query Beekon’s history range and post-process in your code.
- It doesn’t store anything other than positions. State transitions, errors, and pause reasons are not persisted.