-
Notifications
You must be signed in to change notification settings - Fork 1
Router
The Router is TrackHub's multi-protocol integration layer. It connects to external GPS tracking providers — each with its own API technology (REST, GraphQL, SOAP, vendor SDK) — and standardizes their position data into a unified format the rest of the platform consumes.
It has no database of its own. Master data is read and written through the Manager; positions, history, health checks and sync runs go to Telemetry. The repository also contains the SyncWorker, the background host that drives synchronization.
Related pages: Adding a Provider · Telemetry · Inter-Service Communication
graph LR
subgraph Providers["GPS Tracking Providers"]
rest["REST API<br/><i>e.g. Traccar</i>"]
gql["GraphQL API"]
soap["SOAP / XML"]
sdk["Vendor SDK<br/><i>e.g. Geotab</i>"]
end
subgraph Router["TrackHub Router"]
adapter["Protocol Readers<br/><i>keyed by ProtocolType</i>"]
norm["Data Normalizer"]
cache["Session / Token Store"]
end
rest -->|HTTP/JSON| adapter
gql -->|HTTP/GraphQL| adapter
soap -->|HTTP/XML| adapter
sdk -->|SDK| adapter
adapter --> norm
cache --- adapter
norm --> unified["Unified Position Model<br/><i>TransporterId, Lat, Lng, Speed,<br/>Altitude, EventDate, Attributes</i>"]
Downstream services and the portal always work with the same data shape, whatever the provider underneath.
| Protocol | Description | Authentication | Real-time positions | Position history |
|---|---|---|---|---|
| CommandTrack | CommandTrack REST API | Bearer token (absolute expiry, persisted to Manager) | ✅ | ✅ |
| Traccar | Open-source tracking server | Basic auth | ✅ | ✅ |
| Flespi | Flespi telematics platform | Static token header | ✅ | ✅ |
| GeoTab | Geotab fleet management (vendor SDK) | Session-based (cached; the SDK self-heals) | ✅ | ✅ |
| GpsGate | GpsGate tracking platform | Static token header | ✅ | ❌ (no history API) |
| Navixy | Navixy tracking platform | Login → session hash (cached) | ✅ | ✅ |
| Samsara | Samsara fleet platform | Static bearer token | ✅ | ✅ |
| Wialon | Wialon hosting platform | Token login → session id (cached) | ✅ | ✅ |
| Protrack | Protrack GPS platform | MD5-signature → bearer token (absolute expiry, persisted to Manager) | ✅ | ✅ |
Mettax is reserved in the ProtocolType enum but has no provider assembly yet.
Enabled protocols are listed in configuration:
AppSettings__Protocols__0=CommandTrack
AppSettings__Protocols__1=Flespi
AppSettings__Protocols__2=GeoTab
AppSettings__Protocols__3=GpsGate
AppSettings__Protocols__4=Navixy
AppSettings__Protocols__5=Protrack
AppSettings__Protocols__6=Samsara
AppSettings__Protocols__7=Traccar
AppSettings__Protocols__8=WialonRegistration is keyed, uniform, case-insensitive and fail-fast (ProtocolRegistrationExtensions):
- Each provider's
DeviceReader,PositionReaderandConnectivityTesteris registered keyed by itsProtocolType(AddKeyedTransient). -
PositionRegistry,DeviceRegistryandConnectivityRegistryare scoped and resolve exactly one reader per lookup from the caller's live request scope viaGetKeyedService— no resolve-all-and-filter, no disposed-scope hand-off. - Readers are keyed transient and
HttpClientServiceis transient, so two concurrent same-protocol operators in one sync scope never share mutableInitstate. - The
AppSettings:Protocolsentry resolves the assembly's real namespace regardless of casing (GeoTab↔Geotab).
There is one uniform registration path. The old fake-async adapter subsystem is gone: every provider's Init is a real Task, and each reader implements IPositionReader / IExternalDeviceReader directly.
Failure modes are loud, not silent:
| Situation | Result |
|---|---|
| A configured protocol resolves no assembly or no reader types | Throws at startup |
| A capability declaration mismatches the reader classes in its assembly | Throws at startup |
An operator's ProtocolTypeId has no registered reader |
ProtocolNotSupportedException, naming the protocol — never a masked .First()
|
What each provider's API actually supports is declared once, in TrackHub.Router.Domain.Constants.ProviderCapabilityCatalog, as ProviderCapability flags: RealTimePositions, PositionHistory, DeviceCatalog, ConnectivityPing. GpsGate lacks PositionHistory; Mettax is None.
The catalog is exposed to clients through the Router's providerCapabilities GraphQL query (Positions/Read, [PlatformScoped]).
Requesting something the operator's provider cannot do throws ProviderCapabilityNotSupportedException → GraphQL code PROVIDER_CAPABILITY_NOT_SUPPORTED, with protocol and capability extensions. This is deliberately distinct from:
-
FEATURE_DISABLED, which means TrackHub account/feature gating; -
ProtocolNotSupportedException, which means no reader is registered.
The distinction matters commercially: a paying customer must be able to tell that the limitation is their GPS provider's, not TrackHub's. The message says so explicitly.
Enforcement happens in PositionBaseHandler before the provider call; GpsGate's history stub throws the same typed exception as defense in depth. Never let such a request reach the provider, degrade to empty results, or surface as a masked server error.
The limitation applies only to provider-side history replay. Accounts with the
gps.positionHistoryfeature still get TrackHub-stored history for any provider, because the Router records synced positions regardless.
sequenceDiagram
participant SW as SyncWorker
participant Auth as AuthorityServer
participant Router as Router API
participant Mgr as Manager API
participant GPS as GPS Provider
participant Tele as Telemetry API
participant Geo as Geofencing API
participant Trip as TripManagement API
SW->>Auth: Client Credentials Grant
Auth-->>SW: Access Token (syncworker_client)
SW->>Router: accountsToSync()
Router->>Mgr: accountsToSync()
Mgr-->>Router: Accounts list
Router-->>SW: Accounts list
loop For each Account (concurrently, under a global operator gate)
SW->>Router: operators(accountId)
Router->>Mgr: operatorsMaster(accountId)
Mgr-->>Router: Operators + Credentials
Router-->>SW: Operators
loop For each Operator
SW->>GPS: Authenticate (cached session / token)
GPS-->>SW: Session or new token
alt Token refreshed
SW->>Router: updateToken(credentialId, newToken)
Router->>Mgr: updateToken(...)
end
SW->>GPS: Fetch device positions
GPS-->>SW: Raw position data
SW->>Tele: bulkTransporterPosition(...)
SW->>Tele: appendPositionHistoryBatch(...)
SW->>Geo: processPositions(...)
SW->>Trip: processTripPositions(...)
SW->>Tele: recordOperatorSyncRun(...)
end
end
Note over SW: Reverse geocoding runs AFTER the projection write
-
Accounts are processed concurrently, under a single global operator-concurrency gate (
AppSettings:MaxConcurrentOperatorSyncs, default 10), so one slow account cannot stall the rest while total provider load stays bounded. - Reverse geocoding runs after the latest-projection write and geofence detection. Enrichment re-upserts the projection only when it resolved a blank address, so it never delays position freshness.
-
The operator device catalog is cached (
IDeviceCatalogCache,AppSettings:DeviceCatalogCacheSeconds, default 60) and invalidated by the device-sync loop, instead of re-fetched from Manager every cycle. -
A persistently failing operator backs off exponentially (
IOperatorSyncBackoff, 1 → 30 min) out of the device-sync and health loops until its first success. -
Provider failures are recorded, not swallowed. A position fetch that throws is logged and carried on
PositionsRetrieved.Notification(ProviderErrorCode), which records the sync run asFAILEDwith the error and raisesGpsOperatorPositionSyncFailed— no more silentSUCCEEDEDwithread = 0. A single failing operator does not abort the account fan-out or skip the interval update. -
The Geofencing feed is best-effort and isolated, so a Geofencing outage cannot flip an already-stored batch to
FAILED. -
Every device-sync attempt records a health observation (
checkType = DEVICE_SYNC;HEALTHYwhen the provider answered,OFFLINEwhen it did not), under the Router's service identity. Accounts that only sync manually — no SyncWorker, nogps.integration— still get a meaningful Health status. The worker's 1-minute PING loop remains the background monitor for GPS-enabled accounts.
Session and token artifacts are cached in-process per credential in a singleton IProviderSessionStore, keyed by CredentialId and fingerprinted by the full decrypted credential — so a credential rotation is automatically a cache miss.
| Provider | Cached artifact | Lifetime |
|---|---|---|
| Wialon | sid |
4 min sliding |
| Navixy | session hash | 30 min sliding |
| Geotab | SDK session id | 8 h sliding (API constructed with password + sessionId so the SDK self-heals; readers call PersistSession() after calls) |
| CommandTrack, Protrack | Bearer token | Durable copy in Manager (UpdateTokenAsync) plus a non-sliding in-process fast path |
Wialon and Navixy re-login only on a miss and retry once after re-auth on an invalid-session error. That also fixed both providers' HTTP-200 error payloads, which previously deserialized into silent empty results — IWialonResponse.Error and INavixyResponse.Success + Status now throw.
Connectivity
Pingalways performs a REAL provider round-trip. A cached session must never no-op the health monitor.
The store is in-process, consistent with the single-instance SyncWorker deployment.
| Guard | Scope | Why |
|---|---|---|
IOperatorSyncLock |
Per operator, in-process singleton | The manual-sync and background device-sync paths both acquire it in SyncOperatorDevicesCommandHandler, so a manual "sync now" cannot race the background loop and cause a ResetDeviceCatalog wipe/rebuild conflict |
ExecutionIntervalManager |
Position loop interval, in-memory | Single-instance by design |
MaxConcurrentOperatorSyncs |
Global | Bounds total provider load across concurrent accounts |
Both guards assume a single SyncWorker instance. Scaling the worker out would require a cross-instance claim (a PostgreSQL advisory lock or SKIP LOCKED).
triggerOperatorSync is an authorized user feature, not an internal-only operation. It carries [Authorize(Resource = Resources.Credentials, Action = Actions.Custom)] — the same grant portal roles hold for PingOperator — plus [RateLimiting]. Any provider-touching operation reachable on demand must ship with both.
AttributesVm keeps five promoted fields strongly typed — ignition, satellites, mileage, hourmeter, temperature — and carries everything else in an open Extra bag (a JSON object string, built and read through Domain.Helpers.AttributesExtra).
Extra maps 1:1 onto Telemetry's additive extra: String field on AttributesDtoInput / AttributesVm. Because the underlying column is a schemaless json, adding a new provider signal requires no migration and no cross-service schema change. It round-trips on read and flows through the history path.
Units at the boundary:
-
Hourmeteris in hours (its producer is a Hobbs meter reading decimal engine hours). -
Mileagehas no conversion applied at the mapper boundary — a consumer must not assume metres or kilometres without checking the provider. - Speed is km/h, coordinates are decimal degrees, timestamps are UTC
DateTimeOffset.
The wire name is hourmeter everywhere. CommandTrack's payload calls it hobbsMeter and is remapped only at that provider's model, with [JsonPropertyName("hobbsMeter")].
graph LR
Router["Router API"]
Manager["Manager API<br/><i>Operators, devices,<br/>credentials, master data</i>"]
Telemetry["Telemetry API<br/><i>Positions, history,<br/>health, sync runs</i>"]
Geofencing["Geofencing API<br/><i>Visit detection</i>"]
Trip["TripManagement API<br/><i>Trip detection</i>"]
Router -->|"GraphQL (read/write)"| Manager
Router -->|"GraphQL (write)"| Telemetry
Router -->|"GraphQL (write)"| Geofencing
Router -->|"GraphQL (write)"| Trip
The Router pushes one position per transporter per call to the detection feeds. Any debounce or run length held in memory downstream could therefore never elapse — see Trip Management.
-
GraphQL at
/graphql— operators, devices, positions, position history, provider capabilities, sync dispatch. - REST — a third-party integration surface for reading unit information, documented through Scalar.
-
/health—AddHealthChecks()+UseHealthChecks("/health"), per the canonical middleware order.
Provider HttpClients disable automatic redirects: an operator-configured base URL must not be able to 302-redirect the Router inward.
Platform
- Architecture
- Technology
- Inter-Service Communication
- Database
- Security and Identity
- User Permissions Overview
Services
- Manager
- Telemetry
- Router
- Adding a Provider
- Geofencing
- Trip Management
- Reporting
- Common Library
- Frontend
Engineering
User docs
- User Guide (ships in the app)