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.
The wiring
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-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
pltsci-mobile-hos-app
The ELD itself. Turns raw vehicle signals into legally defensible duty-status records, and keeps working with no network.
:core:domain contracts, :infra:* horizontal tech, :feature:* vertical slices, :hos as composition rootapi/hos/device/v2/event, api/hos/v2/events, api/hos/device/v2/udt/revert, api/device/loadspltsci-orion
The system of record and the multi-tenant boundary. Everything that matters is persisted here, and every other repo reads from it.
packages/orion-*api/* device, service/* portal, internal/* service-to-service, everything else BladeDeploymentServiceProvider registers first; DeploymentContext scopes every queryhos_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 endpointpltsci-asclepius
Two jobs in one monorepo: the portal's entire front end, and the Node compute that post-processes Orion's compliance data.
pltsci.jfrog.ioorion-clientweb-app (Vue 2 + Vuetify + Vuex), orion-vue-common, portal-menu-navigation, one *-client package per domainqueues: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 eventshierarchy-service, entity-state-search-service, CDK stacks, shared MySQL/ES/Redis/logger librariesOne event, two very different fates
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.
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
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.
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
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
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.
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 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.
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.
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.