Feature spec
AdvancedCategory
notifications-alerts
Build with AI
6-10 hours with Lovable or V0
Custom build
2-4 weeks custom dev
Running cost
$0-25/mo up to 100 users · $75-150/mo at 10,000 users
Works on
Everything it takes to ship Geofencing Notifications — parts, prompts, and real costs.
Geofencing notifications need four pieces: browser Geolocation API to watch the user's position, a Haversine or turf.js calculation to check whether that position crosses a zone boundary, a state machine to fire the notification only once per zone entry (not on every position update), and a Supabase table with PostGIS for server-side validation of high-stakes triggers. With Lovable or V0 you can ship a working web geofencing feature in 6-10 hours. Running cost is $0-25/month at 100 users, rising to $75-150/month at 10,000 users as Mapbox map loads become the main variable.
What Web Geofencing Notifications Actually Are
Geofencing notifications fire when a user's device crosses into or out of a defined geographic zone. On the web, this means the browser Geolocation API continuously watches the user's position using watchPosition(), and a client-side function evaluates whether the current coordinates fall inside any active geofence radius. When the user transitions from outside to inside a zone (or vice versa), a Web Notification fires and a geofence_events row is logged in Supabase. The critical architectural constraint for web geofencing is that it is entirely foreground-only — the browser cannot detect location when the tab is closed or the device screen is off. True background geofencing requires a native mobile app (Flutter with the geolocator package). The product decisions that matter are: who defines zones (admins draw them on a map), how large they are (100m radius works reliably, 10m does not), and whether zone entry should trigger server-side actions (unlock a discount, log a compliance check-in) in addition to a client-side notification.
What users consider table stakes in 2026
- Users opt into location tracking by clicking an explicit 'Enable location alerts' button — geolocation is never requested automatically on page load
- Notification fires once when the user enters a zone, not on every GPS position update while they remain inside the zone
- Zone management map shows all active geofences as visible circles with the radius drawn at scale, with zone name labels, so admins can verify zone coverage before activating
- Notification includes the zone name and a relevant call to action (e.g., tap to see the offer, confirm check-in) matching the zone's configured metadata
- When a user denies geolocation permission, the app shows a clear in-app explanation with browser-specific instructions for re-enabling it — not just a generic error
- Event log of zone entries and exits per user is visible in an admin dashboard for analytics and compliance verification
Anatomy of the Feature
Seven components. The map UI and Supabase tables are what AI tools generate correctly. The zone membership state machine, watchPosition cleanup, and PostGIS coordinate-axis ordering are the components where builds break.
Geolocation Watcher
Servicenavigator.geolocation.watchPosition(successCallback, errorCallback, { enableHighAccuracy: false, maximumAge: 30000, timeout: 5000 }) starts tracking when the user clicks 'Enable location alerts'. The maximumAge: 30000 setting allows the browser to cache positions for up to 30 seconds, reducing battery impact. enableHighAccuracy: false avoids GPS radio activation (uses WiFi/cell triangulation) which is sufficient for zone radii above 50 meters.
Note: watchPosition must be cleaned up by calling navigator.geolocation.clearWatch(watchId) when the component unmounts or the user disables tracking — a forgotten watchId keeps the browser's location permission indicator active and drains battery.
Zone Definition UI
UIMapbox GL JS or Leaflet.js map with circle drawing capability. Admins click a point on the map to set the zone center and drag a radius slider to configure the radius in meters. The zone center (center_lat, center_lng) and radius_meters are saved to the geofences Supabase table on form submit. Mapbox GL JS renders existing zones as circle layers using the addSource / addLayer API inside the map.on('load') callback.
Note: All map source and layer additions must be wrapped inside the map.on('load', () => { ... }) callback — not at React component mount time. Adding layers before the style loads produces a silent failure where no circles appear on the map.
Geofences Table
DataSupabase PostgreSQL table with PostGIS extension enabled: geofences (id uuid, name text, center_lat numeric, center_lng numeric, radius_meters int, trigger_on text CHECK trigger_on IN ('enter','exit','both'), is_active bool, metadata jsonb, created_at timestamptz). The metadata jsonb field stores zone-specific CTA data (offer text, deep link, compliance action type). PostGIS is enabled via CREATE EXTENSION IF NOT EXISTS postgis in the Supabase SQL editor.
Note: PostGIS must be enabled manually in the Supabase Dashboard under Database → Extensions — it is not enabled by default. Without it, the ST_DWithin server-side validation query will fail with 'function does not exist'.
Client-Side Zone Checker
ServiceJavaScript Haversine formula or turf.js booleanPointInPolygon() / distance() evaluates the current watchPosition coordinates against all active geofences loaded into memory. Zone membership state is tracked in a JavaScript Map keyed by geofence ID: { [geofenceId]: 'inside' | 'outside' }. An enter notification fires only when the state transitions from 'outside' to 'inside'. An exit notification fires only on 'inside' to 'outside'. The geofence list is fetched once on initialization and refreshed every 5 minutes.
Note: Turf.js is the recommended library for polygon geofences; the Haversine formula is sufficient for circle geofences defined by center + radius and is simpler to implement in a prompt.
Zone Event Logger
BackendSupabase insert via the client-side SDK records geofence_events (user_id, geofence_id, event_type 'enter'|'exit', lat, lng, occurred_at) when a zone transition fires. Deduplication is enforced at the application layer: only log if the previous event for the same user+geofence pair was of a different type (no consecutive 'enter' events). A UNIQUE constraint on (user_id, geofence_id, event_type, date_trunc('minute', occurred_at)) provides a database-level safety net.
Note: Do not rely solely on the unique constraint for deduplication — the zone membership state machine in the client-side checker is the primary guard. The constraint is a last-resort failsafe for edge cases.
Notification Trigger
ServiceOn zone enter or exit: if Notification.permission === 'granted', calls new Notification(zoneName, { body: notificationBody, icon: '/icon.png', data: { geofenceId, ctaUrl } }) to show a Web Notification. Falls back to an in-app toast (sonner or shadcn/ui Toast) if permission is denied or if the user has not opted in to browser notifications. The notification body and CTA link are drawn from the geofence's metadata.cta_text and metadata.deep_link fields.
Note: Web Notifications require Notification.requestPermission() to have been called first — this must happen in response to a user gesture (button click), never automatically. The browser blocks auto-triggered permission requests and logs a console warning.
PostGIS Server-Side Validation
BackendSupabase Edge Function for server-side zone membership verification using ST_DWithin: SELECT id, name FROM geofences WHERE is_active = true AND ST_DWithin(ST_MakePoint(lng, lat)::geography, ST_MakePoint(center_lng, center_lat)::geography, radius_meters). Called for high-stakes triggers like unlocking discounts or logging compliance check-ins where client-side spoofing must be prevented. Returns the list of geofences containing the submitted coordinates.
Note: ST_MakePoint takes arguments as (longitude, latitude) — x-axis first. Passing (latitude, longitude) causes the function to return no results even for coordinates clearly inside a radius. Always validate with a known coordinate pair against a known geofence before deploying.
The data model
Run this in the Supabase SQL editor. Enables PostGIS, creates the geofences and event log tables with RLS, and adds the spatial index for fast zone lookup. Enabling PostGIS is the first step — do it before creating the geofences table.
1-- Step 1: Enable PostGIS extension (run this first)2create extension if not exists postgis;34-- Geofences table: zones defined by admin users5create table public.geofences (6 id uuid default gen_random_uuid() primary key,7 name text not null,8 center_lat numeric not null,9 center_lng numeric not null,10 radius_meters int default 100,11 trigger_on text default 'enter' check (trigger_on in ('enter','exit','both')),12 is_active bool default true,13 metadata jsonb, -- { cta_text, deep_link, compliance_action, offer_code }14 created_at timestamptz default now()15);1617alter table public.geofences enable row level security;1819-- Authenticated users can read active geofences (to load into client-side checker)20create policy "Authenticated users can read active geofences"21 on public.geofences for select22 to authenticated23 using (is_active = true);2425-- Only service role (admins) can create/update/delete geofences26create policy "Service role manages geofences"27 on public.geofences for all28 to service_role using (true);2930-- Spatial index for fast ST_DWithin queries31create index geofences_location_idx32 on public.geofences33 using gist (st_makepoint(center_lng, center_lat)::geography);3435-- Geofence events: zone entry and exit log36create table public.geofence_events (37 id uuid default gen_random_uuid() primary key,38 user_id uuid references auth.users,39 geofence_id uuid references public.geofences(id) on delete cascade,40 event_type text check (event_type in ('enter','exit')),41 lat numeric,42 lng numeric,43 occurred_at timestamptz default now()44);4546alter table public.geofence_events enable row level security;4748-- Users can insert their own events and read their own history49create policy "Users can insert own geofence events"50 on public.geofence_events for insert51 with check (auth.uid() = user_id);5253create policy "Users can read own geofence events"54 on public.geofence_events for select55 using (auth.uid() = user_id);5657-- Admins (service_role) can read all events for the dashboard58create policy "Service role can read all events"59 on public.geofence_events for select60 to service_role using (true);6162create index geofence_events_user_time_idx63 on public.geofence_events (user_id, occurred_at desc);6465-- PostGIS server-side validation function66-- Call this from an Edge Function with the user's lat/lng67create or replace function public.check_geofence_membership(68 user_lat numeric,69 user_lng numeric70) returns table (geofence_id uuid, geofence_name text, trigger_on text, metadata jsonb)71language sql72security definer73set search_path = public74as $$75 select id, name, trigger_on, metadata76 from geofences77 where is_active = true78 and st_dwithin(79 st_makepoint(user_lng, user_lat)::geography,80 st_makepoint(center_lng, center_lat)::geography,81 radius_meters82 );83$$;Heads up: The spatial index on geofences uses a GiST index type required by PostGIS geography operations. The check_geofence_membership function uses SECURITY DEFINER so it can be called from client-side code via Supabase RPC without granting direct table access to the geography functions.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Full geofencing stack: PostGIS spatial indexes, native mobile background geofencing via Flutter geolocator package, Mapbox GL JS with custom polygon drawing, and a multi-zone analytics dashboard. Required for background detection or high-accuracy use cases.
Step by step
- 1PostGIS with spatial indexes (GiST on geography column) for fast server-side zone lookup across thousands of geofences without full table scan
- 2Native mobile background geofencing via Flutter geolocator package with CLRegion (iOS) and Android geofencing API — these fire zone events even when the app is not in the foreground or the device screen is off
- 3Mapbox GL JS with polygon drawing tools (Mapbox Draw plugin) for irregular shape zones beyond simple circles — retail floor plans, campus boundaries, hospital wings
- 4Zone analytics dashboard: heat map of entry/exit events per zone per hour, user dwell time per zone, conversion events linked to zone entries
- 5Server-side webhook trigger on zone entry via Supabase Realtime: geofence_events INSERT triggers a webhook to an external CRM, access control system, or logistics platform
Where this path bites
- Native background geofencing on iOS via CLRegion is limited to 20 monitored regions per app — apps monitoring more regions need a server-side zone clustering strategy
- Sub-meter accuracy for construction, healthcare, or access control use cases is beyond what browser or standard Flutter GPS provides — requires dedicated hardware (UWB beacons, BLE positioning)
Third-party services you'll need
The Geolocation API and PostGIS are free. Mapbox is the primary cost variable — the map rendering library charges per map load above the free tier.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Mapbox GL JS | Map rendering for the admin zone management UI and visual radius overlays; Geocoding API for address-to-coordinates conversion | 50,000 map loads/mo free | Pay-as-you-go $5 per 1,000 map loads above free tier |
| Turf.js | Client-side geospatial calculations — booleanPointInPolygon for polygon geofences, distance for circle geofences | Free, open-source | Free |
| PostGIS (via Supabase) | Server-side spatial validation using ST_DWithin for high-stakes zone entry checks | Included in all Supabase tiers when extension is enabled | No additional cost; Supabase Pro $25/mo needed for Edge Function execution |
| Google Maps JavaScript API | Alternative to Mapbox for map rendering; broader familiarity but higher cost above free tier | $200/mo credit (approximately 28,000 map loads) | $7 per 1,000 map loads (approx) |
| Supabase Pro | Needed for PostGIS extension availability and Edge Function execution for server-side validation | PostGIS available on free tier; Edge Functions limited | $25/mo |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Mapbox free tier covers 50K map loads/mo — 100 users loading the admin map rarely stays well within this. Supabase free tier covers the geofences and events tables. If any Edge Functions are used for server-side validation, Pro $25/mo may be needed. Range: $0-25/mo.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Geolocation API returns permission denied even though user clicked Allow
Symptom: The browser Geolocation API only operates in secure contexts (HTTPS) and is blocked entirely inside sandboxed iframes. Both Lovable's preview pane and V0's sandbox run the app in an embedded iframe with a different origin from the published URL. When navigator.geolocation.getCurrentPosition() or watchPosition() is called in this context, the browser silently denies it and calls the error callback with code 1 (PERMISSION_DENIED), even if the user explicitly clicks Allow in the system dialog — because the permission applies to the iframe origin, not the published domain.
Fix: Publish the app to HTTPS and test all geolocation and notification functionality from the live URL on a real device. Add feature detection: if (!('geolocation' in navigator)) show a clear message. Never interpret PERMISSION_DENIED in preview as a code bug — it is an expected browser security restriction.
Enter notification fires dozens of times while user stands inside a zone
Symptom: watchPosition fires a callback on every significant position update from the GPS or WiFi positioning system, which can happen every 2-10 seconds. Without a zone membership state machine, each position update that falls inside a geofence triggers a new notification. For a user standing in one location for 5 minutes, this produces 30-150 duplicate enter notifications — effectively spamming the user and burning through any rate limit on the notification API.
Fix: Maintain a JavaScript Map (zoneStates) keyed by geofence ID with values 'inside' or 'outside'. On each position update, compute the zone state for every geofence. Only fire the enter notification when the state transitions from 'outside' to 'inside'. Only fire the exit notification when transitioning from 'inside' to 'outside'. Initialize all zones as 'outside' on load.
Zone circles don't appear on the Mapbox admin map
Symptom: Mapbox GL JS requires that addSource() and addLayer() calls are made after the map style has fully loaded. In React, the map object is available as soon as the Map constructor runs inside useEffect, but the style — which determines what layers can be added — finishes loading asynchronously. If addSource/addLayer are called at Map constructor time or at component mount time, the style is not ready and the calls fail silently. The map renders tile imagery but no geofence circles appear.
Fix: Wrap all addSource and addLayer calls inside the map.on('load', () => { addGeofenceLayers() }) callback. When new geofences are created or the geofence list is refreshed, check if the style is already loaded using map.isStyleLoaded() before attempting to add layers, and call map.on('load', callback) otherwise.
PostGIS ST_DWithin returns no results even for coordinates inside the radius
Symptom: ST_MakePoint takes arguments in (longitude, latitude) order — x-axis (east-west) first, y-axis (north-south) second. This is the opposite of the conventional (latitude, longitude) order used in the Geolocation API result (coords.latitude, coords.longitude), most mapping documentation, and everyday language. When the arguments are passed in the wrong order, the ST_DWithin calculation computes the distance between two geographically nonsensical points and returns no matches.
Fix: Always use ST_MakePoint(longitude, latitude) — not ST_MakePoint(latitude, longitude). Validate by running the function against a known coordinate pair: pick a location you know is inside a zone, run the query manually in the Supabase SQL editor, and verify it returns that zone. Add a comment in the SQL to document the argument order to prevent future confusion.
Best practices
Never request geolocation permission automatically on page load — request it after the user clicks an explicit 'Enable location alerts' button and show an in-app explanation modal before the browser's system dialog appears
Use enableHighAccuracy: false in watchPosition options — for zone radii above 50 meters, WiFi/cell triangulation is sufficient and does not activate the GPS radio, significantly reducing battery impact
Always clean up the watchPosition watchId in useEffect's cleanup function — a forgotten watchId keeps the browser location indicator active, confuses users, and drains battery
Wrap all Mapbox addSource and addLayer calls inside the map.on('load') event — this is the single most common source of invisible geofence circles in new builds
Always pass ST_MakePoint(longitude, latitude) — never (latitude, longitude) — and validate against a known coordinate pair before deploying the PostGIS validation function
Cache the geofence list in a useRef (not useState) after the initial fetch to avoid re-renders triggering re-fetches on every position update; refresh the list every 5 minutes via a separate interval
For map load cost optimization at scale, load the geofence list via a lightweight JSON endpoint instead of rendering a full Mapbox map on every page visit — only show the map on the admin zone management page
When You Need Custom Development
Lovable and V0 handle web foreground geofencing well. Certain requirements make a custom build necessary:
- App requires background geofencing that triggers when the user's mobile device is not actively using the app — web browsers cannot detect location when the tab is closed; true background geofencing requires a native Flutter app with the geolocator package using CLRegion (iOS) and Android Geofencing API
- Geofence entry must trigger server-side actions beyond client-side notifications — unlocking content, logging a compliance check-in with a timestamp, or posting a webhook to an external access control or logistics system requires a hardened PostGIS server-side validation pipeline
- Large number of geofences (1,000+) require a spatial indexing strategy and zone clustering on the server side — loading thousands of geofences into the client-side checker creates a performance bottleneck on every position update
- Sub-meter accuracy required for construction site access control, healthcare facility tracking, or restricted area compliance — browser Geolocation provides 10-50 meter accuracy under typical conditions, which is insufficient for these use cases
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
Does geofencing work when the browser tab is closed?
No. Web browser geofencing using the Geolocation API is entirely foreground-only — the API stops firing position updates as soon as the tab loses focus or the screen locks. If your use case requires notifications when the device is away from the app, you need a native mobile app built with Flutter using the geolocator package, which uses iOS CLRegion and Android Geofencing API to detect zone crossings in the background.
How accurate is browser geolocation for geofencing?
Typical accuracy with enableHighAccuracy: false (WiFi and cell triangulation) is 10-50 meters in urban areas with good signal. With enableHighAccuracy: true (GPS radio), accuracy improves to 3-10 meters outdoors but degrades significantly indoors and drains battery faster. For zone radii under 50 meters, GPS accuracy is required and even then indoor detection is unreliable. Design your zones with radii of at least 100 meters for reliable detection in all conditions.
Can I draw an irregular polygon zone or only circles?
Both are possible. Circle geofences (center_lat, center_lng, radius_meters) are simpler to implement and cover the majority of use cases — a store's entrance area, a campus building, a neighborhood. Polygon geofences require storing coordinate arrays (PostGIS POLYGON geometry type) and using turf.js booleanPointInPolygon() for client-side checking instead of the Haversine formula. For a first build, start with circles; add polygon support as a follow-on feature once the circle pipeline is working.
What is the smallest geofence radius I can reliably detect?
100 meters is the practical minimum for reliable detection in a web app using browser geolocation. Below 100 meters, GPS drift and positioning jitter (±10-30 meters) cause false enter and exit events as the device appears to move in and out of the zone while the user is standing still. If you need smaller zones — a specific door, a retail display, a hospital room — you need BLE beacons or UWB hardware, not browser geolocation.
Will geofencing drain the user's phone battery?
Using enableHighAccuracy: false and maximumAge: 30000 in watchPosition significantly reduces battery impact — the browser uses WiFi and cell positioning rather than the GPS radio. On Chrome on Android, watchPosition in this mode consumes roughly the same battery as having a background tab open. For comparison, a native app using Android's Geofencing API or iOS CLRegion monitoring uses far less battery because the OS handles zone checking at the hardware level rather than a browser tab.
How do I prevent the same notification from firing repeatedly inside a zone?
Track zone membership state in a JavaScript Map keyed by geofence ID. Initialize all zones as 'outside'. On each position update, if a zone's computed distance is less than its radius and the current state is 'outside', transition to 'inside' and fire the notification. Only fire again when the user exits and re-enters the zone. This state machine is the single most important piece of logic in the feature — without it, every position update inside a zone triggers a notification.
Can I trigger server-side actions when a user enters a zone?
Yes, via the PostGIS server-side validation API route. When a zone entry fires client-side, send the user's coordinates to a Next.js API route or Supabase Edge Function that runs ST_DWithin to verify membership server-side before triggering the action (unlock a discount, record a compliance check-in, post to a webhook). This prevents client-side spoofing and ensures high-stakes actions are validated independently of the client-side zone checker.
What is the difference between web geofencing and native app geofencing?
Web geofencing runs in a browser tab and stops detecting location the moment the tab is not active. Native app geofencing (iOS CLRegion, Android Geofencing API, Flutter geolocator package) runs at the OS level and fires zone events in the background even when the device screen is locked. Native also uses significantly less battery because the OS handles zone detection in hardware rather than a JavaScript process. Web geofencing is appropriate for kiosk-style apps, retail store tools, or web apps where users are expected to have the page open; native is required for any use case where detection must happen while the app is not in the foreground.
Need this feature production-ready?
RapidDev builds geofencing notifications into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.