Skip to content

Manager

Sergio Hernandez edited this page Jul 24, 2026 · 1 revision

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


What it owns

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.


Accounts, features and multi-tenancy

An Account is the tenant boundary. Every tenant-owned row across the platform carries its AccountId, and AccountScopeBehavior enforces it centrally (see Architecture).

Account features

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 notifications gates. 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 IFeatureFlagService silently ignores every [RequireFeature] attribute — Common's default is fail-open. See Architecture.

Account status

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.


Documents

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.


Workforce

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.


Alerts and notifications

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
Loading

Evaluation

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

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.

Role recipients are read-time fan-out

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.

Dispatch

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
Email MailKit / SMTP, AppSettings:Smtp
Webhook HMAC-SHA256 X-TrackHub-Signature, 10 s timeout, per-rule URL and secret in ConfigurationJson
WhatsApp 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.


Localization

Localized text lives in exactly two places — the recipient-facing resources, and the UI.

  1. 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_templates holds account-authored overrides only, and the templates panel's read-only "defaults" are synthesized from the resources at query time (NotificationTemplateReader.BuiltInKeys, id Guid.Empty).
  2. Never in code. Default subject and body texts live in Infrastructure/ManagerDB/Notifications/NotificationDefaultMessages.resx (plus an .es.resx satellite), read through Common.Domain.Localization.ResourceLocalizer — the same primitive the AuthorityServer login messages use.
  3. The locale comes from the recipient (NotificationLocaleResolver): a User recipient's own UserSettings.Language, normalized (es-COes) → the rule's ConfigurationJson.localeen. Contacts and webhooks have no user identity, so they use the rule fallback.
  4. In-app deliveries are never rendered server-side. The feed returns event metadata; the portal localizes through its i18n layer.
  5. 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.

Platform announcements

PlatformAnnouncement (app.platform_announcements) backs the maintenance banner on the public status page.

  • Administered through Administrator-only GraphQL (Administrative resource).
  • 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.


Background jobs

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.


Unscoped readers

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.


Report catalog

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.


Gotchas

  • ApplicationDbContext is NoTracking by default. A row fetched for mutation must be Attached or its changes are silently discarded at SaveChangesAsync. EF InMemory defaults to TrackAll, so unit tests will not catch it.
  • Common.Domain.Enums must NOT be added to Manager's Infrastructure GlobalUsings — its TransporterType collides with the Infrastructure.Entities table 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.

Clone this wiki locally