-
Notifications
You must be signed in to change notification settings - Fork 1
Manager
The Manager is TrackHub's master-data service and the largest one. It owns the app and map schemas, and the DDL for the telemetry schema that the Telemetry service serves.
Related pages: Database · Inter-Service Communication · Reporting
| Module | Contents |
|---|---|
| Accounts | Accounts and lifecycle status, account settings, account features, branding, support grants |
| Identity (portal-side) | Users, user settings, groups, user–group and transporter–group membership |
| Assets | Transporters, transporter types, devices, transporter–device assignments |
| GPS integration | Operators, encrypted provider credentials, the device-synchronization command surface |
| Geospatial reference | Geocoding providers, points of interest (map schema) |
| Documents | Documents, versions, types, signatures, sharing, retention |
| Workforce | Drivers, driver qualifications, driver–transporter assignment history |
| Alerts & notifications | Alert events, notification rules, deliveries, templates, alert subscriptions |
| Platform | Announcements, audit events, background job runs, public link grants, the report catalog |
High-volume position data is not here — it belongs to Telemetry.
An Account is the tenant boundary. Every tenant-owned row across the platform carries its AccountId, and AccountScopeBehavior enforces it centrally (see Architecture).
app.account_features is the per-account feature switchboard. A feature key gates whole modules, and a request for a disabled feature fails with GraphQL code FEATURE_DISABLED — distinct from a permission failure.
| Feature key | Gates |
|---|---|
gps.integration |
GPS provider synchronization |
gps.positionHistory |
Position history storage and replay (also drives retention days) |
geofencing |
Geofence management and detection |
trip-management |
The entire trip module — dispatch board, commands, queries, jobs, position feed, public links |
documents |
Document management |
workforce |
Extended workforce capabilities: qualifications, time-bounded assignment history, expiration alerts, workforce reports |
notifications |
The notification delivery layer (rules, subscriptions, templates, dispatch) |
notifications.email |
The email channel — billable, enforced at configuration time and re-checked at dispatch |
notifications.whatsapp |
The WhatsApp channel — same treatment |
public-links |
Public link grants |
driver-mobile |
Driver mobile application access |
Two notes worth internalizing:
-
Driver identity itself — registry, credentials, device administration — is core platform and is deliberately NOT gated by
workforce. Only the extended capabilities are. -
Alert events are platform baseline and ungated. It is the delivery layer that
notificationsgates. A dispatcher holds deliveries as Pending or Deferred (never sends them) when the base key or the channel's billable key is disabled.
Feature decisions are cached 30 s, the same window as authorization decisions and account status.
A service that does not register its own
IFeatureFlagServicesilently ignores every[RequireFeature]attribute —Common's default is fail-open. See Architecture.
Accounts carry a lifecycle status. A suspended account is rejected platform-wide with ACCOUNT_SUSPENDED by an opt-out AccountStatusBehavior, read cross-service through the same 30 s cache.
Uploaded files are stored outside PostgreSQL — on the manager-documents volume by default, or in S3 / Azure Blob. That volume is the only stateful data outside the database, so it must be backed up separately; backup-database.sh does not cover it.
| Capability | Behaviour |
|---|---|
| Versioning | Every upload creates a document_versions row; the document row points at the current version |
| Scanning | The document-scan job scans uploads; access policy enforces clean-scan before a document can be shared or attached |
| Signatures |
document_signatures records signing evidence |
| Sharing | Through public_link_grants — revocable, expiring, audited on resolution |
| Expiration | The document-expiration job raises alert events for documents nearing or past expiry |
| Retention | The document-retention-cleanup job removes expired versions per policy |
The REST surface for upload and download is mounted at ~/documents (not under api/). Uploads are capped at 50 MB by nginx.
| Entity | Purpose |
|---|---|
drivers |
The driver registry — core platform, not feature-gated |
driver_qualifications |
Licences and certifications with validity windows |
driver_transporter_assignments |
Time-bounded assignment history |
The workforce-expiration-scan job raises alert events for qualifications approaching expiry. Three reports (driver registry, qualification expirations, assignment history) are in the catalog, all ManagerOnly — their feeds require Drivers/Read, which only the Manager role holds, and widening the dispatcher role to read driver records would be the wrong trade for a report.
Everything about alerting lives in Manager — a deliberate decision, so there is exactly one alert pipeline for the whole platform.
graph LR
src["Source event<br/><i>geofence visit, doc expiry,<br/>comm loss, trip delay…</i>"]
rec["recordAlertEvent"]
ev["AlertEventRecorded<br/><i>domain event</i>"]
eval["AlertRuleEvaluator"]
del["NotificationDelivery<br/><i>Pending / Deferred</i>"]
disp["NotificationDispatchService<br/><i>30 s</i>"]
ch["Channels"]
src --> rec --> ev --> eval --> del --> disp --> ch
RecordAlertEventCommandHandler publishes AlertEventRecorded, which IAlertRuleEvaluator picks up. It matches enabled rules on TriggerEvent == EventType, applies throttling, resolves recipients and creates Pending deliveries.
| Control | Default |
|---|---|
| Per rule + event dedupe window | 60 min, unless dedupeWindowMinutes is set |
maxPerHour |
per rule |
digest |
deliveries start Deferred and are folded hourly |
Evaluation failures never fail the originating command. Jobs that raise alerts directly on the DbContext (document expiration, comm loss) call the evaluator explicitly.
Visibility follows the source resource. AlertEventReader and the in-app feed filter transporter-mapped events through IVisibleTransporterReader for non-privileged users — at read time, where the caller's token role is known, because the evaluator cannot know a subscriber's role. Events without a group-mappable resource stay Administrator/Manager-only.
AlertEventWriter.RecordAlertEventAsync rejects known source resources (Transporter, Operator, Document, Driver) that do not belong to the event's account; unmapped resource types such as Geofence pass through. Rule writes reject selector user and driver ids outside the account.
Manager stores no per-user roles. A roles selector — and the critical-alert escalation — therefore creates one delivery row with RecipientPrincipalType = "Role", and getMyNotifications matches it against the caller's token role within the account. See Security and Identity.
Rule, template and delivery administration is enforced as privileged inside the writers, because the Notifications grants are held by all portal roles for the self-service surfaces.
Email and WhatsApp always target explicit contact endpoints (selector contacts, or subscription Contact, defaulted from Driver.Phone for drivers). Contacts live on subscriptions and selectors, never on identity records, and delivery list VMs mask them.
NotificationDispatchService runs every 30 s over Pending rows. Backoff eligibility is derived from LastModified plus attempt count — 1 / 5 / 15 / 60 minutes, maximum 5 attempts, then Failed plus a NotificationDeliveryFailed alert recorded without evaluation (loop safety).
| Channel | Implementation |
|---|---|
| In-app | Mark Sent; the portal renders from event metadata |
MailKit / SMTP, AppSettings:Smtp
|
|
| Webhook | HMAC-SHA256 X-TrackHub-Signature, 10 s timeout, per-rule URL and secret in ConfigurationJson
|
Meta Cloud API, AppSettings:WhatsApp — an outbound utility template with the rendered text as {{1}}; the stored ProviderMessageId is the metering record |
|
| Push | Contract only, pending the mobile application |
Other Manager jobs in this area: AlertEvaluationService (5 min — comm loss from stale transporter_position per rule thresholdMinutes, single-step escalation per escalateAfterMinutes, plus a daily in-process credential-expiry scan), NotificationDigestService (hourly — folds Deferred deliveries into one pre-rendered summary per rule/recipient/channel), and DeliveryRetentionService (daily, AppSettings:NotificationDeliveryRetentionDays, default 90).
JSON column contracts (ChannelsJson, RecipientSelector, ThrottlingJson) are formalized in TrackHub.Manager.Domain.Records.NotificationRuleContracts and validated by the rule CRUD validators. Legacy '' values parse as empty.
Localized text lives in exactly two places — the recipient-facing resources, and the UI.
-
Never in the database. No platform-default templates are seeded; the DBInitializer actively deletes any
AccountId-null template rows an earlier version may have left.notification_templatesholds account-authored overrides only, and the templates panel's read-only "defaults" are synthesized from the resources at query time (NotificationTemplateReader.BuiltInKeys, idGuid.Empty). -
Never in code. Default subject and body texts live in
Infrastructure/ManagerDB/Notifications/NotificationDefaultMessages.resx(plus an.es.resxsatellite), read throughCommon.Domain.Localization.ResourceLocalizer— the same primitive the AuthorityServer login messages use. -
The locale comes from the recipient (
NotificationLocaleResolver): a User recipient's ownUserSettings.Language, normalized (es-CO→es) → the rule'sConfigurationJson.locale→en. Contacts and webhooks have no user identity, so they use the rule fallback. - In-app deliveries are never rendered server-side. The feed returns event metadata; the portal localizes through its i18n layer.
- WhatsApp localization belongs to Meta. The pre-approved per-language template handles the framing; Manager passes the recipient-resolved language code plus the parameter text.
PlatformAnnouncement (app.platform_announcements) backs the maintenance banner on the public status page.
-
Administered through Administrator-only GraphQL (
Administrativeresource). -
Read through an anonymous REST endpoint,
GET /api/PlatformStatus/announcements.
The anonymous path deliberately bypasses the mediator — the pipeline's logging, authorization and account-status behaviors all assume a principal — and calls the reader directly behind 60 s output caching and a per-client-IP rate limit. A non-partitioned limiter would share one budget platform-wide and fail hardest during an incident, which is why Manager also runs UseForwardedHeaders so the partition key is the real client rather than nginx.
Author text is user content, not a system string — the same precedent as account-authored notification templates. It is written per language (MessageEn required, MessageEs optional, ES falling back to EN) and rendered as plain text.
Accepted degradation: announcements are stored and served by Manager, so a Manager outage removes the banner while the service tiles keep working. A maintenance notice must therefore be published before Manager goes down.
Announcements are not seeded by db-init and are not part of any notification channel — they are display-only.
| Job key | Interval | What it does |
|---|---|---|
alert-evaluation |
5 min | Comm-loss detection, escalation, daily credential-expiry scan |
notification-dispatch |
30 s | Sends Pending deliveries |
notification-digest |
hourly | Folds Deferred deliveries into summaries |
delivery-retention |
daily | Prunes old delivery rows |
document-scan |
— | Scans uploaded documents |
document-expiration |
— | Raises document expiry alerts |
document-retention-cleanup |
— | Removes expired document versions |
workforce-expiration-scan |
— | Raises qualification expiry alerts |
trial-expiration |
— | Account trial lifecycle |
platform-retention |
— | Platform-wide retention |
alert-evaluation is the only key with a guaranteed recording floor, and that floor is daily. Every other job records a run only when it actually did work, so an old timestamp there is the healthy steady state. See Deployment and Operations.
Two readers deliberately do not derive from AccountScopedDataAccess, because they are documented platform-wide reads gated by [Authorize(Administrative, Read)], which only Administrator holds: BackgroundJobStatusReader here, and PlatformSyncActivityReader in Telemetry.
BackgroundJobStatusReader issues one query per distinct JobKey rather than a single grouped query, because GroupBy(...).Select(g => g.OrderByDescending(...).First()) is not reliably translatable — and EF InMemory would client-evaluate it, hiding the failure until production.
Manager owns the governed report catalog (app.reports). Each row carries Category, RequiredFeatureKey (null = global), ManagerOnly, SupportsPdf and SortOrder. getReports filters server-side by the caller's account features and role; Reporting re-enforces the metadata at execution time. See Reporting.
The catalog is re-seeded idempotently on every start by the DBInitializer, per-code upsert. Code is the source of truth for seeded metadata — a SuperAdministrator's edits to Description, Category, RequiredFeatureKey, ManagerOnly, SupportsPdf or SortOrder are transient and revert on the next restart. Only Active (enable/disable a report) persists. That is intentional: the catalog's shape ships with the deployment, and updateReport is for toggling availability, not re-authoring it.
-
ApplicationDbContextisNoTrackingby default. A row fetched for mutation must beAttached or its changes are silently discarded atSaveChangesAsync. EF InMemory defaults toTrackAll, so unit tests will not catch it. -
Common.Domain.Enumsmust NOT be added to Manager's InfrastructureGlobalUsings— itsTransporterTypecollides with theInfrastructure.Entitiestable entity of the same name. Import it per file there. - Manager → Router uses a 120 s client timeout: the manual-sync dispatch awaits the provider fetch.
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)