Asgard backend · Mobile app programme

Release 1 (Core tracking) — API design notes and codebase gap analysis

What the client brief asks for, what the updated phasing keeps in Release 1, how the mobile API should be shaped, and how much of it the current Asgard backend already delivers.

Written 29 Sep 2026 Reviewed against branch prod at b65c0f27 Companion to the OpenAPI draft docs/openapi/mobile-v1.yaml

1. Spec vs. updated phasing

The client brief (Asgard Fleet Tracking App Development Brief) listed nine core mobile functions. The updated Release 1 phasing keeps the tracking core and defers the rest.

Brief itemRelease 1Notes
Secure login and permissionsYesBrief demands role-based access and secure sessions
Unit list (search, filter, state, ignition, last update)YesPlus group visibility
Live mapYesBrief: clustering for thousands of units, optional geofence overlay
Vehicle summary (speed, ignition, mileage, sensors)YesFuel level and "recent events" dropped from the R1 wording
History and tracksYes
Trips, parkingYesBrief calls these reports; R1 treats them as screens, not exports
Geofences display, visits, dwellBasic
Selected reports + PDF/CSV exportDeferred
Notifications (view, filter, acknowledge)Deferred
Location-sharing linksDeferredNavigation deep links (Google/Apple Maps) are kept
Fuel events, brake, TPMS alertsDeferredTPMS values can still surface as "available sensors"

Two brief items are not features but gate Release 1.

24-month history. "Historical trip data access" only works if Asgard holds the data. Today Asgard keeps 180 days and fetches older ranges from Wialon Local on demand. Once Wialon is retired that fallback is gone, so the storage and partitioning work in brief §3 is an R1 dependency, not a later phase.

Devices → Flespi direct. Ingest already consumes Flespi MQTT, so this is mostly device reconfiguration and a channel change, but it gates the Wialon exit criteria and should be planned alongside R1.

2. API shape decision

The existing API is a single graphene GraphQL endpoint at /graphql, authenticated by a static apiToken argument on every query. There is no REST layer, no OpenAPI document, no JWT and no role model.

Recommendation: add a new versioned REST API at /api/v1 for the mobile app, implemented with Django REST Framework and drf-spectacular so the Swagger document is generated from code, and leave GraphQL untouched for the web platform.

  • The brief explicitly asks for "secure, versioned APIs". Path versioning plus generated OpenAPI is the simplest way to prove that.
  • The mobile team gets typed client generation straight from the spec.
  • Both APIs sit on the same Django models and a shared service layer (trip engine, status calculator, visibility filter), so nothing is duplicated except the transport.
  • The alternative, bolting JWT and per-resolver access checks onto GraphQL, touches over a hundred resolvers and still leaves the web app's token model unchanged.

Endpoint summary (24 paths)

AreaEndpoints
AuthPOST /auth/login · POST /auth/refresh · POST /auth/logout · GET /auth/me
GroupsGET /groups
VehiclesGET /vehicles · GET /vehicles/{id} · GET /vehicles/{id}/status · GET /vehicles/{id}/sensors · GET /vehicles/{id}/navigation
Live mapGET /live (bulk, delta via since, bbox and clustering) · GET /live/stream (SSE, optional)
HistoryGET /vehicles/{id}/track · GET /vehicles/{id}/timeline
TripsGET /vehicles/{id}/trips · GET /vehicles/{id}/stops · GET /trips/{id}
GeofencesGET /geofences · GET /geofences/{id} · GET /geofences/{id}/visits · GET /vehicles/{id}/geofences · GET /vehicles/{id}/geofence-visits
SystemGET /health · GET /config (feature flags, minimum app version, poll interval)

3. What exists today, per Release 1 item

Reuse wrap existing code Extend existing code needs real changes New build from scratch

3.1 Secure login and permissions Extendlarge

ExistsGap
operative.User with bcrypt password, login mutationNo JWT or refresh, no expiry. Static plaintext api_token in the database, generated with random.choice
Separate Admin, Driver, Mechanic login tablesNo role or permission model. "Role" is which table you are in
Visibility = User.units M2M (flattened from Wialon groups by sync_users)Group.users does not grant visibility. Decide whether R1 visibility is unit-list or group-based
ApiToken for integrationsAccess checks are inconsistent: historicMessages, historicTrips, generateLocator and gpsHealthReport never verify the caller can see the unit
Driver.firebase_token plus FCM senderNo push registration for fleet-manager users (fine for R1, needed for R2)

Work: JWT access and refresh tokens (rotation, revocation list), a me endpoint, a role and permission enum, one visible_units(user) queryset used by every R1 endpoint, and rate limiting on login.

3.2 Vehicle list, search, groups, live status Extendmedium

ExistsGap
entities.Unit (name, plate, ident, type, drivers, groups)No make, model or VIN fields, only a custom_fields JSON blob
telemetry.LastMessage, one per unit, trigger-maintained: position, payload, sensors, odometer, geofence ids, last movement timeNo stored moving / stopped / parked / offline state. Ignition is only derivable from payload['engine.ignition.status']
units query with search and pagination (raw SQL)Search covers name and plate only. No driver search, no status filter
groups queryGroup membership exposed as ids only
StationaryState table (trigger-maintained)Not exposed anywhere
DriverLinkLog for the current driverFine to reuse

Work: a motion-state calculator (speed, ignition and a staleness threshold from config), promote ignition, fuel and speed to real columns or a materialised view on the last-message table, and a list endpoint with status and group filters plus cursor paging.

3.3 Live fleet map Extendmedium

ExistsGap
The last-message table gives every unit's last position in one queryNo bounding-box filter, no delta (since), no clustering
SSE endpoint /telemetry/stream on Postgres LISTEN notifyupdateBroken. The notify-handling loop sits after a return, so clients only ever receive heartbeats (telemetry/views.py:81-85)

Work: a /live endpoint with a since delta and server-side clustering (PostGIS cluster function or a grid at low zoom). Fix or rewrite SSE, which is optional for R1 since polling every ten seconds is acceptable at fleet sizes in the hundreds.

3.4 Vehicle details: speed, ignition, last update, mileage, sensors Reusesmall to medium

ExistsGap
Last-message resolvers for speed, ignition, mileage, fuel, RPM, voltageAll read raw Flespi keys from JSON with no unit normalisation
Sensor config (formula or table per unit) loaded from ExcelValues are never evaluated server-side, so the app would receive raw parameters
Extensive TPMS, EBPMS, DTC and overweight modelsTrailer-focused, but fine to expose as sensor values with categories
DailyOdometer, odometerHistoricReuse for "today" mileage

Work: a sensor-value normaliser (key, label, unit, category, updated time) applied to the last message.

3.5 Navigate in Google or Apple Maps Newtrivial

Nothing exists. The endpoint formats the last position into deep links.

3.6 Route history and track replay Extendmedium

ExistsGap
historicMessages(unitId, from, to) returns raw pointsNo access check, no pagination, no simplification, no ignition per point
Daily partitions on telemetry_message, PostGIS point column180-day retention. Older data is fetched live from Wialon (wialon_fetch.py)

Work: a paged, simplified track endpoint (Douglas-Peucker via PostGIS), an enforced maximum range, and the 24-month retention decision in section 4.

3.7 Trips, stops, parking, journey details Extendlarge

ExistsGap
get_trips.py: on-the-fly trip detection from raw messages (speed above zero starts, 300 s gap ends)Recomputed on every request, linear in message count. Timezone hard-coded to Europe/Dublin (get_trips.py:112)
historicTrips query, tripsReport XLSX/PDFRejects ranges older than 180 days
Trip, CurrentTrip tables plus trigger t00015_telemetry_message_trip_handlerNever read by any resolver. Correctness unverified
StoodStatus (stationary periods with odometer)Exposed, but the trigger's lifecycle across migrations is unclear

Work: a persisted trip and stop engine (ignition-aware, idle versus parked, max and average speed, distance, addresses) that runs incrementally on ingest and backfills history. This is the biggest pure-backend item in R1, and it also unlocks the timeline endpoint and later reports.

3.8 Geofence presence, display, visits, dwell Extendmedium

ExistsGap
resources.Geofence with PostGIS shape, polygon / radial / linear, categories, colourVisibility via a per-user M2M. No group scoping
Presence per message (geofences_ids) and a live containment query for all unitsPresence has no "entered at" timestamp on the last message
Geofence enter and exit events via trigger into telemetry_eventEvents are kept for 14 days in the query layer
Visits and dwell computed inside the geofences XLSX reportNo visits table, no query, rejected beyond six months

Work: a geofence-visit table populated from enter and exit events (or derived from transitions in the per-message geofence ids), plus GeoJSON serialisation of the PostGIS shape.

3.9 Historical trip data access (24 months) Newlarge, mostly infrastructure

Today: 180 days local plus the Wialon fallback. Brief: 24 months in Asgard, searchable through the API, without hurting live performance. See section 4.

4. Cross-cutting backend work R1 depends on

Retention to 24 months

Three options, cheapest first.

Option A · Cheapest

Grow the existing partitions

Keep daily partitions, raise the cleanup job to about 730 days, add monthly cold-partition compaction and move partitions older than 90 days to a cheaper tablespace. Storage grows roughly fourfold and needs sizing against current partition sizes.

Option B · Best long-term

Adopt TimescaleDB

Native compression of ten to twenty times on telemetry and built-in retention policies. A bigger migration, but the strongest footing for two years of positions.

Option C · Needs sign-off

Keep raw six months, derive the rest

Persist trips, stops, visits and daily odometer for 24 months but raw messages for six. Cheapest by far, but track replay beyond six months would be unavailable, which the client must accept.

  1. Ingest hardening. Production ingest appears to be an external receptor script (bash/mqttreceptor.sh references /home/gmadmin/receptor/app.py, which is not in this repo) alongside mqttretranslator, which self-restarts every 40 minutes. Bring it into the repo and containerise it before device migration.
  2. Access-control audit. One visible_units(user) helper, applied everywhere, with tests.
  3. Timezone. Store per user (User.time_zone already exists) and stop hard-coding Dublin and London.
  4. Public media. Report files are served unauthenticated from /public/. Out of R1 scope, but worth flagging to the client alongside the security requirements in brief §5.

5. Rough sizing

Backend only, one senior Django developer, excluding the mobile app, device migration and Wialon data migration. Ranges are wide because the trip engine and retention decisions are open. Bars are drawn to a common scale of 0 to 20 days.

AreaSizeDev-daysRange
DRF and spectacular scaffolding, error and paging conventions, CI publish of SwaggerS3–4
Auth: JWT, refresh, roles and permissions, me, visibility helper, testsM8–12
Vehicles list, detail, status, sensors; motion state; sensor normaliserM6–9
Live map: /live delta, bbox and clustering (SSE fix optional, add 3)M5–8
Track: paged and simplified historyS3–4
Trip and stop engine (persisted, incremental, backfill) plus trips, stops, trip detail, timelineL12–18
Geofences: GeoJSON, presence with entered-at, visits table and endpointsM6–9
Navigation links, health, configS1–2
24-month retention (partition option, including sizing, archive job, restore test)M–L8–15
Ingest hardening, receptor into the repoM5–8
Total57–89About 12–18 weeks for one developer, 7–10 weeks for two

Reusable as-is with minimal wrapping: Unit, LastMessage, Geofence with PostGIS presence, DailyOdometer, DriverLinkLog, Group, the TPMS models, daily partitioning and its cleanup command, and the Flespi MQTT consumer.

The OpenAPI draft this document accompanies is published separately as a Redoc reference page and lives in the repo at docs/openapi/mobile-v1.yaml.