latlng

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-server installed 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.

Terminal
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-*.

Request
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"}.

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

Request — position report with a 2-minute heartbeat
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.

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

Request
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"] } ]
    }
  }'
Response (abridged)
{
  "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?".

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

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

npm install @latlng/sdk
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-hamburg collection 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.