How to Build a Rideshare Dispatch System with Geofencing
Do you want to build a real rideshare dispatch system for an on-demand ride-hailing platform or ride-sharing app? If so, you’re in the right place. This tutorial will walk you through setting up a deployable dispatch foundation in about a week. No, this won’t be your average application showing you a map on a screen. We’ll focus on two small but high-impact/advanced features that real taxi apps and ride-hailing apps rely on:
Geo-fenced Driver Eligibility — only drivers physically inside the pickup zone are eligible for trips.
Fair FIFO Dispatch Queue — once eligible, drivers are assigned in first-in/first-out order for predictable, dispute-free hand-offs.
We’ll utilize the Google Maps API (a location-based services API used for routing, route optimization, traffic data, points of interest, and map tiles) alongside PubNub to implement real-time App-Store-grade behavior you see on iOS/Android production apps step-by-step. At the bottom, you’ll find the background resources used to build this tutorial. Lastly, everything used here has a free tier — you can build this for free.
Who is this for? A startup or development company building an MVP for Uber/Lyft-style taxi booking in urban areas, taxi service providers and taxi companies modernizing their backend, or teams doing taxi app development who want to streamline their technology stack and improve customer experience, rider/driver retention, and day-to-day user experience.
What you’ll build: a geofencing dispatch system
The following two features are the core of geofencing technology, and we can utilize PubNub to make it easier to create a scalable production-grade taxi booking application—whether it is Lyft, Uber, DoorDash, a mobile app, or a web app:
Geofenced Eligibility (H3 + PubNub Presence/State)
Devices convert GPS location data/device location to H3 hex cells and flip a lightweightinZoneflag. Eligibility becomes a constant-time lookup:currentCell ∈ allowedCells. These virtual boundaries (aka geofencing) are fast and reliable for location tracking.Fair FIFO Dispatch Queue for Ride-Hailing Apps
WheninZonebecomestrue, a driver joins a queue with ajoinTs. New rider requests go to the head of the line; timeouts bump the driver and continue. This reduces wait times and driver travel time while keeping assignment algorithms simple and auditable.
Geofencing in taxi apps usually starts here: virtual boundaries at the pickup zone, not city-wide matching. They all use this functionality at their core for GPS tracking, real-time tracking, notifications (for in-app driver pings), and fair dispatch. Both of these will be used in conjunction as we ship one tiny, user-friendly interactive component: a Queue Visualizer you can drop into a Next.js page to simulate join/leave/assign behavior.
Prerequisites
For this project, we'll launch a minimal, automate-friendly queue + eligibility MVP on a slim technology stack first. You can then layer advanced features like driver in-app notifications, push notifications, surge pricing, dynamic pricing, and fleet management dashboards for future enhancements.
Basic React/Next.js (frameworks & project dependencies)
Node.js 18+
A free PubNub account (for real-time messaging)
(Optional) Google Cloud project if you want to draw maps (the eligibility logic doesn’t require Maps).
(Optional) A test device on Android (uses Google Play services / play services) or iOS for geolocation/current location field testing.
Set up your PubNub account
Go to admin.pubnub.com/signup and create a free account.
If you already have one, sign in to your PubNub Dashboard.
Click Apps in the left-hand sidebar → Create New App → name it (e.g., “Rideshare Dispatch”).
Create a Keyset.
To ensure your keyset is configured correctly, enable Presence (Select “All channels”), Message Persistence (1 day), and Stream Controller (For subscribing to channels). Leave Access Manager disabled for now.
Click Save on all changes.
Important: Don’t deploy to production without Access Manager enabled to protect user data and restrict who can publish/subscribe.
Google Maps geofencing
Google Maps provides interactive maps, geocoding, routes, and places (points of interest) through Google’s location services. You do not need Maps to compute eligibility. H3 + PubNub handles all the logic; Maps are for debugging, demos, and operator views. A typical Google Maps API use case here is to visualize fences, inspect traffic data, and preview route optimization impacts on driver travel time. Make sure to keep an eye on pricing if you exceed free tier usage; you’ll authenticate with an API key.
Create a project in Google Cloud Console
Enable Maps JavaScript API
Enable Places API and Geocoding API
Create an API key with HTTP referrer restrictions
Go to “Keys & Credentials” from the sidebar to view your key
Store your key:
# Next.js
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your-google-maps-api-key
If you do not want to use Google Maps, there are other options available:
Create the project
We’ll focus on H3 for geofencing and PubNub Presence/State for the inZone flip.
npx create-next-app@latest
Install Dependencies
# Core runtime deps
npm install pubnub h3-js @turf/turf
# (Optional) Visualization only — if you want to draw the zone/cells on a map
npm install @googlemaps/js-api-loader @types/google.maps
Why these:
pubnub— real-time Presence/State forinZoneflips and lightweight events.h3-js— converts GPS positions to hex cell IDs and polyfills polygons to cell sets; constant-time eligibility checks.@turf/turf— union/simplify/buffer messy real-world polygons before hex-filling.@googlemaps/js-api-loader(optional) — loads the Maps JS API (display/debug only).
Accuracy note: device geolocation blends GPS/Cell/Wi-Fi signals; on mobile devices, Google Play services (Android) and Apple Core Location (iOS) manage sensor fusion.
Environment variables
Use public envs for browser code. In Next.js, public keys must start with NEXT_PUBLIC_.
# .env.local (Next.js)
NEXT_PUBLIC_PUBNUB_PUBLISH_KEY=your-publish-key
NEXT_PUBLIC_PUBNUB_SUBSCRIBE_KEY=your-subscribe-key
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your-google-maps-api-key # only if you render Maps
Visualizing your H3 fence on a map
Heads-up (example-only): The map code below is just to visualize your hex fence. It’s okay if it doesn’t fully click yet — we’ll revisit, and it explain what is going on later in the tutorial.
lib/renderH3.ts
// lib/renderH3.ts
import { cellToBoundary as h3CellToBoundary } from "h3-js";
/** Returns a ring of [lat, lng] pairs for a given H3 cell. */
export function cellToBoundary(cellId: string): [number, number][] {
// geoJson = true -> [lat,lng]
return h3CellToBoundary(cellId, true);
}
"use client";
import React, { useEffect, useRef } from "react";
import { setOptions, importLibrary } from "@googlemaps/js-api-loader";
import { cellToBoundary } from "@/lib/renderH3";
import type { Zone } from "@/lib/geoEligibility";
export default function MapComponent({ zone }: { zone: Zone }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
(async () => {
setOptions({
key: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!,
v: "weekly",
});
await importLibrary("maps");
if (!ref.current) return;
const map = new google.maps.Map(ref.current, {
center: { lat: 43.683, lng: -79.612 }, // Center based on OSM data coordinates
zoom: 14,
mapTypeId: google.maps.MapTypeId.ROADMAP,
gestureHandling: "greedy",
});
// Draw the allowed H3 cells as light overlays
zone.cells.slice(0, 800).forEach((cellId) => {
// (slice to avoid rendering thousands at once during dev)
const path = cellToBoundary(cellId).map(([lat, lng]) => ({ lat, lng }));
new google.maps.Polygon({
paths: path,
map,
strokeOpacity: 0.25,
fillOpacity: 0.08,
});
});
})();
}, [zone]);
return <div ref={ref} style={{ height: 400, width: "100%" }} />;
}
If the code is running properly, you should see a map zoomed in on the Toronto International Airport.
If you would like to integrate this into a different project, you can submit the following prompt into Cursor or Codex using the “auto” model feature, as well as copying the .env that was defined above with the appropriate keys:
“I saw this code in a blog post. I need it to run in a React or Next.js project. Start the project by using terminal commands and using the code defined above. Also, use the following .env file for any key needed.”
Geofenced driver eligibility with H3 and PubNub
Riders expect fast, fair pickups. That starts with considering only drivers inside the pickup zone (airport lot, stadium curb, venue circle). Your driver app should treat GPS location inside that fence as eligibility—not proximity to the rider’s pin across town. Letting outside drivers jump the line creates disputes.
The solution is to use a discrete global grid (H3). Eligibility becomes: “Is the current location currently inside one of our allowed hex cells?” This is fast, stable, and scales naturally.
What we’ll use (and why)
H3 (hex grid) —
h3-jspolygonToCells(offline) to convert venue polygon → allowed hex cellslatLngToCell(device) to convert GPS → current cell IDgridDisk(device/server) to add a small neighbor buffer (reduces edge jitter)Start with res 9–10 (~174m / ~66m edge). Tune after field tests.
Geo preprocessing —
@turf/turf
Clean real-world shapes (union/simplify/buffer) before hex-filling.Zone geometry (data) — OpenStreetMap via Overpass Turbo
Export venue polygons as GeoJSON (free & fast). Follow ODbL attribution.(Optional) Map display — Google Maps
Use Maps for visualization only. Keep H3 as the source of truth.Presence/State — PubNub Presence
Publish a tiny state{ inZone, h3, lastSeen, zoneVersion }tozone/<ID>only when the flag flips (saves battery; easy to scale) and trigger driver notifications for in-app UI updates.
Why hex cells over polygons?
Speed: point-in-cell is a hash lookup (no heavy point-in-polygon math).
Smooth edges: add a neighbor buffer (
gridDisk(k=1)) to tolerate GPS jitter.Sharding: cells are natural routing keys for channels, storage, dashboards, and backend rate-limits.
Where pickup zone polygons come from (OpenStreetMap Workflow)
You need a GeoJSON polygon for your pickup zone (airport staging lot, stadium curb, etc.). A simple, field-tested flow:
Open Overpass Turbo and zoom to the venue.
Use the Wizard to search by tags (e.g.,
amenity=parking) or a named area.Run a query and Export → GeoJSON.
Example — parking inside an airport area (Overpass QL):
[out:json][timeout:25];
{{geocodeArea:Toronto Pearson International Airport}}->.a; // airport area
(
way(area.a)["amenity"="parking"];
relation(area.a)["amenity"="parking"];
);
out geom; // include coordinates so you can export as GeoJSON
If you have multiple shapes, union them. If they’re very detailed, simplify a bit to avoid an enormous cell set (Turf helps).
Build once (offline): polygon → hex cells
We do this offline, so the app just downloads a versioned JSON of allowed cells at startup.
tools/build-zone.ts
// tools/build-zone.ts
// npm i h3-js @turf/turf
import { polygonToCells } from "h3-js";
import fs from "node:fs";
// 1) Load your venue polygon as GeoJSON (one polygon or multipolygon)
const fenceGeoJSON = JSON.parse(fs.readFileSync("data/yyz-lot.geojson", "utf8"));
// 2) Choose a resolution:
// res 9 ≈ 174 m edge (large venues)
// res 10 ≈ 66 m edge (curbs/rows)
const RES = 10;
// 3) Convert polygon -> set of hex cell IDs
const [poly] = fenceGeoJSON.features;
const cells = polygonToCells(poly.geometry.coordinates, RES, true);
// 4) Save a versioned fence you can fetch in the app
fs.mkdirSync("public/zones/YYZ", { recursive: true });
fs.writeFileSync(
"public/zones/YYZ/v1.json",
JSON.stringify({ id: "YYZ", version: 1, res: RES, cells }, null, 2)
);
console.log(`Saved ${cells.length} cells at res ${RES}`);
File layout
/data
yyz-lot.geojson # source polygon (from Overpass/GIS)
/tools
build-zone.ts # hex-fill script (polygon -> H3 cells)
/public/zones/YYZ
v1.json # { id, version, res, cells[] } served to clients
Keep fences versioned (
zones/<ZONE>/v1.json, thenv2.jsonas lots change). Your app can hot-swap without redeployments.
Client: GPS location to inZone with PubNub presence state
Load the zone JSON on each mobile device, convert the latest GPS location to an H3 cell, add a small hysteresis buffer (neighbors), and set driver Presence state on a single zone channel. Publish only when inZone changes. lib/geoEligibility.ts
// lib/geoEligibility.ts
import PubNub from "pubnub";
import { latLngToCell, gridDisk } from "h3-js";
export type Zone = { id: string; version: number; res: number; cells: string[] };
// Use only on the client; for SSR-safe usage, create inside components or guard for window.
export function createPubNub() {
return new PubNub({
publishKey: process.env.NEXT_PUBLIC_PUBNUB_PUBLISH_KEY!,
subscribeKey: process.env.NEXT_PUBLIC_PUBNUB_SUBSCRIBE_KEY!,
userId:
"driver-" +
(typeof crypto !== "undefined" && "randomUUID" in crypto
? crypto.randomUUID()
: Math.random().toString(36).slice(2)),
});
}
let zone: Zone;
let allowed = new Set<string>();
let lastInZone: boolean | null = null;
/** Call once on app start (client). */
export async function initEligibility(pubnub: PubNub, zoneId = "YYZ") {
zone = await fetch(`/zones/${zoneId}/v1.json`).then((r) => r.json());
allowed = new Set(zone.cells);
// Subscribe once; everyone in this zone watches eligibility/state here
pubnub.subscribe({ channels: [`zone/${zone.id}`], withPresence: true });
}
/** Returns true if the device is inside the zone (with a small buffer). */
export function isEligible(lat: number, lng: number) {
const cell = latLngToCell(lat, lng, zone.res);
// Buffer with neighbors (k=1) to avoid flapping at the edge
const neighbors = gridDisk(cell, 1);
return neighbors.some((c) => allowed.has(c));
}
/** Call this on significant location changes (not every second). */
export async function onLocation(pubNub: PubNub, lat: number, lng: number) {
const inZone = isEligible(lat, lng);
if (inZone !== lastInZone) {
lastInZone = inZone;
await pubNub.setState({
channels: [`zone/${zone.id}`],
state: {
inZone,
h3: latLngToCell(lat, lng, zone.res),
lastSeen: Date.now(),
zoneVersion: zone.version,
},
});
}
}
Server guard: server-side geofencing for production dispatch
For the tutorial, you can use client-side setState, but for production, verify eligibility server-side (so clients can’t spoof inZone). A tiny Node microservice works well for driver management rules you do not want enforced in the client.
server/eligibility.ts
// server/eligibility.ts
// npm i express pubnub h3-js
import express from "express";
import PubNub from "pubnub";
import { latLngToCell } from "h3-js";
import zones from "./zones-cache"; // load the same v1.json in memory
const app = express();
app.use(express.json());
const pubnub = new PubNub({
publishKey: process.env.PUBNUB_PUBLISH_KEY!,
subscribeKey: process.env.PUBNUB_SUBSCRIBE_KEY!,
secretKey: process.env.PUBNUB_SECRET_KEY!, // server-only
userId: "server-guard",
});
/**
* Drivers POST lat/lng here with an auth token you validate.
* We recompute cell -> eligibility and update Presence state.
*/
app.post("/eligibility/:zoneId/:driverId", async (req, res) => {
const { zoneId, driverId } = req.params;
const { lat, lng } = req.body as { lat: number; lng: number };
const zone = zones[zoneId]; // { res, allow: Set<string>, k1: Set<string>, version: number }
const cell = latLngToCell(lat, lng, zone.res);
const inZone = zone.allow.has(cell) || zone.k1.has(cell); // include buffered neighbors
await pubnub.setState({
uuid: driverId,
channels: [`zone/${zoneId}`],
state: { inZone, h3: cell, lastSeen: Date.now(), zoneVersion: zone.version },
});
return res.json({ inZone, cell });
});
app.listen(3001, () => console.log("Eligibility guard running on :3001"));
server/zones-cache.ts
// server/zones-cache.ts
import fs from "node:fs";
import path from "node:path";
import { gridDisk } from "h3-js";
type Fence = { id: string; version: number; res: number; cells: string[] };
const ZONE_DIR = path.join(process.cwd(), "public", "zones", "YYZ", "v1.json");
const fence: Fence = JSON.parse(fs.readFileSync(ZONE_DIR, "utf8"));
const allow = new Set(fence.cells);
// Precompute k=1 neighbors for a bit of hysteresis at the edge
const k1 = new Set<string>();
for (const cell of fence.cells) {
for (const n of gridDisk(cell, 1)) k1.add(n);
}
export default {
[fence.id]: {
res: fence.res,
version: fence.version,
allow,
k1,
},
} as Record<string, { res: number; version: number; allow: Set<string>; k1: Set<string> }>;
Why bother with a server?
Prevents tampering with
inZone/h3.Lets you roll out new fence versions centrally.
Keeps your secret key safely on the server (never ship it to clients).
Works great with Access Manager (restrict who can set state on
zone/*).Leaves room to add driver notifications, surge pricing, dynamic pricing, and automate fleet management rules later. Picking a resolution (quick guide):
Res 9 (~174 m edge): airport lots, big venues
Res 10 (~66 m edge): curbs, parking rows, stadium gates Choose the coarsest res that still feels fair at the curb — fewer cells → fewer updates and simpler ops. If you see “flapping” at edges, use the neighbor buffer (
k=1) before jumping to a finer resolution. Gotchas & GuardrailsGPS drift near edges: add the neighbor buffer and flip
inZoneonly after two consecutive confirmations.Bad actors: lock
setStatebehind Access Manager roles; prefer server-side updates.Version drift: include
zoneVersionin state; if the client lags behind the server, the server wins.Multi-venue cities: keep one channel per zone (
zone/SFO,zone/YYZ), not per city; shard further by cell only if you need hyper-local fan-out.Testing: try both outdoor GPS and Wi-Fi-heavy urban areas to validate behavior.
Fair FIFO dispatch queue for ride-hailing apps
Problem: Even with a geofence, “who gets the next trip?” can devolve into chaos. Drivers need a predictable first-in/first-out queue to trust the system—the same fairness expectation you see in production taxi dispatch software at airport lots and stadium curbs.
What we’ll build: When a driver becomes eligible (inZone: true), they join the queue with a joinTs. When a rider request arrives on your ride-hailing platform, the next driver is assigned and removed from the head of the line. If they don’t accept within a grace period, they’re bumped, and the next driver is notified. This helps optimize rider wait times and travel time while enabling clear in-app experiences.
We’ll wire a tiny Queue Visualizer into a Next.js page. It’s client-side here; in production, make a PubNub Function your queue authority.
lib/pubnub.ts — a safe, client-only singleton
// lib/pubnub.ts
import PubNub from "pubnub";
let _pubnub: PubNub | null = null;
export function getPubNub() {
if (_pubnub) return _pubnub;
if (typeof window === "undefined") {
throw new Error("getPubNub() must be called on the client");
}
_pubnub = new PubNub({
publishKey: process.env.NEXT_PUBLIC_PUBNUB_PUBLISH_KEY!,
subscribeKey: process.env.NEXT_PUBLIC_PUBNUB_SUBSCRIBE_KEY!,
userId:
"driver-" +
(typeof crypto !== "undefined" && "randomUUID" in crypto
? crypto.randomUUID()
: Math.random().toString(36).slice(2)),
});
return _pubNub;
}
lib/queue.ts — minimal client-side authority (demo only)
// lib/queue.ts
import type PubNub from "pubnub";
import { getPubNub } from "./pubnub";
const CHANNEL = "queue/SF";
export type QItem = { id: string; joinTs: number; inZone: boolean };
const queue = new Map<string, QItem>();
const subscribers = new Set<(items: QItem[]) => void>();
function notify() {
const snapshot = Array.from(queue.values())
.filter((q) => q.inZone)
.sort((a, b) => a.joinTs - b.joinTs);
subscribers.forEach((cb) => cb(snapshot));
}
export function subscribeToQueue(cb: (items: QItem[]) => void) {
subscribers.add(cb);
notify();
return () => subscribers.delete(cb);
}
let wired = false;
export function wireQueueListeners(pubnub?: PubNub) {
const pn = pubnub ?? getPubNub();
if (wired) return;
wired = true;
pn.subscribe({ channels: [CHANNEL], withPresence: true });
pn.addListener({
message: (e) => {
const m = e.message as any;
if (m.type === "join") queue.set(m.id, { id: m.id, joinTs: m.joinTs, inZone: true });
if (m.type === "leave") queue.delete(m.id);
if (m.type === "assign") {
// Highlight assigned driver m.to in your UI (left as an exercise)
queue.delete(m.to);
}
notify();
},
presence: (e) => {
const item = queue.get(e.uuid);
if (item) item.inZone = !!(e.state as any)?.inZone;
notify();
},
});
}
export async function joinQueue(pubnub?: PubNub) {
const pn = pubnub ?? getPubNub();
const id = pn.getUUID();
await pn.publish({ channel: CHANNEL, message: { type: "join", id, joinTs: Date.now() } });
}
export async function leaveQueue(pubnub?: PubNub) {
const pn = pubnub ?? getPubNub();
const id = pn.getUUID();
await pn.publish({ channel: CHANNEL, message: { type: "leave", id } });
}
export async function simulateRiderRequest(pubnub?: PubNub) {
const pn = pubnub ?? getPubNub();
const next = Array.from(queue.values())
.filter((q) => q.inZone)
.sort((a, b) => a.joinTs - b.joinTs)[0];
if (!next) return;
await pn.publish({ channel: CHANNEL, message: { type: "assign", to: next.id } });
queue.delete(next.id);
notify();
}
components/QueueVisualizer.tsx — the interactive UI
// components/QueueVisualizer.tsx
"use client";
import React, { useEffect, useState } from "react";
import {
wireQueueListeners,
subscribeToQueue,
joinQueue,
leaveQueue,
simulateRiderRequest,
type QItem,
} from "@/lib/queue";
import { getPubNub } from "@/lib/pubnub";
export default function QueueVisualizer() {
const [items, setItems] = useState<QItem[]>([]);
useEffect(() => {
const pn = getPubNub();
wireQueueListeners(pn);
const unsub = subscribeToQueue(setItems);
return () => unsub();
}, []);
return (
<div style={{ display: "grid", gap: 12, maxWidth: 420 }}>
<div style={{ display: "flex", gap: 8 }}>
<button onClick={() => joinQueue()}>Join (as current user)</button>
<button onClick={() => leaveQueue()}>Leave</button>
<button onClick={() => simulateRiderRequest()}>Simulate Rider Request</button>
</div>
<div style={{ border: "1px solid #333", borderRadius: 8, padding: 12 }}>
<strong>Live Queue (eligible only)</strong>
<ol>
{items.map((q) => (
<li key={q.id}>
<code>{q.id.slice(0, 8)}…</code> — joined {new Date(q.joinTs).toLocaleTimeString()}
</li>
))}
</ol>
{items.length === 0 && <em>No one in the queue yet.</em>}
</div>
<p style={{ fontSize: 12, opacity: 0.8 }}>
Demo-only: In production, move this logic into a PubNub Function (authoritative queue),
enforce a grace timeout, and guard channels with Access Manager.
</p>
</div>
);
}
app/queue/page.tsx — Next.js page (App Router)
// app/queue/page.tsx
"use client";
import React from "react";
import QueueVisualizer from "@/components/QueueVisualizer";
export default function QueuePage() {
return (
<main style={{ padding: 24 }}>
<h1>Queue Visualizer (Demo)</h1>
<p>
Use the buttons to join/leave the queue as the current user and simulate a rider request.
Open this page in two browser tabs to see it live.
</p>
<QueueVisualizer />
</main>
);
}
If you’re using the Pages Router, create
pages/queue-visualizer.tsxwith the same component and export a default page instead.
Production Notes
Make a PubNub Function the queue authority: on join/leave/request, update a stored list (sorted by
joinTs), publishassign, enforce a grace timer, and prevent clients from spoofingjoinTs.Guard channels with Access Manager (drivers vs. riders).
If a metro is busy, shard queues by geocode/geohash prefix (e.g.,
queue/SF/9q8).Persist queue state (Message Persistence or a KV in your Function) for resilience.
Add in-app notifications and push notifications for assignment pings to elevate user experience.
Open the queue page in two browser tabs to simulate two driver app clients before you ship native iOS/Android builds.
Real-time driver tracking on the rider map
Once Feature 1 is live, you can show only eligible drivers on the rider map:
pubnub.addListener({
presence: (e) => {
if (e.channel === "zone/SF" && (e.state as any)?.inZone) {
// addDriverMarker(e.uuid, (e.state as any).location)
} else {
// removeDriverMarker(e.uuid)
}
},
});
Consider filtering by current location proximity, traffic data, and routing ETA for better route optimization and real-time tracking.
Wrap-up: from tutorial to production dispatch system
You now have the core building blocks of a rideshare dispatch system:
Geo-fenced eligibility with H3 + PubNub Presence/State
A fair FIFO queue with a tiny interactive visualizer (move logic to PubNub Functions for production)
So, where do you go from here? You can implement advanced features to enhance your platform even further:
Add a grace window (e.g., 30s) before bumping an assigned driver
Integrate a nearest-driver prefilter (distance threshold) before FIFO to reduce travel time
Build a lightweight operator dashboard that watches queue length and rider wait times or utilize PubNub Illuminate
Add fleet management and driver management dashboards, surge pricing, and dynamic pricing by vehicle type
Wire payment processing once dispatch and eligibility are stable, then automate safety checks
Expand to iOS/Android mobile devices with in-app UX polish and push notifications
To help implement these features, explore how PubNub supports rideshare and dispatch systems by browsing our resources, and start prototyping by utilizing our in-depth documentation.
Once you are ready, talk to our team to review architecture, ROI, and your security requirements.
Rideshare dispatch and geofencing FAQs
Q: Does this require Google Maps to work?
A: No. Maps are optional for visualization only. The geofencing eligibility works with H3 + PubNub using raw location data from the device (geolocation), whether on iOS or Android (Google Play services / play services).
Q: Will Wi-Fi affect accuracy?
A: Yes. In dense urban areas, Wi-Fi improves indoor fixes. Validate fences outdoors and indoors for the best user experience.
Q: How do I control costs/pricing for Google Maps?
A: Use an API key with referrer restrictions and monitor pricing in Google Cloud. For this tutorial, Maps are optional.
Q: Can I extend this to driver ETA and smarter assignment algorithms?
A: Definitely—feed routing, route optimization, and traffic data into your assignment algorithms to improve fairness, customer experience, and customer satisfaction.
Q: What about backend frameworks and dependencies?
A: Any Node/Express stack works. Keep your backend minimal, restrict secrets to the server, and version your fences. Add only the dependencies you need.
Q: Does this approach fit taxi companies and service providers?
A: Yes. It’s suitable for taxi companies, aggregators, and service providers modernizing legacy taxi dispatch software, and for startups shipping an MVP on a new ride-hailing platform.