Skip to Content
Core ConceptsGeofencing

Geofencing API

Tracelet provides a robust, offline-first geofencing engine. Unlike standard OS geofencing which is limited to 20-100 geofences and requires the OS to wake your app, Tracelet evaluates geofences internally within the Rust engine. This enables unlimited geofences with zero additional battery drain.


1. Circular vs Polygon Geofences

Tracelet supports both standard circular geofences and complex polygon geofences.

Circular Geofences

A circular geofence is defined by a center coordinate and a radius (in meters).

final circular = tl.Geofence( identifier: 'headquarters', latitude: 37.7749, longitude: -122.4194, radius: 200, // 200 meters notifyOnEntry: true, notifyOnExit: true, notifyOnDwell: true, // Triggers if they stay inside loiteringDelay: 300, // Trigger dwell after 5 minutes );

Polygon Geofences

A polygon geofence is defined by an array of coordinates outlining a complex shape (like a park or a building).

final polygon = tl.Geofence.polygon( identifier: 'golden_gate_park', vertices: [ tl.Coordinate(latitude: 37.773972, longitude: -122.431297), tl.Coordinate(latitude: 37.769996, longitude: -122.511055), tl.Coordinate(latitude: 37.764516, longitude: -122.508566), ], notifyOnEntry: true, notifyOnExit: true, );

2. Adding and Removing Geofences

Tracelet stores all geofences in the local SQLite database. This means they are persistent across app restarts. You do not need to re-add them every time the app launches.

Adding Geofences

You can add a single geofence or an array of geofences.

// Add a single geofence await tl.Tracelet.addGeofence(circular); // Add multiple geofences efficiently await tl.Tracelet.addGeofences([circular, polygon]);

Removing Geofences

Remove them by their unique identifier, or clear the entire database.

// Remove a specific geofence await tl.Tracelet.removeGeofence('headquarters'); // Remove all geofences entirely await tl.Tracelet.removeGeofences();

3. Listing Active Geofences

You can query the SQLite database at any time to see which geofences are currently being monitored.

final geofences = await tl.Tracelet.getGeofences(); for (final fence in geofences) { print('Monitoring: \${fence.identifier}'); }

4. Listening for Geofence Events

When the user crosses a geofence boundary, Tracelet fires the onGeofence event. Because the evaluation happens in the Rust core during location updates, these events will fire even if the app is killed (triggering your headless callback).

tl.Tracelet.onGeofence((evt) { if (evt.action == tl.GeofenceEventAction.ENTER) { print('User entered: \${evt.identifier}'); } else if (evt.action == tl.GeofenceEventAction.EXIT) { print('User exited: \${evt.identifier}'); } else if (evt.action == tl.GeofenceEventAction.DWELL) { print('User is dwelling inside: \${evt.identifier}'); } });

5. Android: foreground service & Google Play policy

Google Play policy change — effective October 28, 2026. Geofencing is no longer a permitted use case for foreground service location. Apps that use a foreground service solely for geofencing must remove the FOREGROUND_SERVICE_LOCATION (and FOREGROUND_SERVICE, if unused otherwise) permissions from their merged manifest.

Tracelet’s standard geofence mode uses the native Android Geofence API (GeofencingClient), which fires ENTER/EXIT — and relaunches your app into your headless task — while the app is suspended or terminated, without a foreground service. Calling startGeofences() in standard mode does not start a foreground service (as of 3.5.x), so it is compliant by default.

Aggressive OEMs (Samsung/Xiaomi/Huawei/OnePlus/Oppo/Vivo): as of 3.6.1, an explicit geofenceModeHighAccuracy: false is honored on every device. Before 3.6.1 these OEMs silently forced high-accuracy mode — and with it the foreground service and its persistent notification — even when you set false, which broke the compliant standard-mode path above. If you target these devices, use 3.6.1 or later. On an aggressive OEM the SDK now logs a reliability warning (native geofence delivery can be delayed there) instead of overriding your config.

If you use Tracelet only for geofencing

  1. Do not enable the foreground service:

    await tl.Tracelet.ready(const tl.Config( android: tl.AndroidConfig( foregroundService: tl.ForegroundServiceConfig(enabled: false), ), geofence: tl.GeofenceConfig(geofenceModeHighAccuracy: false), )); await tl.Tracelet.startGeofences();
  2. The Tracelet SDK declares FOREGROUND_SERVICE / FOREGROUND_SERVICE_LOCATION for its continuous-tracking use case. If your app never does continuous tracking, strip them from the merged manifest with the manifest-merger remove rule in your app’s android/app/src/main/AndroidManifest.xml:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" tools:node="remove" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" tools:node="remove" /> </manifest>

If you use continuous tracking and geofencing

You may keep FOREGROUND_SERVICE_LOCATION for the tracking use case (e.g. navigation/fleet tracking), but the foreground service must not be used for geofencing. Tracelet already handles this: the FGS runs for continuous tracking (start()), and switching to geofence-only mode (startGeofences() in standard mode) tears the FGS down.

High-accuracy geofence mode (geofenceModeHighAccuracy: true) uses continuous GPS for tight, in-app boundary detection, which does run a foreground service. That is continuous location — not “geofencing” in the policy sense — so only enable it when your app has a separate permitted location use case. For most apps, standard mode is the compliant and battery-efficient choice.