Skip to content

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

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 FusedLocationProviderClient callback.
  • iOS: write happens synchronously from the CLLocationUpdate.liveUpdates (or legacy delegate) callback inside StateHolder.deliverLocation.
  • Flutter / RN bridges expose history(from, to) for reads only. They never participate in writes.

Each row corresponds to one emitted (gated) position.

ColumnTypeMeaning
tsINTEGER NOT NULLUnix milliseconds (timestamp from the OS provider, not the time we received it)
latREAL NOT NULLDegrees
lngREAL NOT NULLDegrees
accuracyREAL (nullable)Metres, horizontal 1σ; NULL when the OS did not report
speedREAL (nullable)Metres / second; NULL when the OS did not report
bearingREAL (nullable)Degrees, [0, 360) clockwise from true north; NULL when the OS did not report
altitudeREAL (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.

PlatformPath
AndroidApp-private data (Room default), inside the foreground-service process
iOSLibrary/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.

A two-axis policy that runs on every write batch:

TTL: 7 days
Cap: 100,000 rows

After 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.

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 failure
val points: List<Location> = Beekon.history(from = hourAgo, to = now)

Returned list is sorted ascending by timestamp.

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.

  • 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.