MAP
60 minutes

Tutorial: build a ride-share dispatch backend

We'll build the backend behind something like Pathao's ride hailing: drivers send live positions, riders request pickups, the system picks the nearest driver by real-road ETA, draws a route, and pings the rider when the driver enters a geofence near the pickup. Node.js + Express, ~150 lines.

Prerequisites

  • Node 20+, an API key from /signup, and a webhook URL exposed (use ngrok or your VPS).
  • ~20 lines of frontend not covered here — see the /dispatch demo for the rider-facing piece.

Step 1 — Scaffold the project

mkdir dispatch && cd dispatch
npm init -y
npm install express @map-bd/sdk
echo 'export MAP_API_KEY=map_live_...' >> .env
echo 'export WEBHOOK_SECRET=whsec_...' >> .env
source .env

Step 2 — In-memory driver registry

For the tutorial we store driver positions in a plain Map. Production: Redis with TTL.

// drivers.js
const drivers = new Map();   // driverId → { lat, lon, status, updatedAt }

export function updateDriver(id, lat, lon, status = 'idle') {
  drivers.set(id, { lat, lon, status, updatedAt: Date.now() });
}

export function listIdleDrivers(maxAgeSec = 60) {
  const cutoff = Date.now() - maxAgeSec * 1000;
  return [...drivers.entries()]
    .filter(([_, d]) => d.status === 'idle' && d.updatedAt >= cutoff)
    .map(([id, d]) => ({ id, ...d }));
}

Step 3 — Match a pickup to a driver

The core of dispatch: call /v1/distance-matrix with all idle drivers as sources and the pickup as the single target. The driver with the lowest time wins.

// match.js
import { MapClient } from '@map-bd/sdk';
const map = new MapClient({ apiKey: process.env.MAP_API_KEY });

export async function pickDriver(pickup, candidates) {
  if (candidates.length === 0) return null;
  const matrix = await map.distanceMatrix({
    sources: candidates.map(d => ({ lat: d.lat, lon: d.lon })),
    targets: [{ lat: pickup.lat, lon: pickup.lon }],
  });
  const ranked = matrix.sources_to_targets
    .map((row, i) => ({ driver: candidates[i], etaSec: row[0]?.time ?? Infinity }))
    .filter(r => isFinite(r.etaSec))
    .sort((a, b) => a.etaSec - b.etaSec);
  return ranked[0];   // { driver, etaSec }
}

Step 4 — Fetch the driver's route to pickup

// route.js
import { MapClient, decodePolyline6 } from '@map-bd/sdk';
const map = new MapClient({ apiKey: process.env.MAP_API_KEY });

export async function routeTo(from, to) {
  const trip = await map.directions({
    locations: [{ lat: from.lat, lon: from.lon }, { lat: to.lat, lon: to.lon }],
  });
  const leg = trip.trip.legs[0];
  return {
    distanceKm: leg.summary.length,
    etaSec: leg.summary.time,
    polyline: leg.shape,                    // pass to frontend MapLibre as-is
    coords: decodePolyline6(leg.shape),     // [{lat, lon}, ...]
    maneuvers: leg.maneuvers,
  };
}

Step 5 — REST endpoints

// server.js
import express from 'express';
import { updateDriver, listIdleDrivers } from './drivers.js';
import { pickDriver } from './match.js';
import { routeTo } from './route.js';

const app = express();
app.use(express.json());

// 1) Driver app POSTs its position every few seconds
app.post('/drivers/:id/position', async (req, res) => {
  const { lat, lon, status } = req.body;
  updateDriver(req.params.id, lat, lon, status);
  // Also forward to MAP events so subscribed webhooks fire on geofence events.
  await fetch('https://api.map.bd/v1/events/track', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.MAP_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ driver_id: req.params.id, lat, lon }),
  });
  res.json({ ok: true });
});

// 2) Rider requests a pickup
app.post('/rides/request', async (req, res) => {
  const { pickup } = req.body;
  const candidates = listIdleDrivers();
  const winner = await pickDriver(pickup, candidates);
  if (!winner) return res.status(503).json({ error: 'no drivers available' });
  const route = await routeTo(winner.driver, pickup);
  res.json({
    rideId: crypto.randomUUID(),
    driverId: winner.driver.id,
    etaSec: winner.etaSec,
    route,
  });
});

app.listen(3000, () => console.log('dispatch listening on :3000'));

Step 6 — Geofence webhook (rider arrival notification)

Subscribe a webhook at /dashboard/webhooks pointing to your /webhooks/map with event geofence.enter centred on the pickup point with a 100m radius. Then handle it:

import crypto from 'crypto';

app.post('/webhooks/map', express.raw({ type: 'application/json' }), (req, res) => {
  // Verify HMAC signature
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');
  const sig = req.header('x-map-signature');
  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).end();
  }
  const ev = JSON.parse(req.body);
  if (ev.event === 'geofence.enter') {
    // Driver has entered the pickup geofence — notify rider
    notifyRider(ev.driver_id, 'Your driver has arrived');
  }
  res.json({ ok: true });
});

Step 7 — Run + observe

node server.js
# Simulate a driver
curl -X POST http://localhost:3000/drivers/d-42/position \
  -H 'Content-Type: application/json' \
  -d '{"lat":23.7925,"lon":90.4078,"status":"idle"}'

# Simulate a rider
curl -X POST http://localhost:3000/rides/request \
  -H 'Content-Type: application/json' \
  -d '{"pickup":{"lat":23.7806,"lon":90.4193}}'

You should see the ranked driver, ETA, and decoded polyline. Open /dashboard/usage to watch the calls show up.

What you just built

  • A REST backend that takes driver positions and rider requests
  • Real-road ETA-based dispatch (not haversine)
  • Turn-by-turn routes returned to the rider, ready to render in MapLibre
  • Geofence-triggered arrival notifications via signed webhooks

The pieces that production needs but this tutorial skipped:

  • Redis / DB for driver state (not in-memory)
  • Driver state machine: idle → assigned → en-route → on-trip → completed
  • Surge zones (we have /fleet showing zone-count detection)
  • WebSocket push to the rider app instead of polling
  • Driver app + rider app — not in scope here