Transport & Logistics

How to Build a Rideshare Dispatch System with Google Maps Geofencing

0 MIN READ • Markus Kohler on Oct 1, 2025

Do you want to build a real rideshare dispatch system for an on-demand ride-hailing / ride-sharing mobile 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 resources I referenced while building this. 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 aiming to streamline app development and optimize user experience, customer satisfaction, and rider/driver retention.

What You’ll Build

  1. Feature 1 — Geofenced Eligibility (H3 + PubNub Presence/State)
    Devices convert GPS location data/device location to H3 hex cells and flip a lightweight inZone flag. Eligibility becomes a constant-time lookup: currentCell ∈ allowedCells. These virtual boundaries (aka geofencing) are fast and reliable for location tracking.

  2. Feature 2 — Fair FIFO Dispatch Queue
    When inZone becomes true, a driver joins a queue with a joinTs. 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.

We’ll ship one tiny, user-friendly interactive component: a Queue Visualizer you can drop into a Next.js page to simulate join/leave/assign behavior.

These 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. They all use this functionality at their core for real-time tracking, notifications (for in-app driver pings), and fair dispatch.

Prerequisites

  • 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.

MVP tip: You can launch a minimal, automate-friendly queue + eligibility MVP first, then layer advanced features like driver in-app notifications, surge pricing, and fleet management dashboards.

Set up your PubNub Account

  1. Go to admin.pubnub.com/signup and create a free account.

  2. If you already have one, sign in to your PubNub Dashboard.

  3. Click Apps in the left-hand sidebar → Create New App → name it (e.g., “Rideshare Dispatch”).

  4. Create a Keyset. Create a PubNub application

Enable PubNub Features

To ensure your keyset is configured correctly: Enable:

Leave disabled for now:

Important: Don’t deploy to production without Access Manager enabled to protect user data and restrict who can publish/subscribe.

Click Save on all changes. Configure PubNub Keyset

Google Maps API (Optional — visualization only)

Google Maps provides interactive maps, geocoding, routes, and places (points of interest). You do not need Maps to compute eligibility. H3 + PubNub handles all the logic; Maps are for debugging, demos, and operator views.

  • Typical Google Maps API use case here: visualize fences, inspect traffic data, and preview route optimization impacts on driver travel time.

  • Keep an eye on pricing if you exceed free tier usage; you’ll authenticate with an API key.

Setup steps

  1. Create a project in Google Cloud Console

  2. Enable Maps JavaScript API

  3. Enable Places API and Geocoding API
    Configure Google Maps JavaScriptSDK

  4. Create an API key with HTTP referrer restrictions

  5. Go to “Keys & Credentials” from the sidebar to view your key Grab API key from Google Credentials

  6. Store your key:

# Next.js
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your-google-maps-api-key

Other map options (optional):

  • Mapbox: highly customizable styling (great for branding)

  • HERE: strong routing/logistics in EU/Asia

Create the Project

npx create-next-app@latest

Install Dependencies

We’ll focus on H3 for geofencing and PubNub Presence/State for the inZone flip.

# 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 for inZone flips 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

Optional: 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.

I was able to send in a prompt into Cursor 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.” Loading Google Maps

Feature 1 — Geo-fenced Driver Eligibility

Problem: Riders expect fast, fair pickups. That starts with considering only drivers inside the pickup zone (airport lot, stadium curb, venue circle). Letting outside drivers jump the line creates disputes.

Solution: 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. Hexigonal map layout

What we’ll use (and why)

  • H3 (hex grid)h3-js

    • polygonToCells (offline) to convert venue polygon → allowed hex cells

    • latLngToCell (device) to convert GPS → current cell ID

    • gridDisk (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 } to zone/<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 the zone polygon comes from (repeatable workflow)

You need a GeoJSON polygon for your pickup zone (airport staging lot, stadium curb, etc.). A simple, field-tested flow:

  1. Open Overpass Turbo and zoom to the venue.

  2. Use the Wizard to search by tags (e.g., amenity=parking) or a named area.

  3. 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). Overpass Turbo Query

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, then v2.json as lots change). Your app can hot-swap without redeployments. Google Maps Zoning

Client: compute current cell → set ‘inZone’ via PubNub

Load the zone JSON, compute the device’s current H3 cell from lat/lng, 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: make the server the source of truth

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. 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, 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 & Guardrails

  • GPS drift near edges: add the neighbor buffer and flip inZone only after two consecutive confirmations.

  • Bad actors: lock setState behind Access Manager roles; prefer server-side updates.

  • Version drift: include zoneVersion in 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.

Feature 2 — Fair FIFO Dispatch Queue

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.

What we’ll build: When a driver becomes eligible (inZone: true), they join the queue with a joinTs. When a rider request arrives, 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.tsx with 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), publish assign, enforce a grace timer, and prevent clients from spoofing joinTs.

  • 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 for assignment pings to elevate user experience.

(Optional) Showing Nearby Eligible Drivers on the 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 and Next Steps

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)

Where to go from here

  • 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 & analytics, surge pricing, and automate safety checks

  • Expand to iOS/Android mobile devices with in-app UX polish and push notifications

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 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 dispatch, and for startups shipping an MVP.

Resources