---
title: Track SDK
description: Vehicle and asset GPS tracking with geofencing and location history. Real-time tracking for fleets, delivery drivers, drones, and any moving asset.
---

# @nolag/track

Vehicle and asset GPS tracking with geofencing and location history.

## Overview

Track vehicles, delivery drivers, drones, or any moving asset in real time. `@nolag/track` broadcasts location updates to zone subscribers instantly. Geofence detection runs client-side using the haversine formula for circular boundaries and a ray-casting algorithm for polygon boundaries, so triggers fire without a server round-trip. Zones group assets by geographic area or fleet. Join multiple zones to observe overlapping regions. Your app owns one core NoLag client and injects it into `NoLagTrack`; the wrapper attaches its behaviour to that connection.

### Key Features

- Real-time GPS location broadcast to zone subscribers
- In-memory location buffer per asset for client-side history
- Client-side geofence detection for circle (haversine) and polygon (ray-casting)
- Geofences narrow the server-side location filter to the grid cells they cover
- Asset online/offline presence via lobby
- Optional metadata attached to each location point
- Automatic reconnect with zone and presence restoration

## How It Works

`NoLagTrack` attaches to an injected `@nolag/js-sdk` client and manages a lobby that tracks which assets are online. Calling `joinZone(name)` returns a `TrackingZone` that subscribes to two topics: `locations` for GPS points and `_geofence` for geofence events published by your own services. Neither topic is logged server-side, which suits high-frequency GPS data. Location history is kept in memory on the client. Geofence evaluation happens locally on each `locationUpdate`, for your own points and for remote ones.

Geofences also shape what a zone receives. Every location is published with a filter naming its geo-grid cell (0.01 degree squares). When you add or remove a geofence, the zone recomputes the cells its geofences cover and calls `setFilters` on the `locations` subscription, so a zone with geofences only receives location updates from those cells. With no geofences, and no filters of your own, the subscription is a wildcard and receives every update in the zone. The app owns the socket lifecycle; the wrapper never opens or closes it.

| Topic | Purpose |
| --- | --- |
| `locations` | GPS location points with optional metadata, tagged with their geo-grid cell |
| `_geofence` | Inbound `GeofenceEvent` messages from your own services. The SDK only listens here and re-emits them as `geofenceTriggered`; it never publishes to this topic |

A fresh subscribe or an ordinary reconnect never replays history, and this SDK never triggers replay; see [Replay](/docs/concepts/replay) for the one case where the broker replays messages.

**Before you start.** Create an app from the `nolag-track-sdk` blueprint. That seeds the rooms this SDK expects (`fleet-zone`) and the `online` lobby, and returns an app slug with a random suffix. That slug is the `appName` you pass to the wrapper. You can also do this in the portal: Apps, New App, pick the blueprint.

```typescript [Setup (server side, once)]
import { NoLagApi } from "@nolag/js-sdk";

const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...
const app = await api.apps.create({ name: "My App", blueprintId: "nolag-track-sdk" });
console.log(app.slug); // e.g. "my-app-a3f9": this is your appName

const actor = await api.actors.create({ name: "web-client", actorType: "user" });
console.log(actor.accessToken); // shown once; keep it
```

## Installation

```bash [Terminal]
npm install @nolag/track @nolag/js-sdk
```

**Shared connection.** One core NoLag client can back several wrapper SDKs at once, for example asset tracking, a dashboard, and notify on a single socket, as long as each wrapper uses a distinct `appName`. Each wrapper attaches its handlers on construction and releases them with `detach()`, and never touches the socket itself. Your app owns `connect()` and `disconnect()`.

## Quick Start

```typescript [TypeScript]
import { NoLag } from '@nolag/js-sdk'
import { NoLagTrack } from '@nolag/track'

// The app owns one core client. In a browser, pass a token provider so the
// SDK can mint fresh short-lived client tokens from your backend.
const client = NoLag(async () => (await (await fetch('/api/nolag-token')).json()).token)

// Inject the client into the track wrapper
const tracker = new NoLagTrack({
  client,
  appName: APP_SLUG, // the slug returned when you created the app
  assetId: 'drv_001',
  assetName: 'Van 12',
})

await client.connect()   // the app owns the connection
await tracker.ready()    // wrapper setup complete

// Join the seeded tracking zone (groups assets by geographic area or fleet)
const zone = tracker.joinZone('fleet-zone')

// Device side: report a location. Publishers never receive their own messages,
// so the wrapper stores the point locally and emits `locationUpdate` itself.
zone.sendLocation({ lat: 51.5074, lng: -0.1278, speed: 11.6 }, { driverId: 'drv_001' })

// Add a circular geofence (haversine distance check)
zone.addGeofence({
  id: 'depot-central',
  shape: 'circle',
  center: { lat: 51.5074, lng: -0.1278 },
  radiusMeters: 500,
  name: 'Central Depot',
})

// Add a polygon geofence (ray-casting algorithm)
zone.addGeofence({
  id: 'zone-east',
  shape: 'polygon',
  points: [
    { lat: 51.52, lng: -0.05 },
    { lat: 51.50, lng: -0.03 },
    { lat: 51.48, lng: -0.06 },
    { lat: 51.50, lng: -0.09 },
  ],
  name: 'East Zone',
})
// From here on this zone only receives location updates from the grid cells
// the two geofences cover. Remove every geofence to go back to receiving all.

// Controller side: listen for updates
zone.on('locationUpdate', ({ assetId, point, metadata, timestamp }) => {
  console.log(`Asset ${assetId} at ${point.lat}, ${point.lng}`, metadata, timestamp)
})

zone.on('geofenceTriggered', ({ assetId, geofenceId, type, point }) => {
  console.log(`Asset ${assetId} ${type} geofence ${geofenceId} at ${point.lat}, ${point.lng}`)
})

// Read the in-memory location history for one asset (synchronous)
const history = zone.getLocationHistory('drv_001')
console.log(`${history.length} location points in buffer`)

// Teardown: the wrapper releases its handlers; the app closes the socket.
tracker.detach()
client.disconnect()
```

## API Reference

### NoLagTrack

#### Constructor Options

| Option | Type | Description |
| --- | --- | --- |
| `client` | `NoLagSocket` | **Required.** The injected core NoLag client the app owns and connects. |
| `appName` | `string` | The app slug used as the topic prefix. Pass the suffixed slug returned when you created the app; the default `'track'` will not match a hosted app. |
| `assetId` | `string` | Stable identifier for this asset (auto-generated if omitted). |
| `assetName` | `string` | Optional human-readable name for this asset. |
| `metadata` | `Record<string, unknown>` | Optional custom data attached to asset presence. |
| `zoneNames` | `string[]` | Tracking zones to auto-join once the wrapper is ready. |
| `zones` | `Geofence[]` | Client-side geofences to register on every joined zone. |
| `maxLocationHistory` | `number` | Max location history entries per asset (default `500`). |
| `debug` | `boolean` | Enable wrapper debug logging (default `false`). |

| Method | Returns | Description |
| --- | --- | --- |
| `ready()` | `Promise<void>` | Resolves once wrapper setup completed. Join methods throw before this resolves. |
| `detach()` | `void` | Release this wrapper's handlers and topics; terminal, never closes the socket. |
| `joinZone(name, opts?)` | `TrackingZone` | Subscribe to a tracking zone and return it. Synchronous; returns the existing instance if already joined. `opts.filters` adds location filter values of your own, unioned with the geofence cells. |
| `leaveZone(name)` | `void` | Unsubscribe from a zone and release its resources. |
| `getZones()` | `TrackingZone[]` | All currently joined zones. |
| `getOnlineAssets()` | `TrackedAsset[]` | Assets currently present in the `online` lobby. |

### NoLagTrack Events

| Event | Payload | Description |
| --- | --- | --- |
| `connected` | none | Wrapper setup completed on a live connection. |
| `disconnected` | `reason: string` | Connection closed. |
| `reconnecting` | none | The core client is attempting to reconnect. |
| `reconnected` | none | Connection restored after a drop; zone membership and presence are restored automatically. |
| `error` | `error: Error` | A transport or protocol error occurred. |
| `assetOnline` | `asset: TrackedAsset` | An asset joined the lobby. |
| `assetOffline` | `asset: TrackedAsset` | An asset left the lobby. |

### TrackingZone

| Method | Returns | Description |
| --- | --- | --- |
| `sendLocation(point, metadata?, opts?)` | `LocationUpdate` | Broadcast a `GeoPoint` with optional metadata and return the update that was sent. Tagged with its grid cell unless `opts.filter` replaces it, which opts the update out of geofence routing. |
| `getLocationHistory(assetId?)` | `LocationUpdate[]` | Read location points from the in-memory buffer (up to `maxLocationHistory` per asset). Omit `assetId` to get all assets. |
| `addGeofence(geofence)` | `void` | Register a circle or polygon geofence, evaluated on every location update. Recomputes the grid-cell filter on `locations`. |
| `removeGeofence(id)` | `void` | Deregister a geofence by its ID and recompute the filter. |
| `getGeofences()` | `Geofence[]` | All geofences registered on this zone. |
| `getAssets()` | `TrackedAsset[]` | Remote assets currently present in this zone. |
| `getAsset(assetId)` | `TrackedAsset \| undefined` | One remote asset by ID. |
| `setFilters(values)` | `void` | Replace your own location filter values. They are unioned with the geofence cells, never replace them; an empty array leaves only the geofence cells. |
| `addFilters(values)` / `removeFilters(values)` | `void` | Add or remove your own filter values without touching the geofence cells. |

### TrackingZone Events

| Event | Payload | Description |
| --- | --- | --- |
| `locationUpdate` | `LocationUpdate` | A location point was received from another asset, or sent by this one. |
| `assetJoined` | `asset: TrackedAsset` | An asset joined this zone. |
| `assetLeft` | `asset: TrackedAsset` | An asset left this zone. |
| `geofenceTriggered` | `GeofenceEvent` | An asset crossed a geofence boundary. Evaluated client-side on each location update, or received on `_geofence`. |

### Types

| Type | Shape |
| --- | --- |
| `GeoPoint` | `{ lat, lng, altitude?, accuracy?, heading?, speed? }` (degrees, metres, metres per second) |
| `LocationUpdate` | `{ id, assetId, point: GeoPoint, metadata?, timestamp, isReplay }` |
| `Geofence` | `{ id, shape: 'circle', center: GeoPoint, radiusMeters, name?, metadata? }` or `{ id, shape: 'polygon', points: GeoPoint[], name?, metadata? }` |
| `GeofenceEvent` | `{ geofenceId, assetId, type: 'enter' \| 'exit', point: GeoPoint, timestamp }` |
| `TrackedAsset` | `{ assetId, actorTokenId, assetName?, lastLocation?, metadata?, joinedAt, isLocal }` |
