Learn how to add geofencing notifications to your web app for targeted, location-based user engagement and real-time alerts.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
What Is Geofencing and Why Should You Care?
Geofencing creates virtual boundaries around physical locations that trigger actions when users enter or exit them. For web apps, this can power location-based notifications that engage users at exactly the right moment and place.
Think of a coffee shop app notifying loyal customers of a special when they're within a block, or a retail website reminding someone about items in their cart when they're near a physical store. These context-aware notifications can drive engagement rates 3-5x higher than standard push notifications.
Core Technologies You'll Need
Step 1: Setting Up Geolocation Tracking
First, you need to request location permissions and track user positions. The browser's Geolocation API handles this:
// Request permission to use location
function requestLocationPermission() {
if (!("geolocation" in navigator)) {
console.error("Geolocation not supported by this browser");
return Promise.reject("Geolocation not supported");
}
return new Promise((resolve, reject) => {
navigator.permissions.query({ name: 'geolocation' }).then(permissionStatus => {
if (permissionStatus.state === 'granted') {
resolve();
} else if (permissionStatus.state === 'prompt') {
// We'll need to ask the user
navigator.geolocation.getCurrentPosition(
() => resolve(),
() => reject("Permission denied"),
{ enableHighAccuracy: true }
);
} else {
reject("Permission denied");
}
});
});
}
Step 2: Defining Your Geofences
Next, define the geographic boundaries for your geofences. Store these as coordinates with a radius:
const geofences = [
{
id: "store-downtown",
name: "Downtown Store",
lat: 40.7128,
lng: -74.0060,
radius: 500, // meters
message: "You're near our downtown location! Stop by for 15% off today.",
action: "/offers/downtown"
},
{
id: "headquarters",
name: "Company HQ",
lat: 37.7749,
lng: -122.4194,
radius: 200, // meters
message: "Welcome to our headquarters! Check in at reception for a visitor badge.",
action: "/visitor-info"
}
];
// Helper function to calculate distance between points
function calculateDistance(lat1, lon1, lat2, lon2) {
// Implementation of the Haversine formula for calculating
// distance between two points on the Earth's surface
const R = 6371e3; // Earth's radius in meters
const φ1 = lat1 * Math.PI/180;
const φ2 = lat2 * Math.PI/180;
const Δφ = (lat2-lat1) * Math.PI/180;
const Δλ = (lon2-lon1) * Math.PI/180;
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ/2) * Math.sin(Δλ/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c; // distance in meters
}
Step 3: Implementing Position Monitoring
Now, create a system that monitors position and checks for geofence triggers:
class GeofenceMonitor {
constructor(geofences) {
this.geofences = geofences;
this.watchId = null;
this.enteredGeofences = new Set(); // Track which geofences user is already in
}
start() {
if (this.watchId !== null) return;
const options = {
enableHighAccuracy: true,
maximumAge: 30000, // Accept positions up to 30 seconds old
timeout: 27000 // Wait up to 27 seconds for a position
};
this.watchId = navigator.geolocation.watchPosition(
this.handlePositionUpdate.bind(this),
this.handleError.bind(this),
options
);
console.log("Geofence monitoring started");
}
stop() {
if (this.watchId === null) return;
navigator.geolocation.clearWatch(this.watchId);
this.watchId = null;
this.enteredGeofences.clear();
console.log("Geofence monitoring stopped");
}
handlePositionUpdate(position) {
const currentLat = position.coords.latitude;
const currentLng = position.coords.longitude;
// Check each geofence
this.geofences.forEach(geofence => {
const distance = calculateDistance(
currentLat, currentLng,
geofence.lat, geofence.lng
);
const isInGeofence = distance <= geofence.radius;
const wasInGeofence = this.enteredGeofences.has(geofence.id);
// Entered geofence
if (isInGeofence && !wasInGeofence) {
this.enteredGeofences.add(geofence.id);
this.triggerGeofenceEnter(geofence);
}
// Exited geofence
else if (!isInGeofence && wasInGeofence) {
this.enteredGeofences.delete(geofence.id);
this.triggerGeofenceExit(geofence);
}
});
}
handleError(error) {
console.error("Geolocation error:", error.message);
// Implement error handling strategy (retry, fallback, etc.)
}
triggerGeofenceEnter(geofence) {
console.log(`Entered geofence: ${geofence.name}`);
// This is where we'll send the notification
this.sendNotification(geofence);
}
triggerGeofenceExit(geofence) {
console.log(`Exited geofence: ${geofence.name}`);
// Optional: do something when user leaves area
}
sendNotification(geofence) {
// We'll implement this in the next step
}
}
// Usage
const monitor = new GeofenceMonitor(geofences);
requestLocationPermission()
.then(() => monitor.start())
.catch(error => console.error("Couldn't start geofence monitoring:", error));
Step 4: Setting Up Service Worker and Push Notifications
To send notifications when the app isn't active, you need a service worker:
// In your main application code
function registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/geofence-sw.js')
.then(registration => {
console.log('ServiceWorker registered with scope:', registration.scope);
return requestNotificationPermission();
})
.catch(error => {
console.error('ServiceWorker registration failed:', error);
});
}
}
function requestNotificationPermission() {
return new Promise((resolve, reject) => {
if (!('Notification' in window)) {
reject("This browser doesn't support notifications");
return;
}
if (Notification.permission === 'granted') {
resolve();
return;
}
if (Notification.permission !== 'denied') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
resolve();
} else {
reject("Notification permission denied");
}
});
} else {
reject("Notification permission previously denied");
}
});
}
// Initialize everything
Promise.all([
registerServiceWorker(),
requestLocationPermission()
])
.then(() => {
const monitor = new GeofenceMonitor(geofences);
monitor.start();
})
.catch(error => {
console.error("Setup failed:", error);
// Show appropriate UI for permission issues
});
Now implement the service worker in a separate file (geofence-sw.js):
// geofence-sw.js
self.addEventListener('install', event => {
console.log('Service Worker installing');
self.skipWaiting(); // Activate immediately
});
self.addEventListener('activate', event => {
console.log('Service Worker activating');
return self.clients.claim(); // Take control immediately
});
// Listen for messages from the main application
self.addEventListener('message', event => {
const data = event.data;
if (data.type === 'GEOFENCE_ENTER') {
const geofence = data.geofence;
// Show the notification
self.registration.showNotification(
geofence.name,
{
body: geofence.message,
icon: '/images/notification-icon.png',
badge: '/images/notification-badge.png',
vibrate: [100, 50, 100],
data: {
action: geofence.action
}
}
);
}
});
// Handle notification clicks
self.addEventListener('notificationclick', event => {
event.notification.close();
const action = event.notification.data.action;
event.waitUntil(
clients.matchAll({type: 'window'}).then(clientList => {
// If a window client already exists, focus it
for (const client of clientList) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
return client.focus().then(() => {
if (action) client.navigate(action);
});
}
}
// Otherwise, open a new window
if (action && clients.openWindow) {
return clients.openWindow(action);
}
})
);
});
Step 5: Connecting the Geofence Monitor to the Notification System
Now, update the GeofenceMonitor to send notifications through the service worker:
// Update the GeofenceMonitor class with this method
sendNotification(geofence) {
// Check if service worker is active and notification permission granted
if (!('serviceWorker' in navigator) ||
Notification.permission !== 'granted') {
console.warn("Can't send notification - missing permissions or service worker");
return;
}
// Store this entry in local storage to prevent duplicate notifications
const notificationKey = `notified_${geofence.id}`;
const lastNotified = localStorage.getItem(notificationKey);
const now = Date.now();
// Don't notify for the same geofence more than once every 24 hours
if (lastNotified && (now - parseInt(lastNotified)) < 24 * 60 * 60 * 1000) {
console.log(`Skipping notification for ${geofence.name} - already notified recently`);
return;
}
// Send message to service worker
navigator.serviceWorker.ready.then(registration => {
localStorage.setItem(notificationKey, now.toString());
if (registration.active) {
registration.active.postMessage({
type: 'GEOFENCE_ENTER',
geofence: geofence
});
}
});
}
Battery Life Optimization
Continuous location tracking can drain batteries quickly. Here's how to optimize:
// Add this to your GeofenceMonitor class
optimizeTracking() {
// Start with high-frequency updates when app is in use
const foregroundOptions = {
enableHighAccuracy: true,
maximumAge: 10000,
timeout: 15000
};
// Use less frequent updates when app is in background
const backgroundOptions = {
enableHighAccuracy: false,
maximumAge: 60000,
timeout: 30000
};
// Use visibility API to detect when page is hidden/visible
document.addEventListener('visibilitychange', () => {
if (this.watchId !== null) {
// Restart tracking with appropriate options
navigator.geolocation.clearWatch(this.watchId);
const options = document.hidden ? backgroundOptions : foregroundOptions;
this.watchId = navigator.geolocation.watchPosition(
this.handlePositionUpdate.bind(this),
this.handleError.bind(this),
options
);
}
});
}
Handling Poor Connectivity
Background Sync API ensures notifications are processed even if connectivity is spotty:
// In your service worker (geofence-sw.js)
self.addEventListener('sync', event => {
if (event.tag === 'geofence-sync') {
event.waitUntil(
// Process any queued geofence events
getPendingGeofenceEvents().then(events => {
return Promise.all(events.map(processGeofenceEvent));
})
);
}
});
// Helper functions for managing queued events
function queueGeofenceEvent(event) {
return idb.open('geofence-db').then(db => {
const tx = db.transaction('events', 'readwrite');
tx.objectStore('events').put(event);
return tx.complete;
});
}
function getPendingGeofenceEvents() {
return idb.open('geofence-db').then(db => {
return db.transaction('events')
.objectStore('events')
.getAll();
});
}
function processGeofenceEvent(event) {
// Process the event, then remove from queue
// ...
}
Here's how all these pieces come together in a practical retail application:
// Initialize the geofences for a retail chain
const retailStores = [
{
id: "nyc-flagship",
name: "NYC Flagship Store",
lat: 40.7128,
lng: -74.0060,
radius: 500,
message: "Visit our NYC store today and get 20% off your next purchase!",
action: "/stores/nyc/offers"
},
// Additional stores...
];
// Customize for retail-specific behavior
class RetailGeofenceMonitor extends GeofenceMonitor {
constructor(geofences, userPreferences) {
super(geofences);
this.userPreferences = userPreferences; // User's notification preferences
this.cart = null; // Will hold abandoned cart info
}
// Override to add retail-specific logic
triggerGeofenceEnter(geofence) {
// Get the user's shopping cart if they've abandoned one
fetch('/api/user/cart')
.then(response => response.json())
.then(cart => {
this.cart = cart;
// Customize notification based on cart contents and location
if (cart && cart.items && cart.items.length > 0) {
// Modify the geofence message to mention cart items
const customGeofence = {...geofence};
customGeofence.message = `You have ${cart.items.length} items in your cart. Visit our ${geofence.name} to complete your purchase!`;
this.sendNotification(customGeofence);
} else {
// Standard notification for users without items in cart
this.sendNotification(geofence);
}
})
.catch(error => {
console.error("Failed to fetch cart:", error);
// Fall back to standard notification
this.sendNotification(geofence);
});
}
}
// Usage
document.addEventListener('DOMContentLoaded', () => {
// Get user preferences from profile
fetch('/api/user/preferences')
.then(response => response.json())
.then(preferences => {
// Only start if user has opted in to location features
if (preferences.locationEnabled) {
Promise.all([
registerServiceWorker(),
requestLocationPermission()
])
.then(() => {
const retailMonitor = new RetailGeofenceMonitor(retailStores, preferences);
retailMonitor.start();
retailMonitor.optimizeTracking();
})
.catch(error => {
console.error("Setup failed:", error);
// Show UI for enabling permissions
document.getElementById('enable-location').style.display = 'block';
});
} else {
// Show UI for opting into the feature
document.getElementById('location-benefits').style.display = 'block';
}
});
});
What to Measure
Once implemented, track these key performance indicators:
Real-world impact: Retail clients implementing similar systems have seen a 23% increase in abandoned cart recovery and a 15% lift in store visit frequency among their most engaged app users.
Geofencing notifications represent one of those rare features that bridges the digital-physical divide, creating contextually relevant experiences that feel almost magical to users.
While implementation requires careful consideration of location services, permissions, and battery usage, the technical complexity is manageable with modern web APIs. The key is finding the right balance between technical capability and user experience—notifications frequent enough to be valuable but not so persistent they become annoying.
When done right, geofencing can transform passive web app users into active customers engaging with your business in physical locations—all through the power of well-timed, contextually relevant notifications.
Explore the top 3 practical geofencing notification use cases to boost your web app’s engagement and user experience.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â