-
Notifications
You must be signed in to change notification settings - Fork 1
Inter Service Communication
Every service-to-service call in TrackHub is GraphQL over HTTPS. There is no message bus, no gRPC and no direct cross-service database access — a service that needs another service's data either calls it, or maps a small read-only projection of its tables (see Database).
Related pages: Architecture · Security and Identity · Testing Strategy
graph LR
sync["SyncWorker"]
rtr["Router"]
mgr["Manager"]
tele["Telemetry"]
geo["Geofencing"]
trip["TripManagement"]
rpt["Reporting"]
sec["Security"]
sync --> rtr
rtr --> mgr
rtr --> tele
rtr --> geo
rtr --> trip
mgr --> sec
mgr --> rtr
sec --> mgr
geo --> mgr
trip --> mgr
trip --> tele
rpt --> mgr
rpt --> rtr
rpt --> tele
rpt --> geo
rpt --> trip
| From | To | Purpose |
|---|---|---|
| SyncWorker | Router | Drives the position, device and health sync loops |
| Router | Manager | Operators, devices, credentials, master data, manual-sync dispatch |
| Router | Telemetry | Latest-position and history writes, health-check and sync-run records |
| Router | Geofencing |
processPositions — the geofence detection feed |
| Router | TripManagement |
processTripPositions — the trip detection feed |
| Manager | Security | User creation, identity and authorization checks |
| Manager | Router | Manual sync dispatch (120 s timeout — the call awaits a provider fetch) |
| Security | Manager | Account and master-data reads |
| Geofencing | Manager | Alert emission, background job runs |
| TripManagement | Manager | Alerts, job runs, driver/transporter/group validation, document metadata, public-link grants |
| TripManagement | Telemetry |
positionHistoryRange for route replay |
| Reporting | Manager, Router, Telemetry, Geofencing, TripManagement | Report datasets |
TripManagement makes no call to Geofencing — it reads geofencing.geofences as a read-only EF projection, once, to snapshot a stop's arrival area when a trip starts.
Clients are created through IGraphQLClientFactory.CreateClient(Clients.X[, asService]), where Clients is the shared constant catalog (Hub, Geofence, Identity, Manager, Router, Security, Telemetry, TripManagement). The endpoint comes from configuration key AppSettings:GraphQL{ServiceName}Service.
Registration is centralized. Every named client is registered through services.AddGraphQLClient(name, …) or AddGraphQLServiceClient(name) in Common.Infrastructure — never a raw AddHttpClient. The extension applies:
- a 30 s timeout (Manager → Router uses 120 s)
-
Authorizationandx-correlation-idheader propagation - an explicit resilience choice
| Policy | When | Services using it |
|---|---|---|
NoRetry |
Default. Timeout + circuit breaker only. GraphQL is always POST, so a retried mutation is a duplicated mutation. |
Everything not listed below |
WithRetry |
Query-only clients, where a retry is safe | The Identity client; Reporting's Router, Geofence and Telemetry readers |
An unregistered client name silently yields a default
HttpClient— 100 s timeout, no header propagation, no resilience. Always register through the extension.
This is the single most important rule in this area.
Tenant scope follows the CALLER; provider credentials are read with the SERVICE identity.
A service registers both client variants (AddGraphQLClient + AddGraphQLServiceClient) and picks per call:
- the propagating client for reads whose scope must follow the caller — the caller's bearer token is forwarded, so the producer applies that user's account and group visibility;
- the service client (
asService: true, orAuthorityServer:IsService = truefor a whole host) for work the platform performs on the caller's behalf, using the host's own client-credentials identity.
The Router uses this split to keep GPS provider credentials out of user permissions entirely. Every interactive provider-touching surface — live map, per-transporter position, history replay, trips, device listing, ping, manual sync — resolves the operator through IOperatorReader (propagating, so Manager applies the caller's visibility), then reads the decrypted credential through IOperatorSystemReader (service identity). A user's permissions are never widened to obtain credential material.
Two permissions are deliberately distinct:
| Grant | Means | Held by |
|---|---|---|
Operators/Custom |
"may operate this GPS integration" — ping, manual sync | Manager and User roles |
Credentials/Custom |
"may view decrypted credential material" | Credential administration only — granting it anywhere also grants sight of provider secrets |
The *SystemReader / *SystemWriter suffix marks the client-credentials counterpart of a propagating client: PositionSystemWriter, PositionHistorySystemWriter, OperatorHealthCheckSystemWriter, ResolvedAddressSystemWriter, OperatorSystemReader, GeocodingProviderReader.
The SyncWorker registers everything with headerPropagation: false and IsService: true, so both variants resolve to its own identity there — it has no user to propagate.
Every service-identity call needs two things on the Security side, or it is denied:
- the client name registered in
security.clients; - a matching row in
security.service_client_permissions— client, resource, action, scope and audience.
Both are seeded by the Security DBInitializer. A missing registration or grant produces a clean FORBIDDEN naming the required Resource.Action.
Current platform-internal grants include:
| Identity | Grants |
|---|---|
router_client / syncworker_client
|
The Telemetry surface, the Manager master-data surface, Geofencing/Custom (without which the SyncWorker's processPositions feed is denied), and TripTracking/Custom — and nothing else in the trip module, so those two identities can call processTripPositions only |
security_client |
Audit/Write |
geofence_client |
Alerts/Write, BackgroundJobs/Write
|
trip_client |
Alerts/Write, BackgroundJobs/Write, Drivers/Read, Transporters/Read, Groups/Read, Documents/Read, `PublicLinks/Write |
Each client class holds its GraphQL documents as internal const string fields, exposed to the contract test suite via [assembly: InternalsVisibleTo("TrackHub.ServiceContracts.Tests")].
Never re-type a query string in a test. Reference the production constant, so a drifted document fails the test instead of silently passing against a stale copy. See Testing Strategy.
Enum values travel in GraphQL variables, invisible to document validation. Every enum-typed literal a client sends (
checkType,triggerType,result, …) needs a round-trip (Layer B) test asserting it coerces into the producer's enum. Document validation alone will not catch a renamed enum member.
-
Authorization decisions are cached for 30 s in
Common.Infrastructure.IdentityService— both the user decision (authorizeUser) and the service decision (isValidServiceForResource). Configurable viaAppSettings:AuthorizationCacheSeconds;0disables it. This is the same staleness window as account status and feature flags. - The client-credentials token is cached process-wide, with a 60 s expiry margin.
- Provider sessions and tokens are cached across sync cycles — see Router.
Several hot paths are batched deliberately, to avoid an N+1 across the network:
| Call | Consumer | Why |
|---|---|---|
Manager allAccountFeaturesMaster
|
SyncWorker, feature-matrix report | One read instead of one per account |
Manager transporterDocumentCompliance
|
Documents report | |
Manager operatorsMaster
|
SyncWorker | Carries credentials for service principals, so the worker never re-fetches operators one by one |
Telemetry transporterPositionsByOperators
|
Live-map hot path |
Keyless service-called surfaces are invisible to the account-scope coverage gate. The gate (
Common.Application.Testing.AccountScopeCoverage) only sees wire keys, so a batched…Masterquery is caught only by enumerating the consumers'internal constdocuments and each client's identity mode. Do that enumeration for every new cross-service call.
-
Attribute naming is
hourmetereverywhere. The CommandTrack provider payload calls ithobbsMeterand is remapped only at that provider boundary, with[JsonPropertyName("hobbsMeter")]. -
AttributesVm.Hourmeteris expressed in hours.Mileagehas no conversion applied at the mapper boundary, so a consumer must not assume metres or kilometres without checking the provider. -
Timestamps are
DateTimeOffsetin UTC, end to end. See Database. -
Delete mutations return the deleted entity's identifier (typed to follow the entity key —
Guid,longorint), not a boolean. -
x-correlation-idpropagates across every hop, so one user action can be traced through the whole call graph in the centralized logs.
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)