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