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
- Apollon POSTs a GPS payload to
/v1/gpswith an API key in theAuthorizationheader. - The API resolves a device from the request (header, API key, or NULL).
- Auto trip_id resolution: if a device was resolved, the API queries for an active trip (
status = 'active'andgps_logging_enabled = true) linked to that device and automatically setstrip_idon the GPS point. - The point is inserted into TimescaleDB.
- Redis caching: the API updates three caches -- per-device (
gps:latest:{device_id}), global (gps:latest:global), and appends to the trip GPS cache iftrip_idis set. Per-device and global caches use a 5-minute TTL; the trip GPS cache uses a 2-minute TTL. - WebSocket broadcast: a
GpsUpdatemessage is sent to all clients subscribed to the"gps"channel. - Geofence check: all active geofences are evaluated for proximity (see Geofences: Proximity Check).
GPS Data Fields
| Field | Type | Nullable | Description |
|---|---|---|---|
id | UUID | no | Point ID |
device_id | string | yes | Resolved device (see Device resolution) |
trip_id | UUID | yes | Active trip at time of insert |
latitude | float | no | Degrees (WGS-84) |
longitude | float | no | Degrees (WGS-84) |
altitude | float | yes | Meters |
heading | float | yes | Degrees 0–360 |
speed_kmh | float | no | km/h |
speed_mps | float | no | m/s (raw from Apollon) |
speed_mph | float | no | mph |
speed_knots | float | no | Nautical knots |
pdop | float | yes | Position dilution of precision |
hdop | float | yes | Horizontal dilution of precision |
vdop | float | yes | Vertical dilution of precision |
timestamp | timestamptz | no | ISO 8601 timestamp |
created_at | timestamp | no | Server insert time |
REST Endpoints
| Method | Path | Permission | Description |
|---|---|---|---|
| POST | /v1/gps | gps:write | Ingest GPS point |
| GET | /v1/gps | gps:read | List GPS data (paginated) |
| GET | /v1/gps/current | gps:read | Latest GPS point (optionally per device) |
| GET | /v1/gps/current/devices | gps:read | Latest GPS point per device |
| GET | /v1/gps/{id} | gps:read | Get single point |
| DELETE | /v1/gps/{id} | gps:delete | Delete 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 Key | Content | TTL |
|---|---|---|
gps:latest:global | Full GPS point JSON | 5 min |
gps:latest:{device_id} | Full GPS point JSON (per device) | 5 min |
| Trip GPS cache (append) | Appended GPS point for the resolved trip_id | 2 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).
Related
- 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