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 NOT NULL | Unix milliseconds (timestamp from the OS provider, not the time we received it) |
lat | REAL NOT NULL | Degrees |
lng | REAL NOT NULL | Degrees |
accuracy | REAL (nullable) | Metres, horizontal 1σ; NULL when the OS did not report |
speed | REAL (nullable) | Metres / second; NULL when the OS did not report |
bearing | REAL (nullable) | Degrees, [0, 360) clockwise from true north; NULL when the OS did not report |
altitude | REAL (nullable) | Metres above WGS-84 ellipsoid; NULL when the OS did not report |
There’s an index on ts ASC for range queries. Both platforms keep the schema in lockstep — accuracy/speed/bearing/altitude are nullable on both, and a NULL value means “the OS did not measure this field,” distinct from a zero measurement.
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”The historical-range API returns locations inclusive of both bounds.
val now = Instant.now()val hourAgo = now.minus(1, ChronoUnit.HOURS)
// suspend; throws BeekonError.StorageFailure on DB failureval points: List<Location> = Beekon.history(from = hourAgo, to = now)Returned list is sorted ascending by timestamp.
let now = Date()let hourAgo = now.addingTimeInterval(-3600)
// async throws — note the method is named `locations`, overloaded with the live propertylet points = try await Beekon.shared.locations(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<Location> is sorted ascending by timestamp.
const now = new Date();const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
const points = await Beekon.history(hourAgo, now);Returned array 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 locations. State transitions, errors, and stop reasons are not persisted.