Current State, Future Paths & Technical Reference
Core module - compliance, documents, assets
User management, roles, activity logs
Brokers, factoring, load board
QuickBooks, invoicing, settlements
Daily operations management
Task management, calendar
Backend: /root/truxflow-hosting/app/backend/
Frontend: /root/truxflow-hosting/app/frontend/
Scripts: /root/truxflow-hosting/scripts/
./scripts/deploy-all.sh
./scripts/deploy-all.sh backend
./scripts/deploy-all.sh frontend
pm2 logs truxflow-staging
pm2 restart truxflow-staging --update-env
pm2 restart truxflow-webhook
cd backend && npx prisma studio
npx prisma migrate deploy
npx prisma migrate status
weLinkCargoEnterprise/TruxFlow
weLinkCargoEnterprise/TruxFlow-Front
Auto-deploy on push to main branch
Carriers: 18 | Drivers: 235
Trucks: 199 | Trailers: 74
Documents: 1,900+ total
Complete TruxFlow quickly for WeLink's internal use. Keep current flat structure, add modules incrementally, refactor later when scaling to external customers.
modules/safety/modules/admin/modules/dispatch/. Load / load-board is build-new work still pending.Consider refactoring to modular structure when ANY of these occur:
| Trigger | Why It Matters |
|---|---|
| First external customer signs up | Multi-tenant isolation becomes mandatory |
| Team grows to 3+ developers | Developers stepping on each other in flat structure |
| Controllers folder has 25+ files | Navigation and mental overhead too high |
| Need different access per module | Current RBAC can't scope by business domain |
| Subdomain/white-label request | Requires account-level tenant model |
Build multi-tenant architecture from the start. Modular monolith structure with clean boundaries. Ready for external customers without major refactoring.
(req, res) signatures preserved to avoid Safety's context-rewrite regressions.Amendments on top of Path B that make the modular SaaS structure MCP-ready from day one β instead of bolting agent surfaces on later. Tracked as Phases 0β4. Baseline: v0.1.0-welink-mvp on both backend and frontend repos.
These are the gaps between the original Path B proposal and what MCP readiness requires. Each becomes a blueprint amendment plus an implementation task inside the phase it belongs to.
| # | Gap | Lands in |
|---|---|---|
| 1 | Module boundaries as MCP tool surfaces. Each module exposes one tool-entrypoint file enumerating MCP-callable actions. | Phase 1 |
| 2 | Context object standardization. All service functions take Context ({ account, user, carrierAccess, correlationId }) as first arg β MCP can forge context identically to HTTP. | Phase 2 |
| 3 | Zod schemas as contract. MCP tool schemas reuse the same Zod input/output schemas the REST handlers validate against. Single source of truth. | Phase 2 |
| 4 | Service / transport split. Services know nothing about Express, HTTP, or MCP. Transports (REST + MCP) are thin adapters. | Phase 1 |
| 5 | Account / CarrierAccess before MCP. Multi-tenant scoping must land first, or agents leak cross-tenant data. | Phase 0 β |
| 6 | Idempotency keys on write tools. Agents retry more than humans β every mutating tool accepts an idempotency key. | Phase 2 |
| 7 | Read vs. write tool separation. Readonly and mutating tools live in different namespaces so per-agent permissions are simple. | Phase 3 |
| 8 | Audit log on every tool call. Who (human or agent), what, when, before/after state. | Phase 2 |
| 9 | Rate limiting at transport boundary. REST has its own, MCP needs its own β agents burst differently. | Phase 3 |
| 10 | Error taxonomy. Structured error codes (not free-text) so agents can recover or escalate sensibly. | Phase 2 |
v0.1.0-welink-mvp as reference. Introduce TenantAccount + CarrierAccess multi-tenant model so all later phases have a tenant to scope to. No user-visible API change yet β carrierScope.js remains authoritative.modules/<name>/{service,schema,transport-rest}. Still fully REST-only. Tests green throughout.Context through every service. Zod as cross-transport contract. Error taxonomy. Audit log + idempotency-key support later in the phase.88bfcc6 + ef8e9d8). Slice C (Safety) shipped Apr 23, 2026 late evening β all 8 Safety services on (context, input) + ServiceError throws + errorMiddleware mounted in server.js; modules/safety/tools.js populated with 15 read-only MCP tool entries. Commits 28ace31 (driver) + 492faf4 (remaining 7 + 2 device Rule 9 fixes) + a2390b2 (allowlist unblock). Slices D (Admin) + E (Dispatch) + Preferences migration queued.modules/<name>/transport-mcp wrapping the same services. Read tools first, write tools after per-module review. Per-tenant auth tokens. MCP-boundary rate limiting.TenantAccount (+ TenantAccountType enum), CarrierAccess (+ AccessLevel enum), nullable tenancy columns on Carrier and User. Additive only β no drops, no renames. Migration 20260421182024_phase0_tenant_account.FULL, all 5 modules), 12 users attached via tenantAccountId. Idempotent script kept for re-runs.accountScope middlewareaccountScope.js in parallel with carrierScope.js. Plan controller cutover module-by-module. No user-visible API change until cutover. Also: update the create-carrier controller to populate ownedByTenantAccountId / createdByTenantAccountId on new carriers (currently NULL on new rows β acceptable pre-MCP, must fix before Phase 3).organizationIdOrganization remains source of truth for scoping until then.| Path | Purpose |
|---|---|
prisma/schema.prisma | TenantAccount, CarrierAccess, enums, Carrier/User deltas |
prisma/migrations/20260421182024_phase0_tenant_account/migration.sql | Applied SQL (2 enums, 2 tables, 3 nullable columns, 5 FKs, 2 indexes) |
prisma/_phase0_draft/README.md | Rollback plan + apply sequence |
prisma/_phase0_draft/backfill.cjs | Idempotent backfill script (re-run safe) |
/root/backups/pre-phase0-20260421181841..dump | Pre-Phase-0 pg_dump (278 KB, custom format) |
Phase 0 is foundation only β it intentionally does not change runtime behavior. carrierScope.js remains the active tenant filter; tenantAccountId on User is set but not read by any controller; organizationId is still the source of truth. Those are Phase 1+ work β no controller should consult the new tenancy fields without an explicit sequenced rollout (Steps 3β5).
truxflow-mcp process?Accounts (49-row expense-category table) to ExpenseCategory in a separate PR β kept distinct from TenantAccount to avoid naming collision.truxflow/secrets-rotation.md.Safety module (Apr 21, 2026)
4299f98 β feat(safety): add extraction guardrails and restore endpoint parity (backend, 15 files)
662e7b1 β fix(safety): address Phase 1 edit/view regressions and user carrier display (frontend, 5 files)
Apr 22 hotfix sprint β 13 commits closing regressions from the Safety extraction: input sanitization, $transaction semantics, P2002 error translation, Airtable field-name quirks, tolerant singleSelect normalizer, per-entity sync lock, tolerant date parser. Full incident log captured.
Admin module (Apr 22, 2026)
7c77d74 β feat(admin): extract user + role controllers into modules/admin/ (backend, 12 files, +1778/-7)
Dispatch module β broker + factoring (Apr 22, 2026)
bfa71ce β feat(dispatch): extract broker + factoring into modules/dispatch/ (backend, 11 files, +1834/-5). Zero-data extraction β all four dispatch tables (Broker, BrokerAssignment, Factoring, FactoringAssignment) had 0 rows at extraction time, so this was a clean relocate with no data-integrity surface.
Third module carved out the same day as Admin. Same discipline: literal code motion, (req, res) signatures preserved. Factoring (1026 LoC) assembled via cat+sed to guarantee a byte-identical body β only the import paths differ from the legacy controller.
| What moved | From | To |
|---|---|---|
| Broker CRUD + bulk assignment + terminate | controllers/brokerController.js (7 endpoints) | modules/dispatch/service/broker.service.js + transport-rest/broker.routes.js |
| Factoring CRUD + count + expiring-contracts + single/bulk assignment + terminate + carrier history | controllers/factoringController.js (13 endpoints) | modules/dispatch/service/factoring.service.js + transport-rest/factoring.routes.js |
| Cross-cutting auth plumbing (auth.js, accountScope, computePermissions, revokeSession) | middleware/ + utils/ | Unchanged β shared by every module |
The upcoming Load / load-board work will NOT carry an Airtable sync mapper (unlike Safety's driver/truck/trailer sync). Dispatch becomes the single source of truth for loads once the Load schema lands. Broker + Factoring controllers never had Airtable hooks, so nothing to strip during this extraction. Safety's Recruiting β Dispatch sync for driver/truck/trailer docs is orthogonal and stays.
| Test | Result |
|---|---|
| Row counts before extraction (Broker / BrokerAssignment / Factoring / FactoringAssignment) | β 0 / 0 / 0 / 0 β zero-data extraction |
Route diff routes/brokerRoutes.js β modules/dispatch/transport-rest/broker.routes.js | β Empty |
Route diff routes/factoringRoutes.js β modules/dispatch/transport-rest/factoring.routes.js | β Empty |
| 20 dispatch endpoints β unauthenticated smoke | β All 401 (routed, auth-gated) |
| Neighboring routes (Safety, Admin, carriers, dashboard) | β Unaffected β all 401 |
npm run lint:scope | β No new un-scoped queries |
| 9-item regression checklist (both services) | β 9/9 preserved |
Dead-code carry: legacy factoringController.js declared listCarrierFactoringAssignments but never wired a route for it. Preserved in the extracted service (Rule 4 β don't rewrite during extraction). Phase 2 cleanup can drop it.
Second module carved out. Literal code motion β kept (req, res) signatures because Safety's (context, params) rewrite during extraction was the source of ~8 of the Apr 22 hotfix-sprint regressions. Phase 2 will do signature unification across Safety + Admin at once with Zod as the boundary.
| What moved | From | To |
|---|---|---|
| Role CRUD + presets + permissions shim | controllers/rolesController.js (7 endpoints) | modules/admin/service/role.service.js + transport-rest/role.routes.js |
| User CRUD + Phase 1.5 sub-routes (roles + carrier-restrictions) | controllers/userController.js (12 endpoints) | modules/admin/service/user.service.js + transport-rest/user.routes.js |
Inline requireAdmin gate (duplicated in both old route files) | Inline | middleware/requireAdmin.js (new, shared) |
| Cross-cutting auth plumbing (auth.js, requireDepartment, requireAction, accountScope, computePermissions, tokenVersionCache, revokeSession) | middleware/ + utils/ | Unchanged β infrastructure, not Admin domain; used by every module |
Every regression the Safety extraction silently introduced (13 traced in the Apr 22 hotfix sprint) is represented in feedback/phase1-extraction-regressions.md. The Admin extraction was walked through the 9-item checklist for both services β 9/9 preserved.
| # | Checklist item | Preserved how |
|---|---|---|
| 1 | Input sanitization | Explicit destructure throughout (no ...body spread); phone: phone || null kept; no date fields on Role or User payloads. |
| 2 | Immutable field stripping | organizationId, tenantAccountId read from req.user, never from body; id/createdAt/updatedAt never referenced. |
| 3 | Transaction semantics | createUser + replaceUserRoles + addUserRole + removeUserRole + replaceUserCarrierRestrictions + updateRole β every $tx boundary preserved byte-for-byte. handoffSuperAdmin runs inside tx. revokeUserSessions(target.id, tx) inside same tx as the mutation. |
| 4 | Upsert idempotency | addUserRole keeps userRoleAssignment.upsert on composite key. |
| 5 | Error propagation | No return null; all errors logged + 5xx returned. |
| 6 | Required-field validation | 400 on missing firstName/lastName/email/password preserved; validateRoleIds/validateCarrierIds return structured errors. |
| 7 | Prisma error translation | createUser: P2002 β 409 with fields; createRole: P2002 β 409; ROLE_LIMIT_REACHED / ROLE_IN_USE / SELF_DEMOTION_BLOCKED / SUPER_ADMIN_REQUIRED all preserved. |
| 8 | Response envelope | { user } / { users } / { message } / { departments } / bare role array / { carrierIds, carriers } β all identical to legacy. |
| 9 | Phantom flags | No feature flags; permissionsChanged is computed-diff, not phantom. |
User browser-walked the extracted surface on staging. Plus one end-to-end save on a real user (Moe Abraham) during testing.
| Test | Result |
|---|---|
Route diff routes/userRoutes.js β modules/admin/transport-rest/user.routes.js | β Empty |
Route diff routes/rolesRoutes.js β modules/admin/transport-rest/role.routes.js | β Empty |
| 19 admin endpoints β unauthenticated smoke | β All 401 (routed, auth-gated as expected) |
| Neighboring routes (Safety, carriers, dashboard, docs, organization, factoring, broker) | β Unaffected β all 401 |
npm run lint:scope | β No new un-scoped queries |
/admin/roles loads with preset + custom + cap counter | β |
| Create custom role β counter increments | β |
| Edit Dispatcher preset β edit icon absent (client gate) | β |
| Assign user to custom role β counter reacts | β
(via POST /users/:id/roles/:roleId) |
| Delete role while in use β blocked | β
409 ROLE_IN_USE preserved |
| Delete unused role | β |
/admin/users renders with role chips | β |
| Live save on Moe Abraham (profile + roles + carriers) | β
PATCH /users/:id 200 (25ms), PUT /users/:id/roles 200 (27ms), PUT /users/:id/carrier-restrictions 200 (27ms) β tx committed, revoke fired |
| Re-login as Moe β Safety scope + one-carrier restriction intact | β
(JWT replay + listUserRoles + carrier restrictions all extracted) |
Not caused by the Admin extraction. In frontend/src/components/forms/UserForm.jsx, dirty-state detection misses several field changes β Save button stays disabled when the user edits firstName, toggles the Restricted/Unrestricted carrier radio, or removes a role chip. Typing digits into Phone does activate Save. Backend behavior is correct when Save eventually fires. Logged for follow-up; pre-dates the Admin extraction.
Four prevention measures to catch the kind of regressions that slipped through the initial Safety extraction (missing endpoints, shape mismatches, verb drift). All four run against modules/safety/* and are ready to apply to the next module extraction.
| Tool | Purpose | Location |
|---|---|---|
| Request logger | Surface every 4xx/5xx in pm2 logs with method, path, status, latency β drops the guesswork when the frontend reports a broken call. | server.js (inline middleware) |
| Route extractor + parity snapshot | Regex-pulls every registered route from old & new routers, sorted, diff-friendly. Current state + regen instructions captured per entity. | scripts/extract-routes.js, modules/safety/ENDPOINT_PARITY.md |
| Response-shape contract tests | Declarative list of endpoint β expected envelope (list / { data } / { message, data } / bare entity). Runner logs in, walks the contracts, exits non-zero on drift. | modules/safety/contracts.js, scripts/check-response-shapes.js |
| Extraction rules doc | Four codified rules: preserve endpoint surface, preserve HTTP verbs, preserve response envelopes, don't rewrite query logic during extraction. | modules/safety/EXTRACTION_RULES.md |
| Entity | What was missing / drifted | Fix |
|---|---|---|
| Truck | GET /assignments and GET /statuses/availability not registered in new router | Restored both routes + imports in transport-rest/truck.routes.js |
| Trailer | getTrailer dropped the trailerAssignments include on nested truck, so the overview couldn't render sibling trailers | Added trailerAssignments include in service/trailer.service.js |
| Contractor / Device / Driver | Minor include, permission, and response-shape drift | Aligned services + routes with pre-extraction behavior |
| Page | Issue | Fix |
|---|---|---|
TruckProfileV2.jsx | Close nav targeted /safety/units (undefined route) | Route changed to /safety/trucks |
CarrierProfileV2.jsx | Edit modal framed lg β cramped vs. other entities | Bumped to xl to match Driver/Trailer/Truck |
DriverProfileV2.jsx | Edit button rendered DriversFormV2 (still hardening) | Swapped back to the stable DriversForm pending V2 completion |
Drivers.jsx | Status dropdown offered INACTIVE and OUT_OF_SERVICE β backend enum rejects both with VALIDATION_ERROR | List trimmed to real enum: ACTIVE, ONBOARDING, TERMINATED |
Users.jsx | User cards didn't render assigned carriers even though the API returns them | Added Carriers display (truncates to 3 + "+N more") |
Same HTTP surface, reorganized into the Path B modules/<name>/{service,transport-rest} layout. Old files remain in place during Phase 1 (still wired via server.js), but every Safety endpoint is now served by the new module.
| Old (flat) | β | New (modular) |
|---|---|---|
controllers/driverController.js | β | modules/safety/service/driver.service.js |
routes/driversRoutes.js | β | modules/safety/transport-rest/driver.routes.js |
controllers/trucksController.js | β | modules/safety/service/truck.service.js |
routes/trucksRoutes.js | β | modules/safety/transport-rest/truck.routes.js |
controllers/trailersController.js | β | modules/safety/service/trailer.service.js |
routes/trailersRoutes.js | β | modules/safety/transport-rest/trailer.routes.js |
controllers/devicesController.js | β | modules/safety/service/device.service.js + deviceCategory.service.js |
routes/devicesRoutes.js | β | modules/safety/transport-rest/device.routes.js + deviceCategory.routes.js (sub-router mount /categories) |
controllers/contractorsController.js | β | modules/safety/service/contractor.service.js + vendorType.service.js |
routes/contractorsRoutes.js | β | modules/safety/transport-rest/contractor.routes.js + vendorType.routes.js (sub-router mount /subtypes) |
(empty) routes/fuelRoutes.js | β | modules/safety/service/fuelCard.service.js + transport-rest/fuelCard.routes.js (net-new; no parity concern) |
Generated via scripts/extract-routes.js. Empty diff = parity. β same in both, β new in modular, β removed (none β all restored). Sub-router mounts expanded inline below each parent.
| Method | Path | Old | New |
|---|---|---|---|
| GET | / | β | β |
| GET | /driversCount | β | β |
| GET | /endorsements | β | β |
| GET | /status | β | β |
| GET | /:id | β | β |
| GET | /:id/history | β | β |
| GET | /:id/assignmentHistory | β | β |
| GET | /:id/available-carriers | β | β |
| POST | / | β | β |
| POST | /:id/duplicate | β | β |
| POST | /:id/assignTruck | β | β |
| POST | /:id/unlinkTrucks | β | β |
| PATCH | /:id | β | β |
| PATCH | /:id/status | β | β |
| DELETE | /:id | β | β |
| Method | Path | Old | New | Note |
|---|---|---|---|---|
| GET | / | β | β | |
| GET | /count | β | β | |
| GET | /accounts | β | β | |
| GET | /assignments | β | β | restored Apr 21 |
| GET | /dispatchStatuses | β | β | |
| GET | /operationStatuses | β | β | |
| GET | /safetyStatuses | β | β | |
| GET | /statuses | β | β | |
| GET | /statuses/availability | β | β | restored Apr 21 |
| GET | /:id | β | β | |
| GET | /:id/assignmentHistory | β | β | |
| GET | /:id/deviceAssignmentHistory | β | β | |
| GET | /:id/trailerAssignmentHistory | β | β | |
| POST | / | β | β | |
| POST | /:id/devices | β | β | |
| POST | /:id/drivers | β | β | |
| POST | /:id/trailers | β | β | |
| PATCH | /:id | β | β | |
| PATCH | /:id/status | β | β | |
| PATCH | /:id/safetyStatus | β | β | |
| PATCH | /:id/devices/:deviceId | β | β | |
| PATCH | /:id/trailers/:trailerId | β | β | |
| PATCH | /:id/unlinkDrivers | β | β | |
| DELETE | /:id | β | β |
| Method | Path | Old | New | Note |
|---|---|---|---|---|
| GET | / | β | β | |
| GET | /count | β | β | |
| GET | /:id | β | β | nested trailerAssignments include restored Apr 21 |
| GET | /:id/assignmentHistory | β | β | |
| POST | / | β | β | |
| POST | /:trailerId/assign | β | β | |
| PUT | /:id | β | β | |
| PATCH | /:id | β | β | new alias (shared handler) |
| PATCH | /:id/operationStatus | β | β | |
| PATCH | /:id/safetyStatus | β | β | |
| PATCH | /assignments/:assignmentId/unassign | β | β | |
| DELETE | /:id | β | β |
| Method | Path | Old | New | Note |
|---|---|---|---|---|
| GET | / | β | β | |
| GET | /count | β | β | |
| GET | /:id | β | β | |
| GET | /:id/assignments | β | β | |
| POST | / | β | β | |
| POST | /import | β | β | |
| POST | /:deviceId/assign | β | β | |
| POST | /:id/categories | β | β | |
| DELETE | /:id | β | β | |
| DELETE | /:id/categories | β | β | |
| PUT | /:id | β | β | |
| PATCH | /:id | β | β | new alias |
| PUT | /assignments/:assignmentId/unassign | β | β | |
Sub-router USE /categories β deviceCategory.routes.js | ||||
| GET | /categories/all | β | β | |
| POST | /categories | β | β | |
| PUT | /categories/:categoryId | β | β | |
| DELETE | /categories/:categoryId | β | β | |
| Method | Path | Old | New | Note |
|---|---|---|---|---|
| GET | / | β | β | |
| GET | /count | β | β | |
| GET | /:id | β | β | |
| GET | /carrier/:carrierId | β | β | |
| POST | / | β | β | |
| PUT | /:id | β | β | |
| PATCH | /:id | β | β | new alias |
| PATCH | /:id/status | β | β | |
| DELETE | /:id | β | β | |
Sub-router USE /subtypes β vendorType.routes.js | ||||
| GET | /subtypes | β | β | |
| GET | /subtypes/:id | β | β | |
| POST | /subtypes | β | β | |
| PUT | /subtypes/:id | β | β | |
| DELETE | /subtypes/:id | β | β | |
| Method | Path | Old | New |
|---|---|---|---|
| GET | / | β | β |
| GET | /:id | β | β |
| GET | /:id/history | β | β |
| POST | / | β | β |
| POST | /:fuelCardId/assignTruck | β | β |
| POST | /:fuelCardId/unassignTruck | β | β |
| PATCH | /:id | β | β |
| DELETE | /:id | β | β |
Remaining extraction: Organization module (largest legacy surface β carriers, credentials, documents, org profile). Last sizable carve; likely lands after Phase 2 Slices C/D/E so it arrives on the new pattern.
Build-new Dispatch work: Load schema + lifecycle + Load Board + Rate-Con / POD / BOL + Safety availability integration. Design pending; no Airtable sync (per ADR). Will use Phase 2 signatures (Context + Zod) from the start.
Cleanup debt: unmounted-but-on-disk legacy files. As of Apr 22: controllers/{user,roles,broker,factoring}Controller.js + matching routes/*.js; Safety's legacy files since Apr 21. Safe to delete once Organization lands.
Phase 2 prep has its own section below β Slices A+B (scaffolding) landed, Slices C/D/E (service migration) queued.
88bfcc6 β feat(shared): Phase 2 prep scaffolding β Context, ServiceError, validate (5 files, +328 LoC, pure additive)
ef8e9d8 β feat(phase2): install zod + exemplar schemas per module (5 files, +150 LoC)
Neither commit changed any module's runtime behavior β scaffolding is loaded but unconsumed. Services + routes still use their pre-Phase-2 signatures until Slice C starts.
shared/ β Cross-Module Building Blocks| File | Purpose | Gap Closed |
|---|---|---|
shared/errors.js | ServiceError class + ErrorCodes taxonomy (VALIDATION_ERROR, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, UNIQUE_VIOLATION, FK_VIOLATION, ROLE_LIMIT_REACHED, ROLE_IN_USE, SUPER_ADMIN_REQUIRED, SELF_DEMOTION_BLOCKED, SESSION_REVOKED, INTERNAL) with default HTTP status mapping. translatePrismaError() for P2002/P2003/P2025. | #10 |
shared/context.js | Context shape { user, tenant, carrierAccess, correlationId, now } + buildContextFromRequest(req) + test helper. | #2 |
shared/handleError.js | Express errorMiddleware + toErrorBody. Response envelope { message, error, code, details? } on non-2xx. | #10 |
shared/validate.js | parseInput(schema, data) Zod helper throwing ServiceError(VALIDATION_ERROR) with structured issue details. | #3 |
shared/index.js | Public re-exports so domain code has one import path. | β |
One exemplar schema per module establishes the pattern. Full per-endpoint schemas grow inline with each module's Slice C/D/E migration β lazy expansion rather than big-bang schema write.
| Module | Exemplar | Covers |
|---|---|---|
| Safety | modules/safety/schema/driver.schema.js | driverIdParams, listDriversInput (coerce + defaults), createDriverInput (passthrough), updateDriverInput (partial) |
| Admin | modules/admin/schema/role.schema.js | roleIdParams, rolePayload (department + action enum validation), create/update inputs |
| Dispatch | modules/dispatch/schema/broker.schema.js | brokerIdParams, listBrokersInput, create/update inputs, bulkAssignBrokerInput |
npm audit note: 35 pre-existing vulnerabilities (5 moderate, 29 high, 1 critical) surfaced during npm install zod. Unrelated to zod (4.3.6 is clean); project-wide dependency debt logged for a dedicated triage pass.
| Slice | Scope | Risk | Est. |
|---|---|---|---|
| β Slice C β Safety | Migrated 8 services + endpoints to (context, input) + ServiceError. modules/safety/tools.js populated (15 read tools). errorMiddleware mounted. Friendly per-entity P2002 β 409 preserved. Two Apr 21 device-extraction Rule 9 gaps fixed alongside (createDevice + updateDevice categoryIds/carrierId). | Shipped 2026-04-23 | 28ace31, 492faf4 |
| Slice D β Admin | Bigger rewrite: (req, res) β (context, input). Preserve every Phase 1.5 behavior (createUser $tx, handoffSuperAdmin, revokeUserSessions, self-demotion guard, response envelope shape). | Medium | 2-3 hr |
| Slice E β Dispatch | Same rewrite for broker + factoring services. | Low (zero-data tables) | 1-2 hr |
Browser-test checkpoint after each slice. Slice C can be narrowed to driver-only first if the full-Safety migration feels too broad at restart. See session-2026-04-22-phase2-prep.md for the restart pre-flight.
Three-commit progression: 28ace31 (driver narrow start, browser-tested) β a2390b2 (deploy-gate unblock for pre-existing getExpiringDocuments un-scoped query) β 492faf4 (remaining 7 services + 2 device Rule 9 fixes + 15-tool tools.js). Browser smoke green across drivers / trucks / trailers / devices / device categories / contractors / vendor types / carriers. Phase 2 scaffolding now consumed by every Safety service. Slice D (Admin) is the next move β see session-2026-04-23-phase2-slice-c.md for the full log + Slice D pre-flight.
Merged feat/permission-model-v1 β main on both backend + frontend; tagged v0.2.0-permission-model; auto-deployed to staging. Lands between Phase 1 (Safety extraction) and Phase 2 (Context/Audit).
Validated end-to-end in production: admin Adam sees all modules + /admin/roles UI with preset + custom CRUD. Safety Officer Moe (with a custom "Operation - Compliance" role, one-carrier restriction) sees only the Safety + Operations modules, scoped to that one carrier across every list, form, and dropdown.
Scope delivered: 11 commits for the core launch + 4 polish passes on top β Users page brand rewrite, list/card toggle w/ filters + sortable columns, UserForm v2 (single-pass create), Single-Super-Admin invariant w/ handoff. Total 24 commits across both repos.
Phase 0 tenant scoping answers "which carriers does this tenant have?". It cannot answer "of those carriers, which has this specific user been allowed to see?". April 21 smoke test exposed the gap: revoking one carrier from a user still showed all carriers in the UI. The legacy User.carriers relation exists but is not read by any middleware.
Phase 1.5 collapses three pending concerns into one pass:
| Principle | What it means |
|---|---|
| Stateless at request time | Permissions computed once at login, embedded in JWT. Every protected request reads from the token β no DB round-trip for authz checks. |
| JWT-driven frontend | Sidebar, dropdowns, action buttons all read from decoded JWT. Zero per-button API calls. Server stays cool, UX stays fast. |
| Immediate revocation | Permission change bumps user's tokenVersion + evicts cached value. Old JWT fails on next request, forces re-login. |
| Preset + custom, same table | System presets live in the same RoleDefinition table as tenant-custom roles, distinguished by tenantAccountId IS NULL. One query covers both. |
| Ship simple, leave hooks | Entity-level fine-grained perms (e.g. "view drivers but not trucks within Safety") deferred. Schema field entities: string[] reserved β no later migration churn. |
{
"sub": "user-abc",
"tenantAccountId": "xyz",
"tenantType": "SERVICE_PROVIDER",
"isSuperAdmin": false,
"isAdmin": false,
"allowedCarrierIds": ["c1"], // one-carrier restriction
"departmentsMap": { // Phase 1.5 object
"SAFETY": { "role": "Safety Officer", "actions": ["view","edit","delete"] },
"TASKS": { "role": "Safety Officer", "actions": ["view","edit","delete"] },
"SETUP": { "role": "Safety Officer", "actions": ["view","edit","delete"] }
},
"departments": ["SAFETY"], // legacy array (back-compat)
"tokenVersion": 11
}
allowedCarrierIds is already the intersection of tenant.CarrierAccess.carrierIds β© user.carrierRestrictions.ids β computed at login, no request-time math. Frontend reads user.departmentsMap (preferred) or falls back to user.departments when it's the object shape (org-type sessions).
| Change | Details |
|---|---|
Rename User.carriers β carrierRestrictions | Zero data migration β relation reused. Name now reflects semantics ("allow-list subset of tenant carriers"). |
NEW RoleDefinition | id, tenantAccountId?, name, isSystemPreset, isSuperAdmin, isAdmin, departmentScope[], actions[], entities[] (reserved), createdAt. tenantAccountId IS NULL β system preset. |
NEW UserRoleAssignment | Many-to-many join. A user can hold multiple roles (e.g. Safety Officer + Accountant). |
Reused User.tokenVersion | Already exists. Bumped on any permission change to invalidate outstanding JWTs. |
Deprecated (not removed) Role, Permission, RolePermission | Legacy org-scoped tables. Left in place for Phase 2 cleanup, no new reads. |
| Preset | isSuperAdmin | isAdmin | Department Scope | Actions |
|---|---|---|---|---|
| Super Admin | β | β | all 7 departments | view, edit, delete |
| Admin | β | β | all 7 departments | view, edit, delete |
| Safety Officer | β | β | SAFETY, TASKS, SETUP | view, edit, delete |
| Accountant | β | β | ACCOUNTING, TASKS, SETUP | view, edit |
| Dispatcher | β | β | DISPATCH, TASKS, SETUP | view, edit, delete |
All non-admin presets include TASKS and SETUP so every role can see those universal tabs (per-department reflection of Tasks/Setup content handled in UI). DepartmentType enum: ACCOUNTING, DISPATCH, SAFETY, OPERATIONS, RECRUITING, TASKS, SETUP. Dispatcher's former OPERATIONS scope was dropped before Day 6 per tenant feedback.
verifyAccessToken // decode + tokenVersion cache check
β
restrictToTenantCarriers // JWT-read: req.carrierScope = allowedCarrierIds
β
requireDepartment('SAFETY') // NEW β department gate
β
requireAction('edit') // NEW β action gate
β
handler
tokenVersion cache is in-memory Map with 60s TTL. Single PM2 process on staging makes Redis unnecessary. Cache interface abstracted so a Redis swap is later a one-file change.
Admin revokes carrier X from user U β Transaction: - UPDATE user: tokenVersion += 1, carrierRestrictions disconnect X - Evict U from tokenVersion cache β U's next request (old JWT) β JWT tokenVersion (7) != DB tokenVersion (8) β 401 β Frontend: redirect to /login β Re-login β new JWT without carrier X
Propagation: instant on cache eviction, max 60s via TTL expiry if eviction missed.
const { user } = useAuth(); // decoded JWT already in context
// No API calls below:
{user.departments.SAFETY && <SafetyNav />}
{user.departments.SAFETY?.actions.includes('edit') && <EditButton />}
<CarrierDropdown
options={carriers.filter(c => user.allowedCarrierIds.includes(c.id))}
/>
20260421195527_permission_model_v1 adds RoleDefinition + UserRoleAssignment tables (applied to staging DB)tenantAccountId=null rowsscripts/seed-permission-model-v1.js created 35 UserRoleAssignment rows for 13 existing staging usersUser.carriers field rename deferred to Day 2 (11 call sites overlap with login rewrite)623b3e6utils/computePermissions.js β intersects tenant.CarrierAccess.carrierIds β© user.carrierRestrictions, resolves departmentβactions map from role assignments{isSuperAdmin, isAdmin, allowedCarrierIds, departments, tokenVersion} in access tokenUser.carriers β carrierRestrictions rename completed (11 call sites migrated, relation name UserCarriers unchanged so zero DDL)1e4a35butils/tokenVersionCache.js β in-memory Map, 60s TTL, explicit evictUser() + evictOrganization() for instant revocationmiddleware/auth.js β JWT-first validation; stale tokenVersion returns 401 "Session revoked"middleware/accountScope.js rewritten β reads from req.user, zero DB queries; Super Admin/Admin bypass; missing tenantAccountId β allowedCarrierIds:[]middleware/requireDepartment.js + middleware/requireAction.js factories with Super Admin/Admin bypasstokenVersion=999 β 401651751cdriver, truck, trailer, device, deviceCategory, contractor, fuelCard, vendorTyperouter.use(requireDepartment("SAFETY")) after verifyAccessToken + restrictToTenantCarriersrequirePermission() calls with requireAction("SAFETY", view|edit|delete) β create folded into edit7e96460utils/revokeSession.js β composable revokeUserSessions(userId, tx) / revokeOrganizationSessions(orgId, tx) helpers (tx-aware)userController.updateUser detects role/departments/carrierIds/isActive changes; bumps tokenVersion only on real change (no-op PATCH stays silent)userController.deleteUser evicts cache on deletecarrierController.createCarrier auto-assign path revokes creator's sessions inside the transactionmiddleware/requirePermission.js now bypasses on isSuperAdmin || isAdmin from JWT β unblocks legacy admin flowsbca4d1bcontrollers/rolesController.js rewritten for RoleDefinition β list / presets / get / create / update / delete; 5-per-tenant custom-role cap (409 ROLE_LIMIT_REACHED); system presets read-only on PUT/DELETE (403)routes/rolesRoutes.js β inline requireAdmin gate (isSuperAdmin || isAdmin from JWT); static /presets before dynamic /:idutils/revokeSession.js β new revokeUsersByRoleId(roleId, tx) helper; updateRole wraps mutation + all session revocations in a single $transaction20260421210000_department_tasks_setup β additive TASKS + SETUP enum values so every preset can see those universal tabsf4be470/api/users/:userId/*: list roles, replace roles ({roleIds}), add single role, remove single role, get carrier-restrictions, replace carrier-restrictions ({carrierIds})revokeUserSessions in a $transaction β change and revocation commit atomicallyCarrierAccess membership check; Super Admin bypassesisAdmin/isSuperAdmin-granting role; returns 409 SELF_DEMOTION_BLOCKEDcomputePermissions.ALL_DEPARTMENTS extended with TASKS + SETUP so admin-fill matches the seedfbe4e6eorg.tokenVersion β
perms.tokenVersion (the admin USER's tokenVersion from computeUserPermissions) in the access token, while middleware checks Organization.tokenVersiontokenVersion: org.tokenVersion on both login (line 190) and refresh (line 416) paths43296b7pages/Roles.jsx rewritten: Presets section (read-only, cyan accent) + Custom section (editable, teal accent) with live 5-cap countercomponents/forms/RoleForm.jsx β name input + department multi-select (7 values incl. TASKS+SETUP) + actions checkbox grid; revoke warning when role has assignmentscomponents/forms/UserForm.jsx gains Roles multi-select + Carrier Restrictions multi-select; empty restrictions = "see all tenant carriers"api/roles.js, useRoles.js (splitRoles, customCount, atCap); api/users.js +6 fns, useUsers.js +4 mutations + useUserRoles / useUserCarrierRestrictions queries603d31busePermissions() hook + nav gates + 401 handler β
hooks/usePermissions.js reads JWT claims from AuthContext: isSuperAdmin, isAdmin, hasDepartment(d), canDo(d,a), scopeCarriers(list), allowedCarrierIdsdept or adminOnly meta; non-matching items hide for non-adminsaxios.js β redirects to /login?reason=session-revoked with single-shot guard against concurrent 401salert() hack from UserForm self-edit path β global handler covers it cleanlyb7325eccomponents/IfCan.jsx β wraps children behind canDo(dept, action) with optional fallback; Super/Admin bypass handled in the hook<IfCan dept="SAFETY" action="edit">feat/permission-model-v1 β main on both repos; tagged v0.2.0-permission-model; auto-deployed to stagingf8aeef5 β getUserCarrierRestrictions:carrier.findMany added to .scope-allowlist.json (safe by chain of custody β ids derived from tenant-scoped parent load)a5358bfdepartmentsMap shape for user-type sessions β
user.departments as legacy array ["SAFETY"] and the new object at user.departmentsMap. Org-type sessions return the object at user.departments.usePermissions was reading only user.departments β for Moe, departmentsMap["SAFETY"] was undefined β every nav item hid β blank TopBar + empty Sidebar, just dashboarduser.departmentsMap, falls back to user.departments only when it's a non-array objectd286da4getUsers / getUser now return the authoritative roles[] shape + isAdmin / isSuperAdmin. Legacy user.role kept for back-compat.pages/Users.jsx rewrite using the TruxFlow brand palette (navy #01377D / cyan #009DD1 / teal #2EDAC4). Gradient avatars, accent strip on admin cards, StatPill row, live search, carrier restriction summary, hover-revealed action icons with self-edit carve-out.components/RoleChip.jsx β 4 variants (Super Admin navy+teal-dot, Admin solid-navy, preset cyan-tinted, custom teal-tinted) reused on Users + UserForm + Roles pages.hasPermission("x.edit") replaced with canDo("SAFETY", "edit"); Contractors gained missing canDelete binding.a5ded47: AuthContext.hasDepartment was blanking forms for users whose role assignments came from the new admin UI (legacy array empty). Now consults departmentsMap first, falls back to legacy array.e824a02, frontend e6295e1 + a5ded47components/FilterPopover.jsx (cyan/navy gradient header, optional in-popover search, checkbox items): Role Type (Super Admin / Admin / Preset / Custom), Department, Role, Carriers (incl. "Unrestricted (all tenant)" pseudo-value). OR within a filter, AND across filters. Active selections render as removable chips with color-coded facets. Stale persisted ids silently reconcile against the current option set.0841f63 + aeb3cc9createUser accepts roleIds[] + tenant-validated carrierIds[]; user creation + UserRoleAssignment.createMany wrap in one $transaction. Sets tenantAccountId on the new user (fixes a latent legacy bug where new users had no tenant). Response mirrors getUser shape.ACTIVE + ONBOARDING carriers with live "N selected" counter and Clear.alert() hack removed β Day 9 global 401 handler covers self-edit token-version bumps.e046451, frontend 18f712evalidateRoleIds returns {grantsSuperAdmin} and returns 403 SUPER_ADMIN_REQUIRED when a non-SA caller includes the SA preset. Enforced at createUser, replaceUserRoles, addUserRole.handoffSuperAdmin(tx, tenantAccountId, targetUserId) strips SA from every OTHER tenant user and revokes their sessions β runs inside the same $transaction as the role write + target revocation.Caller (you) Β· Super Admin β Admin + Target Β· β Super Admin visualization + amber "both sessions revoked" notice. Cancel reverts the click; Confirm commits only SA as selected.b2f465b, frontend 519eca0| Scenario | Setup | Result |
|---|---|---|
| revoke-carrier | Admin restricts target user's carriers; target's old JWT in-flight | Stale JWT β 401 "Session revoked" + redirect to /login?reason=session-revoked. Re-login mints fresh JWT that includes only the allowed carriers. β |
| custom-role | Admin creates "View Only" custom role (SAFETY + view only), assigns to target | Target's GET /drivers β 200, POST /drivers β 403. Action gate fires correctly for view-only custom role. β |
| cross-tenant | Plain-Admin (no Super Admin) attempts to restrict a user to a carrier outside their tenant's CarrierAccess | 400 with "One or more carriers are not in your tenant's access list". Super Admin bypasses this guard by design. β |
| Live Safety Officer | Moe, Safety Officer, one-carrier restriction, real browser session | TopBar shows Safety only, Sidebar shows full Safety nav, all carrier dropdowns + list pages scoped to the single assigned carrier. Zero leakage. β |
| # | Decision | Rationale |
|---|---|---|
| 1 | Single Super Admin per tenant, with handoff | One account owner per tenant. Granting Super Admin to another user auto-demotes the acting Super Admin to Admin in one transaction. Confirmation modal fires at chip-selection time, not on save. Break-glass for a stranded tenant is a DB-level operation (WeLink support only). |
| 2 | Custom roles allowed, cap 5 per tenant | DB-driven so custom roles compose cleanly. Cap keeps UI + audit manageable. |
| 3 | Entity-level permissions deferred, entities: string[] reserved | Ship simple now, no schema churn later. |
| 4 | Revocation immediate via tokenVersion bump + cache eviction | Max 60s propagation; instant on eviction. |
| 5 | Reuse User.carriers β rename carrierRestrictions | Zero data migration. |
| 6 | In-memory Node Map for tokenVersion cache (not Redis) | Single PM2 process on staging. Interface abstracted for Redis swap later. |
| 7 | Presets as system RoleDefinition rows with tenantAccountId = null | Same table for presets + custom β one query, one UI. |
| 8 | Frontend reads permissions from JWT, no per-button backend checks | User's explicit preference: "server not stressed, UX smooth". |
Deferred polish (candidate follow-up PR):
Users.jsx list card showing RoleDefinition chips instead of legacy user.role.nameuser.role?.name !== "Admin" guard on Users page edit/deleteOut of scope (original decisions hold):
entities: string[] reserved, not wiredRole / Permission / Organization tables β Phase 0.5 cleanup debt| Decision Point | Path A: WeLink-First | Path B: SaaS-Ready |
|---|---|---|
| Folder Structure | Keep flat controllers/routes | Modular (modules/safety/, etc.) |
| Multi-Tenancy | Single org (WeLink), add later | Account model from start |
| External Customers | Requires refactoring | Ready immediately |
| Time to WeLink Production | Faster (~3-4 months) | Slower (~5-6 months) |
| Refactor Cost Later | Higher (2-4 weeks when needed) | Lower (already structured) |
| Module-Level Access | Not supported | Built-in (CarrierAccess.modules) |
| Subdomain Support | Not supported | Account.subdomain ready |
| Team Scaling | Harder (everyone in same folders) | Easier (assign devs to modules) |
| Microservice Extraction | Major surgery | Module already isolated |
| Complexity Now | Lower | Higher |
| Best For | Solo dev, WeLink-only focus | Growth plans, selling to others |
You can start with Path A and adopt Path B patterns incrementally. Build new modules (Dispatch, Accounting) in the modular structure while keeping existing Safety code flat. Migrate Safety when convenient.
Every synced field between TruxFlow and Airtable. Check this BEFORE changing any field to avoid breaking sync. Source: airtableSync.js
| TruxFlow Field | Airtable Field | Dir | Notes |
|---|---|---|---|
| firstName | First Name ( CDL Driver ) | β | Required |
| lastName | Last Name ( CDL Driver ) | β | Required |
| workEmail | Email Address | β | |
| workPhone | Phone | β | |
| type | Type | β | Maps: DriverβCompany Driver, OWNER_OPERATORβOwner Operator, FLEET_OWNERβFleet Owner |
| status | Safety Status | β | Maps: ACTIVEβActive, ONBOARDINGβOnboarding, DEACTIVATEDβInActive, TERMINATEDβTerminated |
| licenseNumber | CDL # | β | |
| licenseExpire | CDL EXPIRATION DATE | β | Date format: YYYY-MM-DD |
| operationStatus | status - Operation Driver | β | INCOMING ONLY - Set by dispatchers |
| dispatchStatus | Dispatch Status | β | INCOMING ONLY - Cascades from truck |
| TruxFlow Field | Airtable Field | Dir | Notes |
|---|---|---|---|
| twicCard | TWIC Card Required? | β | Boolean |
| twicCardNumber | TWIC Card Number | β | |
| twicExpirationDate | TWIC Card Expiration Date | β | Date |
| ownCompany | Do you have LLC / Corp ? | β | Maps: trueβYes, falseβNo |
| companyName | Company name ( If Applicable ) | β | |
| companyAddress | Corp Address | β | |
| companyPhone | Corp Phone | β | |
| companyEmail | Corp Email | β | |
| einNumber | EIN Number | β |
| TruxFlow Field | Airtable Field | Dir | Notes |
|---|---|---|---|
| SafetyNotes | Notes | β | Text |
| endorsementExplanation | CDL Endorcment | β | Note: Airtable field has typo |
| cleanBackground | Clean Background? | β | Boolean |
| cdlRestriction | CDL Restriction | β | |
| showLoadRate | Show Load Rate | β | Boolean |
| orientationDate | Orientation Date | β | Date |
| hiredDate | Hired Date | β | Date |
| telegramLink | Telegram Link | β | URL |
| carrier.name | Carrier Base Link | β | Linked record (carrier Airtable ID) |
| TruxFlow Doc Type | Airtable Attachment | Airtable Expiration | Airtable Required |
|---|---|---|---|
| CDL_FRONT | CDL Front - Image | β | β |
| CDL_BACK | CDL Back - Image | β | β |
| MEDICAL_CARD | Medical Card - Image | MED EXPIRATION DATE | Medical Registry |
| MVR | MVR | MVR EXPIRATION DATE | β |
| PSP | PSP | PSP EXPIRATION DATE | PSP Required? |
| CLEARINGHOUSE | Clearinghouse - Image | Clearinghouse Expiration Date | Clearinghouse Required? |
| DRUG_TEST | Drug Test | β | Drug Test Required? |
| TWIC_CARD | TWIC Card | β | β |
| APPLICATION | APPLICATION | β | β |
| SSN | Social Security - Image | β | β |
| W9 | W9 | β | β |
| TruxFlow Field | Airtable Field | Dir | Notes |
|---|---|---|---|
| unitNumber | UNIT ID | β | Required, unique identifier |
| vin | Truck Vin | β | |
| plateNumber | Plate Number | β | |
| year | Truck Year | β | Converted to String |
| make | Truck Make | β | |
| model | Truck Model | β | |
| transmission | Transmission Type | β | Maps: MANUALβManual, AUTOMATICβAutomatic |
| relation | Truck Relation | β | Maps: COMPANY_OWNEDβCompany Owned, OWNER_OPERATORβLeased Owner Operator, LEASED_FLEET_OWNERβLeased Fleet Owner, LEASED_RENTALβLeased - Rental |
| TruxFlow Field | Airtable Field | Dir | Notes |
|---|---|---|---|
| eldStatus | ELD Status | β | Maps: ACTIVEβActive, INACTIVEβInActive, EXEMPTEDβexampted (lowercase) |
| fuelCardStatus | Fuel Card Statues | β | Maps: ACTIVEβActive Fuel Card, INACTIVEβInActive Fuel Card, NONEβNo Fuel Card |
| safetyStatus | Safety Status | β | Maps: ACTIVEβActive, ONBOARDINGβOnboarding, PENDINGβPending, OUT_OF_SERVICEβOut Of Service, DEACTIVATEDβDeActivated |
| status | Status-Unit Operation | β | INCOMING ONLY - Maps: ActiveβOPERATING, InActiveβNOT_OPERATING, Broken DownβBROKE_DOWN |
| dispatchStatus | Dispatch Status | β | INCOMING ONLY - Set by dispatchers in Airtable |
| telegramLink | Telegram Group | β | URL |
| dispatcherNotes | Dispatcher Notes | β | Text |
| onboardingNotes | Onboarding Notes | β | Text |
| titleOwnershipIssueDate | Title Ownership Issue Date | β | Date |
| carrier.name | Carrier Base Link | β | Linked record |
status (Status-Unit Operation) and dispatchStatus flow FROM Airtable TO TruxFlow only. Dispatchers control these fields. Do NOT sync them outgoing or you'll overwrite dispatcher changes.
| TruxFlow Doc Type | Airtable Attachment | Airtable Expiration | Airtable Required |
|---|---|---|---|
| OWNERSHIP | Title Image | β | β |
| TITLE_RECEIPT | Registration Image | Registration Expiration Date | Registration Required? |
| STATE_INSPECTION | State Inspection Image | State Inspection Due Date | State Inspection Required? |
| DOT_INSPECTION | Annual DOT Inspection IMAGE | Annual DOT Inspection Due Date | β |
| CAB_CARD | Cab Card Image | Cab Card Expiration Date | Cab Card Required? |
| FORM_2290 | 2290 Image | 2290 Expiration Date | 2290 Required? |
| LEASE_AGREEMENT | Lease Agreement | β | β |
| PLATE | Plate Image | β | β |
| W9 | W9 Form | β | β |
| PICTURES | Full picture of the vehicle | β | β |
| TruxFlow Field | Airtable Field | Dir | Notes |
|---|---|---|---|
| trailerNumber | Trailer Number | β | Required, unique identifier |
| vin | Trailer Vin | β | |
| plateNumber | Plate Number | β | |
| trailerType | Trailer Type | β | Maps: DRY_VANβDry Van, REEFERβReefer, FLATBEDβFlatbed, STEPDECKβStepdeck, LOWBOYβLowboy, DOUBLE_DROPβDouble Drop, HOTSHOTβHotshot |
| trailerDoorType | Door Type | β | |
| trailerSize | Trailer Size | β | Maps: 48/48_FEETβ48 Feet, 53/53_FEETβ53 Feet |
| numberOfAxles | Number of Axles | β | Integer |
| trailerMake | Make - Trailer | β | |
| trailerModel | Trailer Model | β | |
| trailerYear | Trailer Year | β | Converted to String |
| TruxFlow Field | Airtable Field | Dir | Notes |
|---|---|---|---|
| relation | Ownership - Trailer | β | Maps: OWNEDβOWNED - Hyper Lane, RENTALβRENTAL, OWNER_OPERATORβOwner Operator |
| suspension | Suspension | β | Maps: AIR_RIDEβAir Ride, SPRINGβSpring |
| hubmeterReading | Hubmeter Reading - Trailer | β | |
| note | Notes | β | Text |
| safetyStatus | Safety Status | β | Maps: ACTIVE/ONBOARDING/PENDINGβActive, OUT_OF_SERVICE/DEACTIVATED/INACTIVEβInActive |
| operationStatus | Operating Status | β | Maps: OPERATINGβOperating, NOT_OPERATING/BROKE_DOWN/TIME_OFF/SITUATIONβNot Operating |
Trailer's Carrier Base Link is a computed lookup field in Airtable β it's inherited through Driver/Unit linkages. Do NOT write to it directly.
| TruxFlow Doc Type | Airtable Attachment | Airtable Expiration | Airtable Required |
|---|---|---|---|
| TITLE | Title | β | β |
| REGISTRATION | Registration | Registration Expiration Date | β |
| DOT_INSPECTION | DOT Inspection | DOT Inspection Expiration Date | β |
| STATE_INSPECTION | State Inspection | State Inspection Expiration Date | State Inspection Required? |
| PLATE | Plate copy | β | β |
| TRAILER_PICTURES | Trailer Pictures | β | β |
Contractors are not synced to Airtable. They are created in TruxFlow during onboarding (auto-generated from Owner Operator driver data) and exist only in TruxFlow database.
| Endpoint | Dir | Purpose |
|---|---|---|
| POST /api/webhooks/airtable/onboard | β | UNIFIED - Create driver/contractor + truck + trailer + documents |
| POST /api/webhooks/airtable/truck/operation | β | Sync Status-Unit Operation β TruxFlow truck.status + driver.operationStatus |
| POST /api/webhooks/airtable/truck/dispatch | β | Sync Dispatch Status β TruxFlow truck.dispatchStatus + driver.dispatchStatus |
| POST /api/webhooks/airtable/driver | β | Legacy - Create driver only (use /onboard instead) |
| POST /api/webhooks/airtable/truck | β | Legacy - Create truck only (use /onboard instead) |
Dispatch: appgkNoBxps3vytbs
Recruiting: appU3d2aJS4tk4A4d
Drivers (2.1.): tbldwlXQlfx1qwRDP
Trucks (2.2.): tblav6peuPlIwP8wg
Trailers (2.5.): tblwGQQ9EomlBiM25
Carriers (4.1.): tblzMTF3LeUAIOBUL
Driver Apps (7.4.): tblGb9dSQkk7r5NAz
Recruiting: /root/airtable-onboard-script.js
Safety: /root/airtable-safety-onboard-script.js
Sync Code: /backend/utils/airtableSync.js
| TruxFlow | Airtable |
|---|---|
| Driver | Company Driver |
| OWNER_OPERATOR | Owner Operator |
| Owner Operator | Owner Operator |
| FLEET_OWNER | Fleet Owner ( Not driving ) |
| TruxFlow | Airtable |
|---|---|
| ACTIVE | Active |
| ONBOARDING | Onboarding |
| PENDING | Onboarding |
| OUT_OF_SERVICE | Out of Service |
| DEACTIVATED / INACTIVE | InActive |
| TERMINATED | Terminated |
| TruxFlow | Airtable |
|---|---|
| COMPANY_OWNED | Company Owned |
| OWNER_OPERATOR | Leased Owner Operator |
| LEASED_OWNER_OPERATOR | Leased Owner Operator |
| LEASED_FLEET_OWNER | Leased Fleet Owner |
| LEASED_RENTAL | Leased - Rental |
| Airtable | TruxFlow Truck | TruxFlow Driver |
|---|---|---|
| Active | OPERATING | On Duty |
| InActive | NOT_OPERATING | Not Available |
| Approved Off | TIME_OFF | Approved Off |
| Broken Down | BROKE_DOWN | Broken Down |
| Home Time | TIME_OFF | Home Time |
| Situation | SITUATION | β |
| Event | Subject | Recipient |
|---|---|---|
| Onboarding Success | β Driver Onboarded: {name} | Safety@welinkcargo.com |
| Onboarding Failure | β Onboarding Failed: {name} | Safety@welinkcargo.com |
POSTMARK_API_TOKEN=3500a88a-...
POSTMARK_FROM_EMAIL=noreply@welinkcargo.com
SAFETY_NOTIFY_EMAIL=Safety@welinkcargo.com
/backend/utils/email.js
Fire-and-forget, non-blocking
| Type | Creates | Links |
|---|---|---|
| Owner Operator | Driver + Contractor + Truck + Trailer | DriverβTruck, TrailerβTruck, Truck.owner |
| Company Driver | Driver only | β |
| Fleet Owner | Contractor + Truck + Trailer | TrailerβTruck, Truck.owner |
| Airtable Value | TruxFlow Enum |
|---|---|
| Active | OPERATING |
| InActive | NOT_OPERATING |
| Approved Off | TIME_OFF |
| Broken Down | BROKE_DOWN |
| Situation | SITUATION |
| Home Time | TIME_OFF |
| Airtable Value | operationStatus |
|---|---|
| Active | On Duty |
| InActive | Not Available |
| Approved Off | Approved Off |
| Broken Down | Broken Down |
| Home Time | Home Time |
Bucket: trux-flow
Region: us-ord-1
Endpoint: us-ord-1.linodeobjects.com
Airtable β Download β S3 β TruxFlow DB
Files transferred during onboarding, URLs stored in Document table
How two humans (and their Claude Code agents) work safely on this VPS β across multiple projects, sometimes the same project on different modules β without corrupting shared state, leaking credentials, or stepping on each other's git working trees.
Status (2026-05-04): Part A (Anthropic Console) complete. Part B (VPS Linux setup) drafted, partially pre-staged. Execution session 2026-05-05.
Each human is a separate Linux user. Auto-memory, settings, and OAuth credentials are per-user by design β no shared MEMORY.md to corrupt. Global rules live in /opt/claude-shared/ and are read-only for both: when Moe edits a global rule, Shady's next session sees the update instantly.
Default workspace~/.bashrc
/root/ is mode 700 β only root can read or traverse it. If shared rules stayed there, Shady couldn't follow a symlink into them. Moving them to /opt/claude-shared/ (mode 755, world-readable) lets both users symlink to the same source. One source of truth, two consumers.
| Layer | Location | Shared / Per-User | Why |
|---|---|---|---|
| Global rules (CLAUDE.md, businesses, coding-standards, etc.) | /opt/claude-shared/ |
Shared (read-only via symlink) | Same standards apply to everyone |
| Feedback corpus (cross-team lessons) | /opt/claude-shared/feedback/ |
Shared (read-only) | Lessons graduate from per-project to here |
personal-info.md |
~/.claude/ |
Per-user | Different person, different identity |
communication-style.md |
~/.claude/ |
Per-user | Different preferences possible |
settings.json / settings.local.json |
~/.claude/ |
Per-user | Permissions, hooks should not bleed across |
.credentials.json (subscription OAuth) |
~/.claude/ |
Per-user (never share) | Personal authentication tokens |
Auto-memory (MEMORY.md + memory tree) |
~/.claude/projects/<cwd>/memory/ |
Per-user (never share) | Concurrent writes corrupt; each builds own context |
| Runtime: sessions, file-history, paste-cache, plugins, telemetry | ~/.claude/ |
Per-user | Per-session state, not shareable |
Per-user auto-memory does NOT mean Shady starts blind every session. The momentum on big projects lives in docs/SESSION-LOG.md, docs/decisions/, and docs/FIELD-REGISTRY.md β files that live inside the project's git repo, not in auto-memory. Both Claudes pull the same git, both load the same project docs at session start, both have full context.
Auto-memory is for per-human shorthand: "Moe prefers blunt pushback", "Moe is non-technical", "Moe's API key was rotated 2026-04-28". Shady's auto-memory holds his shorthand β and that's the whole point. Sharing it would actually be wrong.
The whole system depends on SESSION-LOG.md being updated at end of every session β a global rule already. With two humans, this rule becomes more load-bearing: skip a session log β next person's Claude starts blind on what just changed.
| Repo | State | GitHub Remote | Auth |
|---|---|---|---|
truxflow-hosting/app/backend |
git, has remote | weLinkCargoEnterprise/TruxFlow |
SSH alias github-staging + deploy key |
truxflow-hosting/app/frontend |
git, has remote | weLinkCargoEnterprise/TruxFlow-Front |
SSH alias github-frontend + deploy key |
security/reports-repo |
git, has remote | weLinkCargoEnterprise/security-reports |
SSH alias github-security + deploy key |
welink-ticket |
git, no remote | β | n/a |
welink-website |
git, no remote | β | n/a |
welink-airtable |
git, no remote | β | n/a |
truxflow-preview |
git, no remote | β | n/a |
SleepScape |
git, no remote | β | n/a |
welink-ops |
not a git repo | β | n/a |
10-4Hire |
not a git repo | β | n/a |
Don't share Moe's deploy keys β those are single-purpose private keys per repo.
/srv/git/<project>.gitdevs owns it; both users in the groupGitHub remote restoration deferred to follow-up session. Code lives only on VPS until then β flag for backup.
One human per project at a time, unless using git worktrees on different modules with non-overlapping files.
git pull at session startgit push at session end β never commit and disappearprisma/schema.prisma, docs/03-DATA-MODEL.md, root config) β coordinate before touching mid-sessionBoth can work TruxFlow at the same time only if (a) different modules, and (b) docs are split per-module. Today the docs are still unified β that's a blocker we'll resolve in a follow-up session before any real concurrent work happens.
Different files = no merge conflicts. Each Claude session loads project-wide log + its module's log + its module's decisions.
| Approach | Isolation | Sync | Disk Space | Conflict Risk |
|---|---|---|---|---|
| Same folder, same branch | None | N/A | Low | High |
| Same folder, different branches | Partial | Git checkout | Low | Medium |
| Git worktrees (Recommended for same-project) | Full | Same repo | Medium | Low |
| Separate clones (Default β different humans) | Full | Git pull/push | High | Low |
| Different servers | Full | Git + deploy | Highest | None |
Default: separate clones (per-user home). Same-project parallel work: add a worktree on a feature branch.
| Step | Action | Status |
|---|---|---|
| A1 | Audit existing keys (hash-based, no values exposed) | β Done |
| A2 | Confirm billing model (subscription vs API) | β Done |
| A3 | Upgrade Anthropic account from Individual β Organization | β Done |
| A4 | Create Shady's workspace | β Done |
| A5 | Create Shady's API key, store in Moe's password manager | β Done |
| A6 | Send Shady org-level Member invite (work email) | β Done |
| A7 | Shady accepts invite + added to workspace as Developer | β Done |
| Phase | Action | Estimate | Status |
|---|---|---|---|
| 1 | Migrate shared rules to /opt/claude-shared/ + symlink back | 2 min | β Done |
| 2 | Create shady Linux user with sudo | 30 sec | β Done |
| 3 | Install Shady's SSH public key + lock down auth | 1 min | β Done |
| 4 | Set up /home/shady/.claude/ + symlinks + per-user templates | 2 min | β Done |
| 5 | Install Claude Code (per-user binary) for Shady | 3 min | β Done |
| 5b | Install claude-mem plugin for Shady | 1 min | β³ With Shady |
| 6 | Wire Shady's API key into ~/.anthropic_env | 1 min | β Done |
| 7 | Project clones β hybrid (GitHub for 3 repos, bare repos for rest, git-init for welink-ops & 10-4Hire) | 10 min | β³ With Shady |
| 8 | Shady's first-session protocol verification | 5 min | β³ With Shady |
| 9 | Verification checklist + sign-off | 2 min | β³ With Shady |
~5 of 9 phases pre-executed 2026-05-04 evening. Remaining (with Shady): ~10 min total β claude-mem plugin install + project clones + first-session test.
WeLink-Sale-Bot is misnamed β it's actually Moe's general-purpose key. Momo1claude is the trading bot key, hardcoded in claude_bot.py + 6 backup copies.ANTHROPIC_API_KEY in ~/.bashrc sits idle for Claude Code. Confirms zero double-billing.welink-ops and 10-4Hire aren't even git repos.~/.bashrc, PM2 dump, claude-mem logs, file-history snapshots, .bash_history. Rotation + scrub deferred to a future session.| Item | Why deferred | Approx. effort |
|---|---|---|
| Key rotation + plaintext scrub (bashrc, PM2 dump, logs, file-history, claude_bot.py.save.*) | Touches live bot configs; needs careful sequencing | ~30 min standalone session |
| Trading bot key migration (hardcoded β env var) | Bundle with rotation | ~10 min |
| Sales bot diagnostic β why zero usage post April 7 | Not blocking; investigate during next bot work | ~15 min |
| TruxFlow per-module SESSION-LOG / FIELD-REGISTRY split | Required before truly-concurrent same-project work | ~1 hour |
| GitHub remote restoration for 7 repos without remotes | Bare repos at /srv/git/ are interim |
~30 min β varies by repo |
welink-ops + 10-4Hire git initialization |
Bundle with GitHub restoration | ~10 min |
Two-human coordination rule formalized in /opt/claude-shared/workflow-rules.md |
Tight, just need the section drafted | ~10 min |
| Disable SSH password auth globally after Shady's key works | Safer for both users; mid-Phase-3 if desired | ~2 min |
/opt/claude-shared/
/root/.claude/
/home/shady/.claude/
/srv/git/<project>.git
/root/multi-user-setup/SESSION-LOG.md
/root/multi-user-setup/part-a-anthropic-console.md
/root/multi-user-setup/part-b-vps-linux-setup.md
~/.claude/projects/-root/memory/
multi-user-setup-2026-05-04.md
(indexed in MEMORY.md)
CLAUDE.md, businesses.md
infrastructure.md, coding-standards.md
workflow-rules.md, design-skills.md
project-structure.md, feedback/
personal-info.md
communication-style.md
settings.json, .credentials.json
projects/<cwd>/memory/
SSH key: SHA256:7d2Af3xj... (saved)
GitHub: shady.3zzam@gmail.com (collab added)
Anthropic Member: shady.azzam@welinkcargo.com
No blockers for tomorrow.
Two Linux users on one VPS, sharing read-only global rules from /opt/claude-shared/, isolating per-user state and auto-memory, coordinating through git (GitHub for 3 repos, local bare repos for the rest), with one-human-per-project as the default and worktrees as the escape hatch for parallel module work β all while one Anthropic Org bills both users with per-workspace attribution.