Geofencing is a powerful feature for mobile applications that allows you to trigger events when a user enters or exits a specific geographic area. For example, you might send a notification when a customer arrives near your store, log location-based activity, or build safety alerts for restricted zones.
In this tutorial, we’ll build a geofencing mobile app using the latest Ionic 8, Angular 20, and Capacitor. We’ll integrate the Google Places API to search and select locations, then use Capacitor Geolocation together with a Geofencing plugin to monitor user movement in real time.
By the end of this tutorial, you’ll have a working Ionic app that can:
-
Search for locations using Google Places API.
-
Add and manage geofences around those places.
-
Trigger enter/exit events when crossing geofences.
-
Show results in a clean Ionic UI.
This is an update of our original Ionic 3 + Angular 5 geofence tutorial, modernized for today’s Ionic ecosystem with Capacitor replacing Cordova.
Project Setup (Ionic 8 + Angular 20 + Capacitor)
1. Install Ionic CLI
First, ensure you have the latest version of Node.js 20 or higher installed. Then install the Ionic CLI globally:
npm install -g @ionic/cli
Verify installation:
ionic -v
You should see a version 7.2.1.
2. Create a New Ionic Angular App
Generate a new Ionic Angular project using the blank starter:
ionic start ionic-geofence blank --type=angular
Navigate to the project:
cd ionic-geofence
3. Add Mobile Platforms
Let’s add Android and iOS platforms:
ionic cap add android
ionic cap add ios
This creates the native projects inside android/
and ios/
folders.
4. Run the App in Browser
To test everything is working:
ionic serve
Your app should open in the browser at http://localhost:8100/
.
5. Build and Sync
Before working with plugins, build the app and sync with Capacitor:
ionic build
ionic cap sync
This ensures dependencies are installed in both the Angular and native layers.
✅ At this point, you have a working Ionic 8 + Angular 20 + Capacitor project ready for geofencing.
Installing Capacitor Plugins (Geolocation, Geofence, and Google Places API Integration)
Our app needs three key features:
-
Geolocation – to track the device’s current position.
-
Geofencing – to monitor when the device enters or exits defined geographic boundaries.
-
Google Places API – to search and pick locations by name/address.
1. Install Capacitor Geolocation
The official Capacitor plugin provides accurate device GPS data.
npm install @capacitor/geolocation
No additional native setup is required, but on iOS and Android, we must add location permissions (we’ll cover that in the native config section later).
2. Install a Geofencing Plugin
The capacitor itself doesn’t ship with a geofencing plugin yet, so we’ll use a community-maintained one.
npm install @capacitor-community/background-geolocation
ionic cap sync
3. Install Google Maps / Places API
We’ll use Google Places API for searching and selecting locations. For that, install the Places JavaScript SDK:
npm install @types/google.maps --save-dev
In your index.html
, load the Places API (replace YOUR_API_KEY
with your real key):
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places"></script>
👉 You can create an API key from the Google Cloud Console. Enable Places API, Maps JavaScript API, and optionally Geocoding API.
4. Sync Capacitor
After installing plugins, sync them into the native platforms:
ionic cap sync
5. Verify Installations
Run the project in browser mode:
ionic serve
You won’t see geofencing working in the browser (only on device/emulator), but you can test that the Google Places autocomplete script loads by checking the browser console for errors.
✅ Now the project has the required Capacitor and Google APIs ready for building geolocation and geofencing features.
Configuring Android and iOS for Location and Geofence Permissions
Mobile platforms require explicit permission declarations for apps that access the user’s location. Without these, your geolocation or geofence logic will fail silently.
1. Android Permissions
Open your android/app/src/main/AndroidManifest.xml
and add the following inside the <manifest>
tag but outside <application>
:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Why?
- ACCESS_COARSE_LOCATION → Approximate location (city/block level).
- ACCESS_FINE_LOCATION → Precise GPS location.
- ACCESS_BACKGROUND_LOCATION → Required for monitoring geofences when the app is closed or in the background.
⚡ Important: On Android 10+, background location requires special runtime permission prompts. We’ll handle that in code later.
2. iOS Permissions
Open ios/App/App/Info.plist
and add the following keys (place them as children of the root <dict>
element):
<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to monitor geofences around selected places.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need background location access to notify you when entering or leaving a geofence.</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
Why?
-
NSLocationWhenInUseUsageDescription
→ Required for foreground location tracking. -
NSLocationAlwaysAndWhenInUseUsageDescription
→ Required for background monitoring. -
UIBackgroundModes
→ Enables background location updates on iOS.
3. Sync Native Projects
After modifying platform configs, always sync changes:
ionic cap sync
Then rebuild your platforms:
ionic cap open android
ionic cap open ios
This opens the projects in Android Studio and Xcode so you can confirm the settings.
4. Testing Permissions
-
On Android → the system will ask for location permission at runtime.
-
On iOS → you’ll see a prompt: Allow app to use your location? with options for While Using or Always.
If you deny these, geofencing won’t work.
✅ At this point, your Ionic 8 + Capacitor app is fully configured to request and use location permissions on both Android and iOS.
Implementing Geolocation and Geofence Logic in Angular (Custom Geofence with Capacitor APIs)
Since Capacitor doesn’t have an official geofence plugin, we’ll build our own logic:
-
Track user location using
@capacitor-community/background-geolocation
. -
Store geofences in an array.
-
Check the distance between the current location and the geofence centers using the Haversine formula.
-
Emit enter/exit events manually.
1. Create a Geolocation Service
Generate a service to encapsulate location logic:
ionic g service services/geolocation.services
Now edit src/app/services/geolocation.service.ts
:
import { Injectable } from '@angular/core';
import { Geolocation } from '@capacitor/geolocation';
import {
BackgroundGeolocationPlugin,
Location,
} from '@capacitor-community/background-geolocation';
import { registerPlugin } from '@capacitor/core';
const BackgroundGeolocation = registerPlugin<BackgroundGeolocationPlugin>("BackgroundGeolocation");
export interface Geofence {
id: string;
latitude: number;
longitude: number;
radius: number; // in meters
inside?: boolean; // track state
}
@Injectable({
providedIn: 'root',
})
export class GeolocationService {
private watchId: string | null = null;
private geofences: Geofence[] = [];
private geofenceCallbacks: ((event: any) => void)[] = [];
constructor() { }
// Get current location once
async getCurrentPosition(): Promise<Location | null> {
try {
const position = await Geolocation.getCurrentPosition();
return {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
} as Location;
} catch (err) {
console.error('Error getting current position:', err);
return null;
}
}
// Watch continuous location updates
async startWatchingLocation(callback: (loc: Location) => void) {
this.watchId = await BackgroundGeolocation.addWatcher(
{
backgroundMessage: 'Tracking your location in the background',
backgroundTitle: 'Location Tracking',
requestPermissions: true,
stale: false,
},
(location, err) => {
if (err) {
if (err.code === 'NOT_AUTHORIZED') {
alert(
'This app needs location access. Please grant permission in settings.'
);
}
console.error('BackgroundGeolocation error:', err);
return;
}
if (location) {
callback(location);
this.checkGeofences(location);
}
}
);
}
async stopWatchingLocation() {
if (this.watchId) {
await BackgroundGeolocation.removeWatcher({ id: this.watchId });
this.watchId = null;
}
}
// Add geofence
addGeofence(id: string, latitude: number, longitude: number, radius: number) {
const geofence: Geofence = { id, latitude, longitude, radius, inside: false };
this.geofences.push(geofence);
console.log(`Geofence ${id} added`);
}
// Register a callback for geofence events
onGeofenceEvent(callback: (event: any) => void) {
this.geofenceCallbacks.push(callback);
}
// Check geofences against current location
private checkGeofences(location: Location) {
this.geofences.forEach((geofence) => {
const distance = this.getDistanceFromLatLonInM(
location.latitude,
location.longitude,
geofence.latitude,
geofence.longitude
);
const wasInside = geofence.inside;
const isInside = distance <= geofence.radius;
if (!wasInside && isInside) {
this.triggerGeofenceEvent({
id: geofence.id,
type: 'enter',
location,
});
} else if (wasInside && !isInside) {
this.triggerGeofenceEvent({
id: geofence.id,
type: 'exit',
location,
});
}
geofence.inside = isInside;
});
}
// Trigger event to listeners
private triggerGeofenceEvent(event: any) {
console.log('Geofence event:', event);
this.geofenceCallbacks.forEach((cb) => cb(event));
}
// Haversine formula: distance in meters
private getDistanceFromLatLonInM(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371000; // Earth radius in m
const dLat = this.deg2rad(lat2 - lat1);
const dLon = this.deg2rad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(this.deg2rad(lat1)) *
Math.cos(this.deg2rad(lat2)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
private deg2rad(deg: number): number {
return deg * (Math.PI / 180);
}
}
2. Update Home Component
In src/app/home/home.page.ts
:
import { Component, OnDestroy, OnInit } from '@angular/core';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel } from '@ionic/angular/standalone';
import { GeolocationService } from '../services/geolocation.services';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
imports: [IonLabel, IonItem, IonList, IonHeader, IonToolbar, IonTitle, IonContent, JsonPipe],
})
export class HomePage implements OnInit, OnDestroy {
currentLocation: any = null;
geofenceEvents: any[] = [];
constructor(private geoService: GeolocationService) { }
async ngOnInit() {
// Get current location
this.currentLocation = await this.geoService.getCurrentPosition();
// Start watching location
await this.geoService.startWatchingLocation((loc) => {
this.currentLocation = loc;
});
// Listen for geofence triggers
this.geoService.onGeofenceEvent((event) => {
this.geofenceEvents.push(event);
});
// Add a sample geofence (Google HQ in Mountain View)
this.geoService.addGeofence('GoogleHQ', 37.422, -122.084, 200);
}
async ngOnDestroy() {
await this.geoService.stopWatchingLocation();
}
}
3. Update the Template
Same as before (src/app/home/home.page.html
):
<ion-header>
<ion-toolbar>
<ion-title>Geofence Demo</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<h2>Current Location</h2>
<pre>{{ currentLocation | json }}</pre>
<h2>Geofence Events</h2>
<ion-list>
<ion-item *ngFor="let event of geofenceEvents">
<ion-label>
<strong>{{ event.id }}</strong>
<p>{{ event.type }}</p>
</ion-label>
</ion-item>
</ion-list>
</ion-content>
✅ Now you have working geofence detection without relying on any outdated/unpublished plugins. It’s all done in TypeScript with Capacitor’s background geolocation as the engine.
Integrating Google Places API to Select Geofence Locations
We’ll allow users to:
-
Search for places using the Google Places Autocomplete API.
-
Select a place and create a geofence around it.
1. Enable Places API in Google Cloud Console
-
Go to Google Cloud Console.
-
Create (or select) a project.
-
Enable Places API.
-
Generate an API Key and restrict it to your app’s package name (Android) and bundle ID (iOS).
2. Install HTTP Client
We’ll call the Places REST API using Angular’s HttpClient
.
npm install @angular/common@latest
3. Create a Places Service
Generate a service:
ionic g service services/places.service
Then edit src/app/services/places.service.ts
:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class PlacesService {
private apiKey = 'YOUR_GOOGLE_PLACES_API_KEY'; // 🔑 Replace this
constructor(private http: HttpClient) {}
// Search place suggestions
searchPlaces(query: string): Observable<any> {
const url = `https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${encodeURIComponent(
query
)}&key=${this.apiKey}`;
return this.http.get(url);
}
// Get place details (lat/lng)
getPlaceDetails(placeId: string): Observable<any> {
const url = `https://maps.googleapis.com/maps/api/place/details/json?placeid=${placeId}&key=${this.apiKey}`;
return this.http.get(url);
}
}
⚡ Don’t forget to replace YOUR_GOOGLE_PLACES_API_KEY
with your actual key.
4. Update Home Page to Use Places Service
Edit src/app/home/home.page.ts
:
import { Component, OnDestroy, OnInit } from '@angular/core';
import { IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel, IonInput } from '@ionic/angular/standalone';
import { GeolocationService } from '../services/geolocation.services';
import { JsonPipe } from '@angular/common';
import { PlacesService } from '../services/places.service';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
imports: [IonInput, IonLabel, IonItem, IonList, IonHeader, IonToolbar, IonTitle, IonContent, JsonPipe, FormsModule],
})
export class HomePage implements OnInit, OnDestroy {
currentLocation: any = null;
geofenceEvents: any[] = [];
searchQuery: string = '';
searchResults: any[] = [];
constructor(
private geoService: GeolocationService,
private placesService: PlacesService
) { }
async ngOnInit() {
// Get current location
this.currentLocation = await this.geoService.getCurrentPosition();
// Start watching location
await this.geoService.startWatchingLocation((loc) => {
this.currentLocation = loc;
});
// Listen for geofence triggers
this.geoService.onGeofenceEvent((event) => {
this.geofenceEvents.push(event);
});
// Add a sample geofence (Google HQ in Mountain View)
this.geoService.addGeofence('GoogleHQ', 37.422, -122.084, 200);
}
async ngOnDestroy() {
await this.geoService.stopWatchingLocation();
}
// Search places
search() {
if (this.searchQuery.trim().length === 0) {
this.searchResults = [];
return;
}
this.placesService.searchPlaces(this.searchQuery).subscribe((res: any) => {
this.searchResults = res.predictions;
});
}
// Select a place and add geofence
selectPlace(place: any) {
this.placesService.getPlaceDetails(place.place_id).subscribe((res: any) => {
const location = res.result.geometry.location;
this.geoService.addGeofence(
place.description,
location.lat,
location.lng,
200
);
alert(`Geofence added for ${place.description}`);
this.searchResults = [];
this.searchQuery = '';
});
}
}
5. Update Home Template
Replace src/app/home/home.page.html
with:
<ion-header>
<ion-toolbar>
<ion-title>Geofence Demo</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<h2>Current Location</h2>
<pre>{{ currentLocation | json }}</pre>
<h2>Add Geofence by Place</h2>
<ion-item>
<ion-input
placeholder="Search a place..."
[(ngModel)]="searchQuery"
(ionInput)="search()"
></ion-input>
</ion-item>
<ion-list *ngIf="searchResults.length > 0">
<ion-item
*ngFor="let result of searchResults"
(click)="selectPlace(result)"
>
<ion-label>{{ result.description }}</ion-label>
</ion-item>
</ion-list>
<h2>Geofence Events</h2>
<ion-list>
<ion-item *ngFor="let event of geofenceEvents">
<ion-label>
<strong>{{ event.id }}</strong>
<p>{{ event.type }}</p>
</ion-label>
</ion-item>
</ion-list>
</ion-content>
6. Allow API Requests (Dev Mode)
During local dev, add this to ionic.config.json
for CORS proxy:
{
"name": "ionic-geofence",
"integrations": {
"capacitor": {}
},
"type": "angular-standalone",
"proxies": [
{
"path": "/googleapi",
"proxyUrl": "https://maps.googleapis.com"
}
]
}
And update your URLs in places.service.ts
to start with /googleapi/...
.
✅ Now users can search places with Google Places API, select one, and instantly create a geofence around it.
Testing the App (Simulating Geofences in Android Studio & Xcode)
1. Testing on Android (Android Studio)
a. Run the App on Emulator
-
Open Android Studio.
-
Select Device Manager > Pixel Emulator (API 34+).
-
Run the app:
ionic cap run android -l --external
b. Simulate Location
-
While the app is running, open More (⋮) → Location in the emulator controls.
-
Enter latitude and longitude near one of your test geofences.
-
Click Set Location.
-
Example:
37.422, -122.084
(Googleplex).
-
c. Move Across Geofence Boundary
-
Start with a location outside the radius.
-
Change the location inside the geofence.
-
Observe logs in Android Studio Logcat:
Geofence Transition: ENTER
Geofence ID: Googleplex
- Move outside again → should trigger EXIT.
2. Testing on iOS (Xcode + Simulator)
a. Run the App on iOS
ionic cap run ios -l --external
Open the project in Xcode and launch the simulator (e.g., iPhone 15 Pro).
b. Simulate Location
-
In the top menu, select:
Features → Location → Custom Location… -
Enter coordinates near your geofence.
-
Example:
37.3318, -122.0312
(Apple HQ).
c. Test Enter & Exit
-
First, set a location outside the geofence.
-
Then switch to a location inside.
-
Check the app’s console logs for:
Geofence Transition: ENTER
Switch back outside → should show EXIT.
3. Debugging Tips
-
No events firing?
-
Ensure you gave Location Always permission in the app.
-
On iOS, background geofencing only works on a real device, not simulator.
-
-
API errors?
-
Make sure the Google Places API is enabled and API key restrictions are correct.
-
-
Radius too small?
-
Some simulators have precision limits — try
200m+
radius.
-
✅ At this point, you should be able to simulate entering/exiting geofences in both Android and iOS without leaving your desk.
Conclusion and Next Steps
✅ What We Built
In this tutorial, we successfully built a Geofencing-enabled mobile app using the latest stack:
-
Ionic 8 + Angular 20 + Capacitor for modern hybrid app development.
-
Capacitor Geolocation & Geofence plugins to monitor user location and trigger events.
-
Google Places API integration for searching and selecting geofence locations.
-
Cross-platform support (Android & iOS) with permissions and simulator testing.
With this foundation, the app can now:
-
Detect when users enter or exit defined areas.
-
Trigger actions like push notifications, UI updates, or background tasks.
-
Be extended into real-world use cases like delivery tracking, attendance apps, or location-based reminders.
📱 Preparing for Publishing
When you’re ready to ship to Google Play or Apple App Store, don’t forget:
-
App Signing & Builds
-
Android:
ionic cap build android --prod
Generate a signed APK/AAB in Android Studio.
-
iOS:
ionic cap build ios --prod
Archive and upload via Xcode Organizer.
-
-
API Key Security
-
Restrict your Google API key to:
-
Android package name (e.g.,
com.yourapp.geofence
). -
iOS bundle ID (e.g.,
com.yourapp.geofence
).
-
-
-
Permissions & Privacy Policy
-
Android: Update
AndroidManifest.xml
withACCESS_FINE_LOCATION
and background permissions. -
iOS: Ensure Info.plist includes
NSLocationWhenInUseUsageDescription
andNSLocationAlwaysAndWhenInUseUsageDescription
. -
Publish a Privacy Policy page if collecting user data.
-
🔋 Battery Optimizations
Geofencing apps can drain battery if not optimized. Follow these best practices:
-
Use larger geofence radii → fewer transitions and GPS checks.
-
Limit active geofences → Google Play recommends ≤ 100 geofences.
-
Rely on system-level geofencing APIs → Capacitor’s plugin already delegates to native Android/iOS geofencing, which is optimized for battery.
-
Throttle background updates → Avoid continuous GPS polling, rely on geofence triggers.
🚀 Next Steps
Here are some features you can add next:
-
Push Notifications → Trigger a notification when entering/exiting a geofence.
-
Server Syncing → Store geofence events in a backend (e.g., Firebase, Node.js, or Go).
-
Multiple Geofences → Allow users to add and manage several geofences at once.
-
Map Integration → Show geofences visually using Google Maps or Leaflet.
🎯 Final Thoughts
By upgrading from the old Ionic 3 + Cordova tutorial to Ionic 8 + Angular 20 + Capacitor, we’ve built a modern, forward-compatible app that leverages the latest web and mobile technologies.
This foundation gives you everything you need to create location-aware apps that are powerful, efficient, and ready for production.
You can find the working source code on our GitHub.
We know that building beautifully designed Ionic apps from scratch can be frustrating and very time-consuming. Check Ionic 6 - Full Starter App and save development and design time. Android, iOS, and PWA, 100+ Screens and Components, the most complete and advanced Ionic Template.
That's just the basics. If you need more deep learning about Ionic, Angular, and TypeScript, you can take the following cheap course:
- Ionic Apps with Firebase
- Ionic Apps for WooCommerce: Build an eCommerce Mobile App
- Ionic 8+: Build Food Delivery App from Beginner to Advanced
- IONIC - Build Android & Web Apps with Ionic
- Full Stack Development with Go, Vuejs, Ionic & MongoDB
- Create a Full Ionic App with Material Design - Full Stack
Thanks!