NETWORK · LOGISTICS PORTAL
The cockpit your forwarders work in. Branded for your operations.
Your forwarder updates you by email. The dispatch team posts milestones into a shared spreadsheet someone keeps forgetting to refresh. Customs clearance dates arrive as PDF screenshots. Rate cards live in a SharePoint folder. When the container slips, you find out from the importer asking why. Your “shipment record” is a fiction reconstructed from three inboxes and a WhatsApp.
The Logistics Portal in TradeOS is a branded cockpit your forwarders, brokers and carriers work in directly — same database, scoped to what they need to see. Structured Inbox replaces ad-hoc email. Real milestones land on real state machines. Customs, dispatch, invoicing and conversations all on one record. Multi-tenant identity: one login, many operator workspaces, each one isolated. Free for the provider. Not a TMS replacement.
Portal sections
7 · inbox, shipments, dispatch, invoices, and 3 more
Logistics modes
Ocean, air, truck, rail, express, and 3 more
Multi-tenant identity
One login, many operator workspaces
Cost to forwarder
$0 · operator pays for TradeOS
The real Home dashboard — greeting strip, world map widget with active shipments at their positions, two-column action queue. Single backend call (GET /portal/logistics/home) returns everything. Operator brand chrome (logo, color, workspace label) renders from the operator's tenant settings.
THE MILESTONE THAT NEVER UPDATES
What “shipment visibility” really looks like today.
The forwarder is competent. The broker is fast. The carrier's tracking page exists. None of that helps when the system of record for a single shipment is split across three inboxes, two spreadsheets, a SharePoint folder, and a WhatsApp group nobody's in the same time zone for.
01 · MILESTONES LIVE IN A SHARED SPREADSHEET
“What’s the ETA?” gets the answer from a tab someone forgot to refresh.
Your forwarder updates a Google Sheet on a different cadence than the importer who's chasing you. The ops team has two sources of truth — the spreadsheet and the carrier's tracking page — and they disagree by three days. By the time you pull the answer, the container has cleared customs without anyone noticing.
02 · CUSTOMS UPDATES LIVE IN EMAIL THREADS
The broker sends “we filed entry on Tuesday” at 11pm. You read it Thursday.
The customs broker is fast but their channel is whatever inbox they had open at the time. The release notification arrives as a PDF attachment. You forward it to accounting, who can't find the entry number because it's inside the screenshot. The shipment goes out three days late because nobody knew it was cleared.
03 · ONE FORWARDER, FIFTEEN PORTALS
Your forwarder ignores the portal you sent them because they have fifteen others.
You stand up “your” portal and ask your forwarder to log in to update milestones. They have one for every client. They check none of them consistently. Your data goes stale the moment they close the browser tab. The portal becomes a system the forwarder treats as a chore — until it doesn't.
7 SECTIONS · ONE PORTAL
Every surface the forwarder works on, in one navigation.
Real LOGISTICS_NAV.flatLinks from client/src/lib/navConfig.ts. Mobile bottom-nav uses the first four: Home · Inbox · Shipments · Dispatch.
Home
Greeting strip + world map with active shipments + 2-column action queue (Needs Response, At Risk, Today, Money). Single backend call returns everything.
Inbox
The structured-response queue. 3 sub-tabs (bookings, RFQs, disputes), each with Needs Action / Awaiting Response / Recently Decided sections. 48h auto-promote on stale counter-quotes.
Shipments
5 tabs — Tracking, Containers, Costs, Customs, History. One backend call (/shipments/overview/all) returns everything; tabs are projections.
Dispatch
11-state machine for ground transport. assigned → accepted_by_driver → en_route_to_pickup → arrived → loaded → departed → en_route_to_delivery → arrived → unloaded → completed (+ failed branch). POD-gated completion.
Rates
Rate sheets with valid-from / valid-to / version / superseded-by. Per-line surcharges, volume tiers, minimum charge. Bulk import 1-500 lines atomic.
Invoices
10-state machine. 3 types: per-shipment, monthly-consolidated, ad-hoc. Dispute branch with resolution tracking.
Messages
Threaded messaging scoped per operator connection. Attached to entity (booking, RFQ, shipment, invoice). Notifications via the same fabric as the other portals.
THE INBOX · 1,252 LINES OF UI
The forwarder’s inbox lives in your record — not in three email threads.
Three workflow types share one queue because from the forwarder’s perspective they’re all “structured response, time-pressured, I’m the bottleneck”: booking requests, RFQs, and invoice disputes. The Inbox surface puts them in one queue with inline action forms so the common path needs zero navigation.
5 quotes submitted · awaiting operator decision · oldest 18h
2 quotes won · 1 quote lost · expand to see
SHIPMENTS · 5 TABS · ONE BACKEND CALL
Five projections of the same shipment data.
The forwarder’s primary working surface. GET /portal/logistics/shipments/overview/all returns everything once; the page keeps it in memory and projects per tab. No fan-out from the browser, no per-tab re-fetch.
Tracking
World map + list with status, route, last milestone, open exceptions. Click a row → inline expansion showing the full milestone timeline + open exceptions for that shipment.
Container planning
Every container on every shipment, with type, utilisation (CBM / weight vs container max), seal #, loading date. The “what’s loaded where” view.
Costs & freight
Per-shipment revenue from invoices, paid amount, outstanding. Aggregate totals at top. Drill into invoice detail from any row.
Customs & compliance
Per-shipment customs filings: jurisdiction (ISO 3166), declaration type (import/export/transit), status (7-state machine), declaration number, HS codes filed. Aggregate cleared/pending/held at top.
History
Chronological merge of events + milestones + exceptions across all of this forwarder’s shipments. Time-ordered feed. The audit log surface.
STATE MACHINES · ENFORCED IN DB
Six lifecycles the forwarder works through — without ever leaving your record.
Every workflow has an explicit state machine — Postgres enum types or CHECK constraints. Service-layer transition rules validate which states can move to which. No magic strings, no UI-side state.
BOOKING · 6 STATES
Terminal alts: declined · cancelled · expired. Accepting creates the linked shipment.
RFQ · 7 STATES
Branches: lost · declined_to_quote · expired · withdrawn. Backend enforces one-pending-quote-per-RFQ.
DISPATCH · 11 STATES
Any active state can transition to failed with a reason. Completion requires POD captured first (gated by tooltip in UI).
INVOICE · 10 STATES
Branches: overdue · disputed · written_off. Three invoice types: per_shipment · monthly_consolidated · ad_hoc.
CUSTOMS FILING · 7 STATES
Branches: held (pending docs / payment / inspection) · queried (customs asked a question) · rejected (terminal). Per-jurisdiction × declaration_type.
EXCEPTIONS · 3+3 ENUM
Severity × status. Status moves open → mitigating → resolved. Distinct from milestones — declared, mitigation tracked, resolution audited.
8 LOGISTICS MODES · PER-MODE MILESTONE TEMPLATES
Eight logistics modes — because real shipments don’t fit one.
The logistics_mode enum has eight values, each with its own per-mode milestone template (validated service-side, not via DB enum so new milestones can be added without ALTER TYPE).
Milestones: booked → VGM filed → gate-in → loaded → departed → transshipped → arrived → discharged → customs → delivered.
Milestones: booked → CFS received → consolidated → loaded → departed → arrived → deconsolidated → customs → delivered.
Milestones: booked → manifest → handover → uplifted → in-flight → landed → received → customs → delivered.
Milestones: Full 11-state dispatch machine (above).
Milestones: dispatch machine with multi-stop pickup + delivery loops.
Milestones: dispatch machine + port appointment workflow.
Milestones: dispatched → loaded → departed → border → gauge change → arrived → unloaded → handover.
Milestones: picked up → in transit → out for delivery → delivered (or returned).
ONE FORWARDER · MANY OPERATORS
One forwarder login. Many operator workspaces. Each one isolated.
A forwarder serving Pendrew Energy, AcmeCorp, BioMed Global, and seven others gets one users row, one login, ten operator workspaces via the external_memberships table (migration 121). Switch with one click. Each operator’s bookings, rate sheets, milestones, invoices, and conversations are isolated by connection_id — there’s no API path through which data fans out across operators.
Your carrier API credentials live at the external_organization level (migration 289) — set up once, encrypted via the KMS plugin, reused across every operator workspace you serve.
CONFIDENTIALITY · ARCHITECTURAL
Cross-operator confidentiality is enforced in the platform, not policy.
Architecture, not handshake. Real redactFor(entity, row, viewerRole) in server/src/modules/portal-shell/redaction.ts — same pattern as the supplier and client portals.
PER-CONNECTION SCOPE
Every query carries a connection_id from the JWT
There is no path through the logistics-portal API that fans out across operators. Cross-operator access returns 404 (not 403) so the existence of the resource is hidden — verified in the projector test suite (portal-logistics/__tests__/projector.test.ts).
FIELD-LEVEL REDACTION
Logistics never sees commercial values
Buy price, sell price, margin, cost basis — stripped before the row leaves the database. Real redactShipmentForClient and redactFor('shipment', raw, 'logistics') functions enforce this. The exception is the BOL: legally-required consignee data appears on the BOL document, governed by Incoterm-driven visibility (next section).
RATE PRIVACY
Your rates with one operator are not visible to others
Rate sheets are scoped per logistics_connection_id. The operator-side rate marketplace exists for comparing across providers (Solo+ feature), not the reverse — forwarders cannot enumerate “who else gets a better rate than I do.” RFQ outcomes (won / lost) are surfaced to the forwarder themselves but never aggregated across forwarders.
INCOTERMS · VISIBILITY CONFIG
What the forwarder sees depends on which party is the legal shipper.
The connection’s visibility_config JSON (migration 122 column, schema docs in 291) sets per-incoterm visibility for origin and destination — country / port / city / factory_name / factory_address / full_address. Plus per-incoterm overrides and label overrides.
DDP · CIF · CFR
Operator = legal shipper of record
Factory identity is abstracted on shipping documents. The forwarder sees the operator’s address as origin; the operator’s tenant settings drive the BOL shipper field. The factory name is never rendered to the forwarder unless explicitly overridden.
FOB · EXW · FCA
Factory = legal shipper of record
The factory is the legal shipper on the BOL because it has to be there legally. Factory name and pickup address are visible to the forwarder; the visibility_config can still constrain what the operator’s downstream client sees (the client-portal redaction applies separately).
DRIVER · PWA
Same portal, scoped to “your dispatches only.”
Drivers install the Logistics Portal to their home screen — manifest.webmanifest + service worker — sign in once, and get the dispatch queue scoped to assignments where they’re the assigned driver. No rates, no invoices, no other shipments. Milestones capture GPS lat/lng and photo uploads; POD requires signature + signer name + signer title before completed transition is allowed.
Six milestone source values are recorded per event: user (ops manual entry), driver (mobile capture), carrier_api, ocr, scheduled_auto_advance, operator_override. The milestone table is append-only — UPDATE is refused by trigger, corrections insert new rows with supersedes_milestone_id pointing at the original.
Geofence-driven auto-transitions, OCR pipeline for BOL upload, and native iOS / Android apps are on the roadmap — the milestone source enum already reserves their slots.
TIER GATING · 15 FEATURE KEYS
Free for the forwarder. The operator’s tier decides which capabilities show up.
The real tier_features registry (migration 291) defines fifteen logistics-portal feature gates. Logistics providers never see a paywall; the connecting operator’s tier decides what’s available in your workspace for that operator. [Roadmap] tags mark features whose tier_feature key exists but whose implementation is post-launch.
Roadmap = tier_features key registered in migration 291 (see db/migrations/291_logistics_tier_features_and_visibility_overrides.sql); engine / aggregation / UI implementation lands post-launch per Logistics Spec §19, §14, §25.
ON THE ROADMAP
What’s coming to the Logistics Portal.
v1 ships seven sections, six state machines, append-only milestone log, multi-tenant identity, field-level redaction, Incoterm-driven visibility, and tier gating across fifteen feature keys. Several capabilities the marketing team would love to claim are flagged honestly as roadmap.
01
AI consolidation suggestions
Migration 290 ships the consolidation_groups table schema. The suggestion engine that populates this table is a post-launch background job; the v1 UI surfaces nothing yet. Tier feature key logistics.ai_consolidation_suggestions registered for Business+.
02
Cross-tenant performance insights + network reputation
Tier feature keys logistics.cross_tenant_insights and logistics.network_reputation_display registered for Business+. Aggregation engine and ≥3-operator anti-inference threshold land post-launch.
03
Multi-operator consolidation (Enterprise)
Cross-operator consolidation requires consent mechanics not yet specified. Migration 290 explicitly defers this — “intentionally NOT modelled here.” Tier feature key reserved.
04
OCR pipeline for BOL upload
Milestone source enum value 'ocr' is registered; the OCR service that ingests a BOL image, extracts container / vessel / seal / voyage fields, and posts the corresponding milestone is post-launch.
05
Geofence-driven auto-transitions
Dispatch state transitions on geofence entry / exit (arrived_at_pickup, arrived_at_delivery) — not currently implemented. v1 captures GPS at the moment the driver taps the milestone; auto-advance from geofence comes later.
06
Carrier API breadth — direct integrations
v1 integration registry: project44 (live), Freightos Baltic / Xeneta / Drewry / MarineTraffic / Vizion (freight-rate + AIS visibility). Direct integrations with Maersk / MSC / CMA CGM / ONE / HMM / Hapag-Lloyd, IATA Cargo IQ for air, and FedEx / UPS / DHL / USPS for express land as the underlying APIs are wired. logistics_carrier_api_credentials.carrier_name is free-text TEXT so adding a carrier doesn’t require schema changes.
07
Native iOS / Android driver app
PWA is the v1 driver surface (manifest + service worker live in client/public/). Native apps with biometric auth, native camera APIs, and push notifications are post-launch.
08
Scheduled carrier-API polling cadence
The logistics_carrier_api_credentials table has a last_polled_at column. The scheduled job that polls each connected carrier per its cadence (daily for ocean, hourly on critical air legs, 12h for rail) is post-launch.
FAQ
The questions every forwarder asks first.
No. We don’t compete with CargoWise, Magaya, Descartes, or Project44. We’re the coordination layer between your TMS and the operator who hired you. Keep your TMS for internal operations; use the Logistics Portal for the operator-side workflow that lives outside it — bookings the operator initiates, milestone visibility the operator needs, customs filings against the operator’s shipments, rate sheets and RFQs the operator queries, and freight invoices the operator pays. TMS integration for operators with CargoWise / Magaya / Descartes is on the Enterprise tier.
Send us your forwarder list and one in-flight booking. We’ll set up a sandboxed Logistics Portal and walk one of your forwarders through it in 30 minutes.
Send a CSV of your top forwarders and one open shipment. We’ll spin up a sandboxed Logistics Portal with your tenant’s branding, your forwarder list as external_organizations, and walk a real forwarder through accepting an RFQ, posting milestones, filing customs, and issuing an invoice. No demo data. Your forwarders, your shipment.