Skip to content

Geofencing

Beekon can watch circular regions and tell you when the device crosses their boundary. Geofences are independent of tracking — they keep firing across start() / stop() cycles, survive process death, and survive reboot. You don’t have to be actively tracking for a crossing to arrive.

Under the hood it’s the platform’s own geofencing engine — GMS GeofencingClient on Android (auto-paged, so you can register more than the OS per-app limit) and CLMonitor on iOS. The crossing vocabulary is Enter / Exit only; Android’s DWELL transition is not surfaced.

These are your geofences — self-managed mode. In Beekon Cloud the server owns the set: fences are authored in the console and delivered to each device as a proximity slice on the sync response (the fences near where the device actually is — so a project can hold thousands). Cloud fences can be circles or polygons (delivered as multipolygon geometry; the device registers a polygon’s bounding circle with the OS and verifies point-in-polygon before emitting the crossing). In cloud mode both addGeofences and removeGeofences report GeofencesManagedByServer, and listGeofences() returns the server-delivered slice the device is operating on (a polygon fence appears as its bounding circle — BeekonGeofence is circle-shaped; the polygon boundary itself is enforced on-device at crossing time).

A geofence is a circle, two notify flags, and an optional notification:

FieldTypeRangeDefault
idstring1 … 100 chars; re-adding a matching id replaces it
latitudenumber (°)−90 … +90, WGS-84
longitudenumber (°)−180 … +180, WGS-84
radiusMetersnumber (m)> 0; ≥ 100 recommended for reliable triggering
notifyOnEntrybooltrue
notifyOnExitbooltrue
notificationobjectoptional native OS notification fired on crossing — see Per-geofence notificationsnull

The OS geofencing backends are reliable from roughly 100 m up — a tighter radius triggers late or not at all, so prefer ≥ 100 even though anything > 0 is accepted.

Geofence management is add / remove / list — there’s no setGeofences.

MethodBehaviour
addGeofences(list)Register geofences. Re-adding an existing id replaces it. Atomic — if any entry fails validation, none are added.
removeGeofences(ids)Unregister by id. Unknown ids are ignored.
listGeofences()The currently registered set.
Beekon.addGeofences(listOf(
BeekonGeofence(id = "home", latitude = 12.9716, longitude = 77.5946, radiusMeters = 150.0),
))
val current: List<BeekonGeofence> = Beekon.listGeofences()
Beekon.removeGeofences(listOf("home"))
// addGeofences throws BeekonException.InvalidGeofence on a bad entry

Crossings arrive on the geofenceEvents stream as a GeofenceEvent:

FieldMeaning
idUnique event id (SDK-generated) — the sync dedup key.
geofenceIdThe BeekonGeofence.id that was crossed.
typeEnter or Exit.
timestampWhen the crossing happened (UTC).
Beekon.geofenceEvents.collect { e ->
Log.d("beekon", "${e.type} ${e.geofenceId} @ ${e.timestamp}")
}

A geofence can carry a notification object that Beekon renders as a native OS notification the instant the device crosses the boundary. The headline: it fires offline, with no network, and even when your app has been killed. The OS geofencing engine schedules the alert and renders it from the device’s local copy, so the user is notified whether or not your app — or any tracking session — is running. No server, no push, no process needed.

The object is a delivery channel plus up to two content blocks, one per direction:

FieldTypeRequiredNotes
deliveryenumyeslocal renders on-device at crossing time (offline). cloud is reserved — server-driven push, not yet implemented. In self-managed mode delivery must be local (there’s no server to push from).
onEntercontentnoRendered on entry. Needs notifyOnEntry = true (the default) to ever fire.
onExitcontentnoRendered on exit. Needs notifyOnExit = true.

Each content block:

FieldTypeRequiredNotes
titlestringyesNotification title.
bodystringyesNotification body.
importanceenumnohigh — heads-up + sound (default) — or the quiet standard alert.
deepLinkstringnoURI placed in the notification payload for you to route on tap.
datastring mapnoFlat string key/value map carried in the payload.

A notification whose only content targets a direction you’ve switched off (notifyOn* = false) never fires — the OS isn’t monitoring that direction — and one with neither block is inert.

In Beekon Cloud the server owns the geofence set, so the notification — including a future delivery: cloud — is configured in the console rather than in app code; it arrives with each fence on the sync-response slice and renders on-device exactly like a local one (offline, killed-state included).

Beekon.addGeofences(listOf(
BeekonGeofence(
id = "home", latitude = 12.9716, longitude = 77.5946, radiusMeters = 150.0,
notification = GeofenceNotification(
delivery = NotificationDelivery.Local,
onEnter = NotificationContent(
title = "Welcome home",
body = "You've arrived",
importance = NotificationImportance.High,
deepLink = "myapp://home",
data = mapOf("zone" to "home"),
),
onExit = NotificationContent(title = "Heading out", body = "See you soon"),
),
),
))

Registered geofences are persisted by the native SDK, so they outlive the process. A crossing can wake a terminated app and deliver an event before any tracking session exists. This is why geofencing is documented separately from the lifecycle state machine: Beekon.state describes the tracking session, while geofences run on the OS’s own monitoring budget regardless of that state.