Skip to main content

GPS Tracking Overview

GPS tracking receives position data from an Apollon forwarding integration, stores it in TimescaleDB, and broadcasts live updates via WebSocket.

Data Flow

Apollon device → POST /v1/gps (with API key) → TimescaleDB → WebSocket "gps" channel
  1. Apollon POSTs a GPS payload to /v1/gps with an API key in the Authorization header.
  2. The API resolves a device from the request (header, API key, or NULL).
  3. Auto trip_id resolution: if a device was resolved, the API queries for an active trip (status = 'active' and gps_logging_enabled = true) linked to that device and automatically sets trip_id on the GPS point.
  4. The point is inserted into TimescaleDB.
  5. Redis caching: the API updates three caches -- per-device (gps:latest:{device_id}), global (gps:latest:global), and appends to the trip GPS cache if trip_id is set. Per-device and global caches use a 5-minute TTL; the trip GPS cache uses a 2-minute TTL.
  6. WebSocket broadcast: a GpsUpdate message is sent to all clients subscribed to the "gps" channel.
  7. Geofence check: all active geofences are evaluated for proximity (see Geofences: Proximity Check).

GPS Data Fields

FieldTypeNullableDescription
idUUIDnoPoint ID
device_idstringyesResolved device (see Device resolution)
trip_idUUIDyesActive trip at time of insert
latitudefloatnoDegrees (WGS-84)
longitudefloatnoDegrees (WGS-84)
altitudefloatyesMeters
headingfloatyesDegrees 0–360
speed_kmhfloatnokm/h
speed_mpsfloatnom/s (raw from Apollon)
speed_mphfloatnomph
speed_knotsfloatnoNautical knots
pdopfloatyesPosition dilution of precision
hdopfloatyesHorizontal dilution of precision
vdopfloatyesVertical dilution of precision
timestamptimestamptznoISO 8601 timestamp
created_attimestampnoServer insert time

REST Endpoints

MethodPathPermissionDescription
POST/v1/gpsgps:writeIngest GPS point
GET/v1/gpsgps:readList GPS data (paginated)
GET/v1/gps/currentgps:readLatest GPS point (optionally per device)
GET/v1/gps/current/devicesgps:readLatest GPS point per device
GET/v1/gps/{id}gps:readGet single point
DELETE/v1/gps/{id}gps:deleteDelete point (blocked if linked to any trip; 204 on success)

Ingest Payload

POST /v1/gps
Authorization: Bearer <api-key>
X-Device-ID: <device-uuid> // optional

{
"latitude": 49.4875,
"longitude": 8.4660,
"altitude": 102.3,
"heading": 270.0,
"timestamp": 1750000000,
"speed_kmh": 72.4,
"speed_mps": 20.1,
"speed_mph": 45.0,
"speed_knots": 39.1,
"pdop": 1.2,
"hdop": 0.9,
"vdop": 0.8
}

timestamp is a Unix epoch in seconds (parsed via DateTime::from_timestamp(secs, 0)); out-of-range values fall back to the server time. Speed fields are required; altitude, heading, and the *dop fields are optional.

Querying Current Position

Single Device or Global

GET /v1/gps/current?device_id=my-tracker-1

Returns the latest GPS point for a specific device. Omit device_id to get the globally latest point across all devices. Results are cached in Redis (gps:latest:{deviceId} / gps:latest:global, TTL 5 min).

GraphQL:

query { currentGps(deviceId: "my-tracker-1") { latitude longitude heading speedKmh timestamp } }

All Devices

GET /v1/gps/current/devices

Returns the latest GPS point per device using DISTINCT ON (device_id). Useful for multi-device live maps.

GraphQL:

query { currentGpsPerDevice { deviceId latitude longitude heading speedKmh timestamp } }

WebSocket Live Updates

Subscribe to channel "gps" to receive GpsUpdate messages after every insert.

The message type tag is camelCase (gpsUpdate), but the inner data object is the raw GpsData model serialized in snake_case with ISO 8601 string timestamps (not the camelCase GraphQL shape).

interface GpsUpdate {
type: 'gpsUpdate';
data: {
id: string;
device_id: string | null;
trip_id: string | null;
latitude: number;
longitude: number;
altitude: number | null;
heading: number | null;
timestamp: string; // ISO 8601
speed_kmh: number;
speed_mps: number;
speed_mph: number;
speed_knots: number;
pdop: number | null;
hdop: number | null;
vdop: number | null;
created_at: string; // ISO 8601
};
}

See the WebSocket API for connection details.

Redis Caching on Ingest

Every POST /v1/gps updates three Redis caches after the TimescaleDB insert:

Cache KeyContentTTL
gps:latest:globalFull GPS point JSON5 min
gps:latest:{device_id}Full GPS point JSON (per device)5 min
Trip GPS cache (append)Appended GPS point for the resolved trip_id2 min

The GET /v1/gps/current and GET /v1/gps/current/devices endpoints read from these caches before falling back to TimescaleDB.

Auto Trip Resolution

When a GPS point is ingested and a device_id is resolved, the API automatically queries for an active trip linked to that device:

SELECT id FROM "Trip"
WHERE device_id = $1 AND status = 'active' AND gps_logging_enabled = true
LIMIT 1

If found, the GPS point's trip_id is set to that trip. This means GPS data is automatically associated with trips without the sender needing to know about trips -- the device link is sufficient.

Deletion Protection

A GPS point whose trip_id is set cannot be deleted — this applies to any linked trip regardless of status. DELETE /v1/gps/{id} (REST, perm gps:delete) returns 409 Conflict in that case, 404 if the point does not exist, and 204 No Content on success. The GraphQL deleteGpsData mutation returns an equivalent error.

Completing a trip does not clear trip_id, so a point stays protected after the trip ends. To delete a linked point you must first unlink the trip (set the point's trip_id to NULL).

  • Devices — link API keys to named devices
  • Geofences — proximity alerts on GPS insert
  • Settings — speed unit, coordinate format, retention
  • Trips: Overview — GPS logging for transport operations