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).
BeekonGeofence
Section titled “BeekonGeofence”A geofence is a circle, two notify flags, and an optional notification:
| Field | Type | Range | Default |
|---|---|---|---|
id | string | 1 … 100 chars; re-adding a matching id replaces it | — |
latitude | number (°) | −90 … +90, WGS-84 | — |
longitude | number (°) | −180 … +180, WGS-84 | — |
radiusMeters | number (m) | > 0; ≥ 100 recommended for reliable triggering | — |
notifyOnEntry | bool | — | true |
notifyOnExit | bool | — | true |
notification | object | optional native OS notification fired on crossing — see Per-geofence notifications | null |
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.
The three methods
Section titled “The three methods”Geofence management is add / remove / list — there’s no setGeofences.
| Method | Behaviour |
|---|---|
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 entrytry await Beekon.shared.addGeofences([ BeekonGeofence(id: "home", latitude: 12.9716, longitude: 77.5946, radiusMeters: 150),])let current = await Beekon.shared.listGeofences()try await Beekon.shared.removeGeofences(ids: ["home"])// addGeofences throws BeekonError.invalidGeofence on a bad entry;// both add and remove throw .geofencesManagedByServer in cloud modeawait Beekon.instance.addGeofences([ BeekonGeofence(id: 'home', latitude: 12.9716, longitude: 77.5946, radiusMeters: 150),]);final current = await Beekon.instance.listGeofences();await Beekon.instance.removeGeofences(['home']);await Beekon.addGeofences([ { id: 'home', latitude: 12.9716, longitude: 77.5946, radiusMeters: 150 },]);const current = await Beekon.listGeofences();await Beekon.removeGeofences(['home']);The geofenceEvents stream
Section titled “The geofenceEvents stream”Crossings arrive on the geofenceEvents stream as a GeofenceEvent:
| Field | Meaning |
|---|---|
id | Unique event id (SDK-generated) — the sync dedup key. |
geofenceId | The BeekonGeofence.id that was crossed. |
type | Enter or Exit. |
timestamp | When the crossing happened (UTC). |
Beekon.geofenceEvents.collect { e -> Log.d("beekon", "${e.type} ${e.geofenceId} @ ${e.timestamp}")}for await e in await Beekon.shared.geofenceEvents { print("\(e.type) \(e.geofenceId) @ \(e.timestamp)")}Beekon.instance.geofenceEvents.listen((e) { print('${e.type} ${e.geofenceId} @ ${e.timestamp}');});Beekon.onGeofenceEvent((e) => { console.log(e.type, e.geofenceId, e.timestamp);});Per-geofence notifications
Section titled “Per-geofence notifications”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:
| Field | Type | Required | Notes |
|---|---|---|---|
delivery | enum | yes | local 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). |
onEnter | content | no | Rendered on entry. Needs notifyOnEntry = true (the default) to ever fire. |
onExit | content | no | Rendered on exit. Needs notifyOnExit = true. |
Each content block:
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | Notification title. |
body | string | yes | Notification body. |
importance | enum | no | high — heads-up + sound (default) — or the quiet standard alert. |
deepLink | string | no | URI placed in the notification payload for you to route on tap. |
data | string map | no | Flat 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"), ), ),))try await Beekon.shared.addGeofences([ BeekonGeofence( id: "home", latitude: 12.9716, longitude: 77.5946, radiusMeters: 150, notification: GeofenceNotification( onEnter: NotificationContent( title: "Welcome home", body: "You've arrived", importance: .high, deepLink: "myapp://home", data: ["zone": "home"] ), onExit: NotificationContent(title: "Heading out", body: "See you soon"), delivery: .local ) ),])await Beekon.instance.addGeofences([ BeekonGeofence( id: 'home', latitude: 12.9716, longitude: 77.5946, radiusMeters: 150, notification: GeofenceNotification( delivery: NotificationDelivery.local, onEnter: NotificationContent( title: 'Welcome home', body: "You've arrived", importance: NotificationImportance.high, deepLink: 'myapp://home', data: {'zone': 'home'}, ), onExit: NotificationContent(title: 'Heading out', body: 'See you soon'), ), ),]);await Beekon.addGeofences([ { id: 'home', latitude: 12.9716, longitude: 77.5946, radiusMeters: 150, notification: { delivery: 'local', onEnter: { title: 'Welcome home', body: "You've arrived", importance: 'high', deepLink: 'myapp://home', data: { zone: 'home' }, }, onExit: { title: 'Heading out', body: 'See you soon' }, }, },]);Persistence & lifecycle
Section titled “Persistence & lifecycle”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.