---
title: Announcing the Capacitor Geofences Plugin
description: Our new Capacitor geofencing plugin monitors OS-managed regions on Android and iOS and delivers enter, exit, and dwell events even after the app is killed.
date:
  created: 2026-08-20
  updated: 2026-08-20
authors:
  - robingenz
categories:
  - Announcements
  - Capacitor
  - SDKs
links:
  - Capacitor Geofences: sdks/capacitor/geofences.md
faq: true
---

# Announcing the Capacitor Geofences Plugin

Most geofencing setups in a Capacitor app work fine until the user swipes the app away. The boundary crossing still happens, nothing in JavaScript is listening, and the event is gone. The [Capacitor Geofences plugin](../../sdks/capacitor/geofences.md) is our answer: a Capacitor geofencing plugin built on the region monitoring that Android and iOS already run for you, treating the terminated app as the normal case rather than the edge case. It's available today to all Capawesome [Insiders](../../insiders/index.md).

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Build and deploy your Capacitor app with Capawesome Cloud" src="https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1" />
  </a>
</div>

**Key takeaways:**

- `@capawesome-team/capacitor-geofences` monitors OS-managed circular regions via `GeofencingClient` on Android and Core Location on iOS, with enter and exit transitions on both platforms and dwell transitions on Android.
- Transitions detected while the app is terminated are replayed once the first `geofenceTransition` listener registers. The buffer holds 100.
- Each geofence can carry a native local notification, shown regardless of app state.
- HTTP sync uploads transitions to your server natively. The queue holds 1,000 and survives force-quits and reboots.
- Hard OS limits: 100 geofences per app on Android, 20 regions on iOS.
- Requires Capacitor 8 or later, no web implementation, part of the Capawesome Insiders subscription.

## Why a Dedicated Capacitor Geofencing Plugin?

Because most of what's available today is a continuous location-tracking SDK with geofencing attached to the side. You install it to learn when a device enters a region, and you inherit a foreground service, a persistent notification, and a battery budget for a location trail you never wanted.

Region monitoring works differently. You hand the operating system a set of circular regions and it watches them with the low-power infrastructure it already runs for itself, without your process staying alive. What a dedicated plugin adds is the part JavaScript can't reach: a transition detected while your app is terminated still has to reach a notification tray, your backend, or your listener.

## Installation

To install the Capacitor Geofences plugin, please refer to the [Installation](../../sdks/capacitor/geofences.md/#installation) section in the plugin documentation. It's published to the Capawesome npm registry, so installation requires the license key that comes with a [Capawesome Insiders](../../insiders/index.md) subscription.

Two platform prerequisites are worth knowing up front. On Android, `ACCESS_BACKGROUND_LOCATION` has to be declared in your own `AndroidManifest.xml`; the plugin leaves it out deliberately, because [Google Play's background location policy](https://support.google.com/googleplay/android-developer/answer/9799150){:target="_blank"} requires you to justify it during review. On iOS, add the `NSLocationWhenInUseUsageDescription` and `NSLocationAlwaysAndWhenInUseUsageDescription` keys to `Info.plist`, or `addGeofences(...)` and `requestPermissions(...)` reject with a clear error.

## Usage

Here's the shape of a typical integration, from registering a region to getting the transition into your backend.

### Add a geofence

[`addGeofences(...)`](../../sdks/capacitor/geofences.md#addgeofences) hands one or more circular regions to the operating system and returns their identifiers. Each region needs a center, a radius in meters, and optionally a notification that is displayed natively when a transition is detected:

```typescript
import { Geofences } from '@capawesome-team/capacitor-geofences';

const addGeofences = async () => {
  const { ids } = await Geofences.addGeofences({
    geofences: [
      {
        latitude: 37.33182,
        longitude: -122.03118,
        radius: 200,
        notification: {
          title: 'Welcome',
          text: 'You have entered the area.',
        },
      },
    ],
  });
  return ids;
};
```

Leave out the `id` and the plugin generates a UUID, returned in `ids` in the order you passed the regions in. `notifyOnEnter` and `notifyOnExit` default to `true`. Dwell transitions are Android-only and opt-in through `androidNotifyOnDwell` with `androidLoiteringDelay`.

Give your regions room. Android recommends a [minimum radius of 100 meters](https://developer.android.com/develop/sensors-and-location/location/geofencing){:target="_blank"}, ideally 100 to 150. iOS only reports a transition once the device has crossed the boundary and moved a minimum distance away, which Apple's region monitoring guide says you can [assume is around 200 meters](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/LocationAwarenessPG/RegionMonitoring/RegionMonitoring.html){:target="_blank"} for testing. Hence the 200 meter recommendation there. Larger radii are clamped to `maximumRegionMonitoringDistance`.

One difference to plan for: on Android, adding a geofence the device is already inside triggers an enter transition immediately, while iOS stays quiet until the boundary is crossed.

### Listen for transitions

Register the [`geofenceTransition`](../../sdks/capacitor/geofences.md#addlistenergeofencetransition-) listener as early in app startup as you can. That first registration flushes the replay buffer, so everything detected while your app was gone arrives right after it:

```typescript
import { Geofences, TransitionType } from '@capawesome-team/capacitor-geofences';

const addListener = async () => {
  await Geofences.addListener('geofenceTransition', (event) => {
    if (event.transitionType === TransitionType.Enter) {
      console.log(`Entered the geofence ${event.id}.`);
    }
  });
};
```

Every event carries the geofence `id`, the `transitionType`, a `timestamp` in milliseconds, and the triggering `latitude` and `longitude`. Those coordinates are always `null` on iOS because Core Location doesn't provide them, so look the position up by geofence `id`. The buffer holds 100 transitions and drops the oldest first.

### Sync transitions to your server

If your backend needs to know about a crossing right away instead of the next time someone opens the app, call [`configureSync(...)`](../../sdks/capacitor/geofences.md#configuresync) once. The configuration is persisted natively, and every transition from then on is queued and uploaded without JavaScript running at all:

```typescript
import { Geofences } from '@capawesome-team/capacitor-geofences';

const configureSync = async () => {
  await Geofences.addListener('syncFailed', (event) => {
    console.error('Upload failed: ', event.statusCode, event.message);
  });
  await Geofences.configureSync({
    url: 'https://api.example.com/transitions',
    headers: {
      Authorization: 'Bearer eyJhbGciOi...',
    },
    extras: {
      userId: 'abc',
    },
  });
};
```

Each upload is an HTTP `POST` carrying the batch of transitions and whatever static `extras` you configured:

```json
{
  "transitions": [
    {
      "id": "1b8935d6-27b4-4a5c-9f0f-4a5c9f0f1b89",
      "geofenceId": "2ca23ff9-b95d-4962-b64f-3e1efe6f2e7d",
      "transitionType": "ENTER",
      "timestamp": 1723291200000,
      "latitude": 52.52,
      "longitude": 13.405
    }
  ],
  "extras": { "userId": "abc" }
}
```

Delivery is at least once, so deduplicate on the server by transition `id`. The response body is ignored and only the status code matters. A `2xx` acknowledges the batch and clears it from the queue. `408`, `429`, `5xx`, timeouts, and network errors keep it queued for a retry with exponential backoff. **Any other status code drops those transitions permanently**, because a rejected upload must never block the queue forever.

So a `400` from a half-deployed endpoint costs you those transitions for good. Answer with a retryable status such as `503` while your server can't accept data, and subscribe to `syncFailed` to notice drops.

The request timeout is 30 seconds, and Android schedules retries through WorkManager, so they outlive process death and a device reboot. [`getSyncStatus()`](../../sdks/capacitor/geofences.md#getsyncstatus) reports `pendingCount`, `droppedCount`, and `lastSyncedAt`, [`triggerSync()`](../../sdks/capacitor/geofences.md#triggersync) forces an attempt, and `clearSyncQueue()` and `disableSync()` clean up when a user signs out.

### Request the required permissions

Geofencing needs the **Always** location authorization on iOS and the **background location** permission on Android, and neither platform lets you ask for both in one prompt. Request the foreground permission first and the background permission only after it was granted:

```typescript
import { Geofences } from '@capawesome-team/capacitor-geofences';

const requestPermissions = async () => {
  // Step 1: Request the foreground location permission.
  let status = await Geofences.requestPermissions({
    permissions: ['location'],
  });
  // Step 2: Request the background location permission.
  if (status.location === 'granted') {
    status = await Geofences.requestPermissions({
      permissions: ['backgroundLocation'],
    });
  }
  // Optionally: Request the notifications permission.
  await Geofences.requestPermissions({ permissions: ['notifications'] });
  return status;
};
```

Android 10 and later forbid requesting both in one call. On Android 11 and later, the second request opens the app's location settings, where the user has to pick *Allow all the time*; on iOS it triggers the system prompt to upgrade from *While Using the App* to *Always*. With only the foreground permission granted, `addGeofences(...)` rejects with `PERMISSION_DENIED`, and [`openSettings()`](../../sdks/capacitor/geofences.md#opensettings) handles permanent denials.

### Inspect and remove geofences

[`getGeofences()`](../../sdks/capacitor/geofences.md#getgeofences) returns everything currently monitored, the quickest way to reconcile the system's state with your own after an update. Removing works by identifier or in bulk:

```typescript
import { Geofences } from '@capawesome-team/capacitor-geofences';

const getGeofences = async () => {
  const { geofences } = await Geofences.getGeofences();
  return geofences;
};

const removeGeofences = async (ids: string[]) => {
  await Geofences.removeGeofences({ ids });
};

const removeAllGeofences = async () => {
  await Geofences.removeAllGeofences();
};
```

## How Many Geofences Can You Register?

100 on Android and 20 on iOS. Both are operating system limits, not plugin limits. Android documents a [limit of 100 geofences per app](https://developer.android.com/develop/sensors-and-location/location/geofencing){:target="_blank"}, per device user on multi-user devices. Apple is blunter: [Core Location limits to 20](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/LocationAwarenessPG/RegionMonitoring/RegionMonitoring.html){:target="_blank"} the number of regions that may be simultaneously monitored by a single app. Exceed either and [`addGeofences(...)`](../../sdks/capacitor/geofences.md#addgeofences) rejects with `GEOFENCE_LIMIT_EXCEEDED`.

Twenty feels tight for a store locator with hundreds of branches, until you notice a user is only ever near a handful. Register the regions closest to the current position and swap them as the user moves, keeping one slot for a large region whose exit triggers the next round.

## What Happens While Your App Is Terminated?

The operating system still detects the transition, and the plugin handles it three ways without the web view starting: it shows the geofence's local notification if you configured one, it queues the transition for upload, and it stores it for replay so your listener receives it in order on the next launch.

Re-registration matters just as much. Android restores geofences automatically after a reboot or an app update, the step most hand-rolled integrations forget. On iOS the regions are persisted by the operating system, and the upload happens during the short background wake-up in which iOS delivers the event.

Two ceilings are worth noting. The replay buffer holds 100 transitions and the sync queue 1,000, both dropping the oldest entries once full. The queue lives unencrypted in sandboxed app storage, and uploads can't be restricted to Wi-Fi or unmetered networks.

## Geofences or Background Geolocation?

Use geofences when all you need is the fact that a boundary was crossed. Use background geolocation when you need the trail, the sequence of positions between two points, at the cost of a continuously running location session. Check-ins and arrival notifications fall into the first category, route recording into the second.

The two run side by side by design. The [Capacitor Background Geolocation plugin](../../sdks/capacitor/background-geolocation.md) covers continuous tracking, and a common pattern is to keep geofences as the always-on layer and start a tracking session only once the device enters a region you care about. The [Capacitor Geocoder plugin](../../sdks/capacitor/geocoder.md) turns a transition into an address.

## FAQ

### Is there a geofencing plugin for Capacitor?

Yes. The [Capacitor Geofences plugin](../../sdks/capacitor/geofences.md) monitors circular regions through the operating system's own region monitoring, `GeofencingClient` on Android and Core Location on iOS, and reports enter, exit, and dwell transitions.

### Does the plugin work on the web?

No. All methods are available on Android and iOS only. On the web, they reject with an unimplemented error.

### Why don't I receive dwell transitions on iOS?

Dwell transitions are an Android feature. On iOS, `androidNotifyOnDwell` and `androidLoiteringDelay` are ignored and only enter and exit transitions are reported.

### Why are `latitude` and `longitude` `null` on iOS?

Core Location does not provide the location that triggered a region transition. Look the coordinates up by the geofence `id` from the event, which you know because you registered the region.

### What radius should I use for a geofence?

At least 200 meters on iOS and at least 100 meters on Android. Smaller regions are detected unreliably in the field, and iOS clamps anything above `maximumRegionMonitoringDistance`.

### Does it work with Ionic, Angular, React, or Vue?

Yes. The plugin is framework-agnostic and works in any Capacitor app, including Ionic with Angular, React, or Vue, as well as plain JavaScript projects.

## Availability

The Capacitor Geofences plugin is available today to all Capawesome [Insiders](../../insiders/index.md) and requires Capacitor 8 or later. The same subscription covers every other Insiders plugin, so there's no separate license to buy.

New plugins and notable releases are announced in the Capawesome newsletter first.

[Subscribe to the Capawesome Newsletter](https://capawesome.io/newsletter/){ .md-button .md-button--primary }

## Conclusion

Reacting to a boundary crossing is less of an API problem than a systems problem. The API is three calls. The hard part is process lifecycle, replay buffers, retry backoff, a two-step permission ladder, and re-registration after a reboot. Get one of them wrong and it shows up as a geofence that "sometimes doesn't fire". That layer is what the [Capacitor Geofences plugin](../../sdks/capacitor/geofences.md) absorbs.

**Further reading:**

- [Capacitor Geofences plugin](../../sdks/capacitor/geofences.md) — platform setup and the full HTTP sync contract
- [API Reference](../../sdks/capacitor/geofences.md#api) — every method, option, event, and error code

**Missing a feature?** [Create a feature request](https://github.com/capawesome-team/capacitor-plugins/issues/new/choose){:target="_blank"} in our GitHub repository.

If you have any questions, join us on the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"}. To stay updated on the latest news, subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"}.
