Compliance High Level Architecture
pltsci-mobile-hos-app pltsci-orion pltsci-asclepius

Compliance High Level Architecture

A driver's Hours of Service record is written by an Android app in the cab, stored and fanned out by a Laravel monolith, and recomputed and rendered by a Node/Vue monorepo. Orion is the hub; Asclepius attaches to it twice — once over HTTP as the portal's front end, once underneath it as compute on Orion's own database.

OFF SB D ON 00 03 06 09 12 15 18 21 24
One driver's day, 11 hours of drive time inside a 14-hour window. The HOS app draws this line, Orion stores every step of it, and Asclepius recomputes the clocks behind it and renders it back to the back office. Everything below is in service of this one graph.

The wiring

Four places the repos touch

Each repo owns a tier and speaks to the next one through exactly one kind of channel. Read the columns as three codebases and the rows as the four conversations between them: the portal handoff, the portal API, the device API, and the push back down to the cab.

HOS app Kotlin · Android · in-cab tablet Orion Laravel 11 · PHP 8.3 · ~50 packages Asclepius Nx + Lerna · Vue 2 · Node 20 Backbone SDK com.trimble.ttm.backbone.api motion · engine · ELD faults · user Web portal — Blade shell session · tenant · app.blade.php orion-clientweb orion-clientweb-app Vue 2 + Vuetify + Vuex SPA mounted at {host}/v# loads bundle {version}/static/js/app.js Duty-status engine state machines · timers · violations source of truth for driver data service/* — portal API orion-service-api · session auth reports · timers · log audit ServiceAPIClient orion-vue-common · $ServiceAPI *-client packages per domain XHR baseURL: '/service' Request queue (Room) framework:network-queue store & forward while offline api/* — device API DeviceAuthMiddleware · OAuth scopes orion-public-api · orion-hos hos-segment ETL 5 Lambdas · MySQL → Elasticsearch Apollo GraphQL behind Auth0 POST api/hos/device/v2/event SQS notify on every duty-status write PushActionWorker feature:push-action · WorkManager reconciles the log, no force logout MQTT publisher AWS IoT · orion- pushnotification Elasticsearch denormalized start/end segments per asset + user · pruned at 6 mo push DutyStatusChanged GraphQL: UDT Manager 2.0 Orion MySQL — one schema, two languages hos_event_log · hos_timers · hos_violations · hos_udt_suggestions · hos_annotations · activity_log Laravel migrations in orion-hos ⟷ TypeORM entities in asclepius-hos Eloquent TypeORM
HOS app writes Orion serves / pushes Asclepius reads / computes shared datastore, no API between
The three arrows on the left are the whole app-to-server relationship: events up over REST, commands down over MQTT. On the right, only the top two rows are HTTP. The bottom of the picture is the load-bearing one — Asclepius' Node workers do not call Orion's API, they open Orion's database. hos-timer is left off this row deliberately — its trigger is much narrower than "every duty-status write" and is drawn correctly in the next figure.

The repos

What each one is actually responsible for

pltsci-mobile-hos-app

HOS app

The ELD itself. Turns raw vehicle signals into legally defensible duty-status records, and keeps working with no network.

Stack
Kotlin, Compose + legacy Fragments, Hilt, Ktor, Room
Shape
Pragmatic Clean Architecture — :core:domain contracts, :infra:* horizontal tech, :feature:* vertical slices, :hos as composition root
Inputs
Trimble TTM Backbone SDK: motion, engine, current user, trip distance, ELD malfunctions, rulesets, trailers
Outputs
api/hos/device/v2/event, api/hos/v2/events, api/hos/device/v2/udt/revert, api/device/loads
Offline
Room-backed request queue with serial and parallel dispatchers and retry/back-off evaluators

pltsci-orion

Orion

The system of record and the multi-tenant boundary. Everything that matters is persisted here, and every other repo reads from it.

Stack
Laravel 11, PHP 8.3, ~50 path-repository packages under packages/orion-*
Shape
Providers load by path prefix — api/* device, service/* portal, internal/* service-to-service, everything else Blade
Tenancy
DeploymentServiceProvider registers first; DeploymentContext scopes every query
Fan-out
Writes hos_event_log → fires HosEventLogCreated → SQS message + AMQP broadcast, for every origin. The Redis job to hos-timer is separate — dispatched inline, only from the admin duty-status endpoint
Stores
MySQL, PostgreSQL/Timescale, Redis, RabbitMQ, Kinesis, AWS IoT

pltsci-asclepius

Asclepius

Two jobs in one monorepo: the portal's entire front end, and the Node compute that post-processes Orion's compliance data.

Stack
Nx + Lerna, Node 20.18.2, ~90 packages published to pltsci.jfrog.io
Front end
orion-clientweb-app (Vue 2 + Vuetify + Vuex), orion-vue-common, portal-menu-navigation, one *-client package per domain
hos-timer
Node worker that BLPOPs queues:hos_timer and runs XState machines for drive, break, on-duty, consecutive off-duty and last-reset. Fed only by OI/Tenant-Manager duty-status changes — never by ordinary tablet events
hos-segment
Five Lambdas — Denormalizer, Hydrator, Pruner, Migrator, API — doing MySQL → Elasticsearch ETL and serving GraphQL
Also here
hierarchy-service, entity-state-search-service, CDK stacks, shared MySQL/ES/Redis/logger libraries

One event, two very different fates

A duty-status write is not one pipeline — it's two, with different triggers

Every duty-status write — tablet or otherwise — feeds the history pipeline into Elasticsearch. Almost none of them touch hos-timer. That worker exists for exactly one caller: an OI or Tenant Manager integration changing a driver's status through Orion's admin API, and even then only when the driver isn't logged into the tablet to compute it themselves. These are the two lanes below — they share an origin only in the narrow case where lane B's write also happens to satisfy lane A.

LANE A — ANY DUTY-STATUS WRITE, ANY ORIGIN Driver or OI changes duty status tablet store()/store2() or admin changeDutyStatus() Orion writes the row INSERT hos_event_log fires HosEventLogCreated — every origin, no exceptions SQS · denormalizer gated: HOS_SEGMENT_ENABLED hos-segment ETL pairs start/end by asset + user, reads ardb replica Elasticsearch segment history, pruned at 6 months LANE B — ONLY AN OI / TENANT MANAGER CALL TRIGGERS THIS OI / Tenant Manager POST hos/duty_status OAuth: compliance-duty- status-admin-scope — not the tablet, not the SPA changeDutyStatus() writes hos_event_log too (feeds Lane A above) then dispatches HosTimerJob inline — not an event listener queues:hos_timer Redis · HosTimerJob hos-timer worker no-ops if the driver has an active tablet session else replays 14 days of events, writes hos_timers Portal timers via /service/* history via GraphQL Lane A gate: HOS_SEGMENT_ENABLED, event type duty-status, record status active or changed. Lane B gate: caller has compliance-duty-status-admin-scope, and the job only computes when hasActiveDevice(user_id) is false. Lane B's write also lands in hos_event_log, so it always feeds Lane A too — the reverse is never true.
origin Orion Asclepius queue, not a direct call
The mistake worth avoiding: reading Lane A's breadth onto Lane B. Every duty-status change reaches the searchable history in Elasticsearch. Almost none of them reach hos-timer — that path exists solely for the OI/Tenant-Manager case where the driver isn't at the tablet to compute their own clocks.

A fourth pipeline

ELD data transfer — getting a compliant log out of Orion

A "transfer" request — a driver hitting print at a roadside stop, a dispatcher pulling logs for an audit, or a Drivewyze e-inspection webhook — has one origin story in Orion but forks into two independent dispatches, gated by two separate feature flags that aren't mutually exclusive. One of those forks leaves this workspace entirely.

Transfer requested tablet print (EldController) portal dispatcher (service-api) Drivewyze e-inspection webhook Orion: EldLogTransfer saves the record, fires EldLogTransferInitiatedEvent — forks below, both can fire SQS · eld-transfer gated: isHosEldLogTransferSqsEnabled eld-transfer service external — not checked out in any local repo; contract only carries generate_only flag Artisan node:output → Redis queues:node_file_output asclepius-pdf CompiledHosReportSnapshoter: SOAP → FMCSA, S/MIME email, Transport Canada LDAP cert stores the output file in S3 PATCH eld_log_transfers/{guid}/results → fires EldLogFileSubmittedEvent Both branches can fire from one request: the SQS branch needs isHosEldLogTransferSqsEnabled(); the Redis branch needs isAsclepiusPdfEnabled() OR that flag being off. With both flags on, both run for the same transfer. Everything inside the dashed box is inferred from the message it receives, not from reading its code — it isn't checked out anywhere in this workspace.
request origin Orion Asclepius async queue / callback
The dashed-border box is the honest part of this figure: Orion's message contract to eld-transfer is readable, but the service itself isn't in ~/dev, so nothing past "here's the payload it receives" is confirmed. The solid path — Redis to asclepius-pdf — is fully traceable, and it's the one that actually files with FMCSA or Transport Canada, not just a PDF renderer despite the package name.

The seams

Nine contracts, only three of them HTTP

Every place the repos touch is a contract someone can break from the other side. Six of these have no schema, no versioning and no compiler enforcing them.

Seam The contract Breaks when
Device RESTHOS app → Orion POST api/hos/device/v2/event and siblings, behind DeviceAuthMiddleware; JSON event envelope A payload field changes shape before every tablet in the field has the new APK
Portal RESTAsclepius → Orion ServiceAPIClient with baseURL: '/service', riding the Blade shell's session and CSRF token A route moves between the service/* and api/* provider contexts
Bundle handoffOrion → Asclepius app.blade.php builds https://{portalDeploymentDomain}/{asclepiusVersion}/static/js/app.js app.asclepius_version and the S3 deploy drift apart — the portal renders a Blade skeleton with no Vue in it
Redis job queueOrion → hos-timer Dispatched inline from HosEventController::changeDutyStatus() only — the OAuth admin route used by OI / Tenant Manager integrations. Not wired to the event system, and never fired by an ordinary tablet duty-status post Someone assumes it fires for every duty-status change — it doesn't. It also no-ops if the driver has an active tablet session, so the payload keys (user_id, deploymentId, guid, event_timestamp_utc) only matter in the offline-driver case
SQS notifyOrion → hos-segment {guid, recordStatus} on a queue whose URL is read from SSM at /{env}/sqs/hos-segment/denormalizer-queue-url — fires for every duty-status hos_event_log write, tablet or admin-originated HOS_SEGMENT_ENABLED is unset in a deployment, or the listener's event-type filter narrows
Shared MySQLOrion ⟷ Asclepius Table columns. TypeORM entities in asclepius-hos mirror Laravel migrations in orion-hos by hand A migration lands in Orion without a matching entity change — the Node services fail at runtime, not at build
MQTT pushOrion → HOS app AWS IoT topic → com.pltsci.hos.DutyStatusChanged broadcast → a Map<String, String> payload the worker reads by key A payload key is renamed server-side; the handler silently returns a failure and the driver's log never reconciles
SQS eld-transferOrion → external service EldLogTransfer payload plus a generate_only flag, on a queue at SSM /{env}/sqs/eld-transfer/queue-url, fired by EldLogTransferInitiatedEvent Nobody in this workspace can verify the far side by reading code — only by reading what Orion sends it
Redis node_file_outputOrion → asclepius-pdf Artisan::call('node:output') dispatches NodeFileOutputJob directly — not through the event system — onto queues:node_file_output Assuming this and the SQS branch above are mutually exclusive — with both feature flags on, both fire for the same transfer

What to watch

Five things worth knowing before you plan work here

The database is the API

Neither hos-timer nor hos-segment calls an Orion endpoint. They open Orion's MySQL directly through TypeORM, which means a schema change in a PHP repo can break a TypeScript service in a different repo with no build failure anywhere. Any migration touching the HOS tables needs an Asclepius reviewer.

hos-timer is a narrow OI fallback, not a shadow calculator

The tablet calculates timers and violations locally and posts them to drivers/{user_id}/timers — that's the normal path for every driver-initiated duty status change, and hos-timer plays no part in it. hos-timer is triggered only when an OI/Tenant Manager integration calls the admin POST hos/duty_status endpoint, and even then it only computes anything if the driver has no active tablet session — the job checks hasActiveDevice() and no-ops if that's true. It exists to keep the back office correct when there's no app around to do the math, not to double-check the app.

The tablet is authoritative and often offline

The app is the legal source of truth and every write is store-and-forward through a Room-backed queue. Server-side changes can't assume the device has seen them — that is exactly why the OI duty-status change became a push action that reconciles the local log rather than a forced logout.

Asclepius is two products in one repo

The portal front end and the compliance microservices share a release pipeline, an Nx graph and a tag system (tag:release:orion, tag:release:hos-segment) but almost nothing else. "An Asclepius change" can mean a Vue component or a Lambda that owns six months of compliance history; the tags are how you tell which.

eld-transfer's internals are outside this workspace

The SQS queue Orion publishes to has a real, readable contract — payload shape, feature-flag gating, the SSM path it resolves from. The service consuming it isn't checked out anywhere in ~/dev, so treat the ELD Data Transfer section above as confirmed only up to "here's what Orion sends it." Whether it still does anything with that message, or has been fully superseded by asclepius-pdf's direct FMCSA/Transport Canada submission, needs a look at that repo or its owning team — not this diagram.

Drawn from the checkouts in ~/dev: pltsci-orion (Laravel 11, packages/orion-*, orion-public-api/src/v2/routes/api/hos.php, orion-hos/src/app/Listeners/HosSegmentDispatchSqsListener.php, orion-hos/src/app/Listeners/EldLogTransferDispatchSqsListener.php, orion-public-api/.../Controllers/Hos/EldController.php, orion-service-api/.../Controllers/Eld/EldLogTransferController.php), pltsci-asclepius (packages/hos-timer/main.ts, packages/asclepius-hos-segment/PRR.md, packages/orion-vue-common, packages/asclepius-pdf/Snapshoter/CompiledHosReportSnapshoter.ts), and pltsci-mobile-hos-app / hos — the same repo, checked out twice locally.

Not shown: Orion's RabbitMQ and Kinesis event broadcasting, the Instinct feature module, and the hierarchy and entity-state-search services, none of which sit on the HOS path.