Build a real-time vehicle tracker
In this tutorial you'll create a fleet, register vehicles with status fields, stream position updates, and answer the question every dispatcher asks: which available vehicle is closest?
Beginner · about 15 minutes · tested against latlng v0.2.0
Before you start
-
latlng-serverinstalled via Homebrew, Docker, or a release binary. -
curl, or Node.js 18+ for the TypeScript version.
01 Start the server
Install latlng with Homebrew, Docker, or a release binary (see Getting started), then start the server with an append-only file so your data survives restarts. The HTTP API listens on port 7421 by default.
latlng-server --aof ./data/appendonly.aof
# in a second terminal
curl -s http://127.0.0.1:7421/ping
# {"ok":true,"pong":true} Authentication is off by default so you can follow along locally. The last tutorial in this series shows how to require scoped JWTs before you deploy.
02 Create a fleet
Objects live in collections. A good default is one collection per fleet, region, or customer — for example fleet-hamburg and fleet-berlin. Queries and geofences run against a single collection, and access tokens can later be scoped to a name pattern like fleet-*.
curl -s -X POST http://127.0.0.1:7421/collections/fleet-hamburg
# {"created":true,"ok":true} 03 Register vehicles with status fields
Each vehicle is an object with an ID, a geometry, and optional fields. Fields are numbers or text and can be filtered on in every query — use them for status, speed, vehicle type, or capacity.
Field values are typed: {"type":"number","value":42} or {"type":"text","value":"available"}.
curl -s -X POST http://127.0.0.1:7421/collections/fleet-hamburg/objects/truck-1 \
-H 'content-type: application/json' \
-d '{
"object": { "Point": { "lat": 53.5511, "lon": 9.9937, "z": null } },
"fields": [
{ "name": "status", "value": { "type": "text", "value": "available" } },
{ "name": "speed", "value": { "type": "number", "value": 42 } },
{ "name": "driver", "value": { "type": "text", "value": "anna" } }
]
}'
# {"ok":true,"stored":true} Add two more vehicles the same way — truck-2 at 53.5601, 10.0012 with status loading, and van-7 at 53.5450, 9.9700 with status available and speed 55 — so the queries below have something to find.
04 Send position updates
Every position report is the same call: a POST to the vehicle's object URL. latlng updates the spatial index in place, so this is cheap to do every few seconds per vehicle.
A SET replaces the geometry and merges the fields: fields in the request are added or overwritten, and fields you leave out are kept — so a position report only needs to carry what changed. The TTL is different: it is replaced on every SET, so send expire_seconds with each report. Vehicles that stop reporting then expire on their own instead of lingering at their last known position.
curl -s -X POST http://127.0.0.1:7421/collections/fleet-hamburg/objects/truck-1 \
-H 'content-type: application/json' \
-d '{
"object": { "Point": { "lat": 53.5530, "lon": 9.9890, "z": null } },
"fields": [ { "name": "speed", "value": { "type": "number", "value": 38 } } ],
"expire_seconds": 120
}'
curl -s "http://127.0.0.1:7421/collections/fleet-hamburg/objects/truck-1?with_fields=true"
# status and driver are still there; speed is now 38
curl -s http://127.0.0.1:7421/collections/fleet-hamburg/objects/truck-1/ttl
# {"ttl":118} Setting a numeric field to 0 deletes it, and a missing field reads as 0 in filters. A parked vehicle reporting speed: 0 therefore simply has no speed field, and a range filter on speed with a minimum of 1 finds only moving vehicles.
05 Change status without moving the vehicle
When a dispatcher assigns a job, only the status changes. Update fields on their own with the fields endpoint — it keeps the position and TTL untouched.
curl -s -X POST http://127.0.0.1:7421/collections/fleet-hamburg/objects/truck-2/fields \
-H 'content-type: application/json' \
-d '{"fields":[{"name":"status","value":{"type":"text","value":"available"}}]}'
# {"ok":true,"updated":true}
curl -s http://127.0.0.1:7421/collections/fleet-hamburg/objects/truck-2/fields/status
# {"type":"text","value":"available"} 06 Find the nearest available vehicles
nearby returns objects within a radius, sorted by distance, with distance_meters on each result. Combine it with filters: where_in_filters matches text values, where_filters matches numeric ranges, and limit caps the result count.
curl -s -X POST http://127.0.0.1:7421/collections/fleet-hamburg/search/nearby \
-H 'content-type: application/json' \
-d '{
"lat": 53.5511, "lon": 9.9937, "meters": 3000,
"options": {
"limit": 5,
"where_in_filters": [ { "field": "status", "values": ["available"] } ]
}
}' {
"count": 3,
"cursor": 0,
"results": [
{ "id": "truck-1", "distance_meters": 375.5, "fields": { ... }, "object": { "Point": { ... } } },
{ "id": "truck-2", "distance_meters": 1116.7, "fields": { ... }, "object": { "Point": { ... } } },
{ "id": "van-7", "distance_meters": 1706.4, "fields": { ... }, "object": { "Point": { ... } } }
]
} For more complex conditions, use an expression filter such as status == available && speed > 50, and ask for "output": "Ids" or "Count" when you don't need the full objects.
07 Query vehicles by area
within returns objects fully inside an area. Areas can be circles, bounding boxes, GeoJSON polygons, map tiles, or sectors. This is how you answer "which vehicles are in the port area right now?".
curl -s -X POST http://127.0.0.1:7421/collections/fleet-hamburg/search/within \
-H 'content-type: application/json' \
-d '{
"area": { "GeoJson": { "type": "Polygon", "coordinates": [[
[9.98, 53.54], [10.01, 53.54], [10.01, 53.565], [9.98, 53.565], [9.98, 53.54]
]] } },
"options": { "output": "Ids" }
}' To page through a whole fleet, use scan with an optional ID pattern. scan takes the options object directly as the request body — {"match_pattern":"truck-*","output":"Count"} — and, like every endpoint, rejects unknown keys with 400, so a typo can't silently widen a query.
08 Inspect and clean up
Fetch a single vehicle with its fields, check collection statistics and bounds, and remove vehicles that are decommissioned.
curl -s "http://127.0.0.1:7421/collections/fleet-hamburg/objects/truck-1?with_fields=true"
curl -s http://127.0.0.1:7421/collections/fleet-hamburg/stats
curl -s http://127.0.0.1:7421/collections/fleet-hamburg/bounds
curl -s -X DELETE http://127.0.0.1:7421/collections/fleet-hamburg/objects/van-7
curl -s http://127.0.0.1:7421/collections/fleet-hamburg/objects/van-7
# {"error":"not found: object not found: fleet-hamburg/van-7"} (HTTP 404) 09 The same flow in TypeScript
The @latlng/sdk package wraps the HTTP API with typed helpers for points, fields, and search options. It runs in Node.js and other runtimes with fetch.
import { LatLngClient, numberField, textField } from "@latlng/sdk";
const client = new LatLngClient({ leaderUrl: "http://127.0.0.1:7421" });
await client.createCollection("fleet-hamburg");
// Position report with fields and a 2-minute heartbeat TTL
await client.setPoint(
"fleet-hamburg",
"truck-1",
{ lat: 53.5511, lon: 9.9937 },
{ fields: { status: textField("available"), speed: numberField(42) }, expireSeconds: 120 },
);
// Status change without touching the position
await client.setFields("fleet-hamburg", "truck-1", { status: textField("on-job") });
// Nearest available vehicles within 3 km
const { results } = await client.nearby("fleet-hamburg", {
lat: 53.5511,
lon: 9.9937,
meters: 3000,
options: { limit: 5, whereInFilters: [{ field: "status", values: ["available"] }] },
});
console.log(results.map((r) => `${r.id}: ${Math.round(r.distance_meters ?? 0)} m`)); What you built
- A
fleet-hamburgcollection with vehicles that carry status and speed fields. - Position updates that expire automatically when a vehicle stops reporting.
- Status changes that don't touch the position.
- Nearest-vehicle and area queries with field filters.
Register a polygon geofence, receive enter and exit events over HTTP, and handle retries and duplicate deliveries.
Use case Fleet management and vehicle trackingOrganize vehicles into fleets, track live positions and status, find the nearest available vehicle, and get alerts when vehicles enter or leave depots.
Use case Ride-hailing and on-demand dispatchMatch riders with the nearest available driver, alert when a driver is close to the pickup, and manage airport and event pickup zones.