MS

0
Skip to main content

Building an AI-Powered Transportation Management System: The Truxo Case Study

March 20, 2025 (1y ago)

In October 2025, I joined Truxo.ai as Senior Software Engineer and Mobile Lead. Truxo is building an AI-powered Transportation Management System (TMS) that serves real trucking operations across the United States — connecting drivers, carriers, brokers, and dispatchers into a single platform that handles everything from load matching to invoicing to compliance.

I am Manjodh Singh Saran, a full stack developer based in Ludhiana, India. This is the story of how we built Truxo — the technical decisions, the architecture, the challenges we faced, and the lessons I learned building software for an industry that moves $940 billion worth of goods annually in the US alone.

This is not a marketing piece. It is a technical case study for developers who want to understand what it looks like to build a complex, production-grade system for a real industry with real constraints.

The Problem: Trucking Is Broken (Technically)

The US trucking industry is massive but technologically fragmented. A typical trucking operation in 2024 uses:

The result is operational chaos. Dispatchers spend hours on the phone coordinating loads. Drivers deal with paperwork at every pickup and delivery. Brokers lose visibility into shipment status. Payments take 30-90 days because invoicing is manual.

Truxo's thesis is simple: unify all of this into one AI-powered platform. One system where loads are matched intelligently, dispatch happens in real-time, documents are processed automatically, payments are fast, and compliance is built into the workflow.

When I joined, the platform had early traction with real carriers. My job was to lead the mobile experience for drivers (the Truxo Tracker app) while contributing to backend systems across the platform.

Architecture Overview

Truxo is a multi-service system. Here is the high-level architecture:

┌─────────────────────────────────────────────────────────┐
│                    Client Layer                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │ Truxo Web    │  │ Truxo Tracker│  │ Broker Portal│  │
│  │ (React/Next) │  │ (React Native│  │ (React/Next) │  │
│  │              │  │  + Expo)     │  │              │  │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘  │
└─────────┼──────────────────┼──────────────────┼─────────┘
          │                  │                  │
          ▼                  ▼                  ▼
┌─────────────────────────────────────────────────────────┐
│                   API Gateway / Load Balancer            │
└────────────────────────┬────────────────────────────────┘
                         │
          ┌──────────────┼──────────────┐
          ▼              ▼              ▼
   ┌────────────┐ ┌────────────┐ ┌────────────┐
   │ Auth       │ │ Shipment   │ │ AI/ML      │
   │ Service    │ │ Service    │ │ Service    │
   │ (FastAPI)  │ │ (FastAPI)  │ │ (FastAPI)  │
   └────────────┘ └────────────┘ └────────────┘
          │              │              │
          ▼              ▼              ▼
   ┌─────────────────────────────────────────┐
   │         PostgreSQL  │  Redis  │  S3     │
   └─────────────────────────────────────────┘

The system is split into multiple FastAPI services, each responsible for a bounded domain. The client layer includes a web dashboard (Next.js), the Truxo Tracker mobile app (React Native + Expo), and a broker-facing portal.

Why FastAPI Over Express

We chose Python with FastAPI for the backend instead of Node.js/Express. I have written a detailed FastAPI vs Express comparison covering when to use each — for Truxo, the decision came down to three factors:

1. AI/ML ecosystem. Truxo's core differentiator is AI — document parsing, load matching optimization, route planning, anomaly detection. Python's ML ecosystem (scikit-learn, transformers, langchain) is unmatched. Running AI models in a Python backend eliminates the overhead of calling out to a separate ML service.

2. Type safety with Pydantic. FastAPI's integration with Pydantic gives us runtime type validation on every request and response. This is better than Express with Zod because Pydantic also generates OpenAPI documentation automatically. Every endpoint is documented the moment it is written.

3. Async performance. FastAPI is built on Starlette and supports async/await natively. For I/O-bound operations (database queries, external API calls, S3 uploads), the performance is excellent. We benchmarked it against Express for our use case and the throughput was comparable, with the added benefit of the Python ecosystem.

The tradeoff is that we lose TypeScript code sharing between frontend and backend. We mitigate this by generating TypeScript types from our OpenAPI spec using openapi-typescript-codegen. The types are auto-generated on every backend deploy and published as an internal package that the frontend and mobile apps consume.

The Truxo Tracker Mobile App

Technical Stack

The Truxo Tracker app is built with:

I have written extensively about my approach to building production React Native apps in my React Native Expo production guide. The Truxo Tracker app applies every principle from that guide at scale.

Real-Time Shipment Tracking

The most critical feature of the driver app is real-time location tracking. Dispatchers and brokers need to know where every truck is at all times. Drivers need to see their route, next stops, and estimated arrival times.

The challenge: GPS tracking is a battery killer. Running continuous high-accuracy GPS on a phone for a 10-hour driving shift will drain the battery by noon.

Our solution uses a tiered tracking approach:

// Simplified tracking logic
import * as Location from "expo-location";
import * as TaskManager from "expo-task-manager";
 
const TRACKING_TASK = "shipment-tracking";
 
TaskManager.defineTask(TRACKING_TASK, async ({ data, error }) => {
  if (error) {
    console.error("Tracking error:", error);
    return;
  }
 
  const { locations } = data as { locations: Location.LocationObject[] };
  const latest = locations[locations.length - 1];
 
  // Batch location updates and send every 30 seconds
  await LocationBuffer.add(latest);
 
  if (LocationBuffer.shouldFlush()) {
    await api.sendLocationBatch(LocationBuffer.flush());
  }
});
 
export async function startTracking(shipmentId: string) {
  const { status } = await Location.requestForegroundPermissionsAsync();
  if (status !== "granted") throw new Error("Location permission denied");
 
  // Background tracking with battery-optimized settings
  await Location.startLocationUpdatesAsync(TRACKING_TASK, {
    accuracy: Location.Accuracy.Balanced, // Not Highest — saves battery
    timeInterval: 10000, // Update every 10 seconds
    distanceInterval: 50, // Or every 50 meters
    foregroundService: {
      notificationTitle: "Truxo Tracker",
      notificationBody: `Tracking shipment ${shipmentId}`,
    },
    pausesUpdatesAutomatically: true, // Pause when stationary
  });
}

Key decisions:

This approach gives us all-day tracking with less than 5% additional battery drain over normal phone usage.

Document Scanning and AI Processing

Trucking involves a lot of paperwork: Bills of Lading (BOL), Proof of Delivery (POD), rate confirmations, insurance certificates. Drivers traditionally photograph these and email them. With Truxo, they scan documents directly in the app.

// Document capture flow
async function captureDocument(type: DocumentType) {
  const result = await ImagePicker.launchCameraAsync({
    quality: 0.8, // Balance between quality and upload size
    allowsEditing: true,
    aspect: [4, 3],
  });
 
  if (result.canceled) return;
 
  // Upload to backend
  const formData = new FormData();
  formData.append("file", {
    uri: result.assets[0].uri,
    type: "image/jpeg",
    name: `${type}_${Date.now()}.jpg`,
  } as any);
  formData.append("document_type", type);
 
  const response = await api.uploadDocument(formData);
 
  // Backend processes with AI:
  // 1. OCR extracts text
  // 2. AI classifies document type
  // 3. Relevant fields are auto-extracted (shipper, consignee, weight, etc.)
  // 4. Data is linked to the correct shipment
  return response;
}

On the backend, uploaded documents go through an AI pipeline:

  1. OCR using Tesseract and cloud-based OCR services for handwritten text
  2. Document classification using a fine-tuned model that identifies BOLs, PODs, invoices, etc.
  3. Field extraction that pulls structured data from the unstructured document — shipper name, consignee, weight, piece count, reference numbers
  4. Validation against the shipment record to flag discrepancies

This pipeline replaces hours of manual data entry. When a driver scans a BOL at pickup, the shipment record is automatically populated with the correct details within seconds.

Authentication and Multi-Role Access

Truxo serves multiple user types: drivers, dispatchers, carrier admins, and brokers. Each has different permissions and data access patterns.

Our auth architecture:

The mobile app handles token refresh transparently:

// RTK Query base query with automatic token refresh
const baseQueryWithReauth = async (args, api, extraOptions) => {
  let result = await baseQuery(args, api, extraOptions);
 
  if (result.error?.status === 401) {
    // Try to refresh the token
    const refreshResult = await baseQuery(
      { url: "/auth/refresh", method: "POST" },
      api,
      extraOptions
    );
 
    if (refreshResult.data) {
      // Store new token
      const { accessToken } = refreshResult.data as AuthResponse;
      await SecureStore.setItemAsync("access_token", accessToken);
      api.dispatch(setToken(accessToken));
 
      // Retry the original request
      result = await baseQuery(args, api, extraOptions);
    } else {
      // Refresh failed — log out
      api.dispatch(logout());
    }
  }
 
  return result;
};

This pattern ensures the driver never sees an auth error during their shift. The token refreshes silently in the background.

Real-Time Communication

Dispatchers need to communicate with drivers in real-time. We built an in-app messaging system rather than relying on phone calls or SMS.

The architecture uses a polling-based approach with push notification triggers:

  1. The mobile app polls for new messages every 30 seconds when the chat screen is open
  2. When the dispatcher sends a message, the backend sends a push notification to the driver
  3. The notification triggers an immediate message fetch, regardless of polling interval
  4. Messages are stored in PostgreSQL with read receipts

We considered WebSockets but decided against them for the mobile use case. Maintaining a persistent WebSocket connection on a mobile device moving through areas with spotty cellular coverage is unreliable. The polling + push notification approach is more resilient and still delivers messages within seconds.

Challenges and Lessons Learned

Challenge 1: Offline-First Architecture

Truck drivers pass through areas with no cellular coverage — rural highways, mountain passes, underground loading docks. The app needs to work when there is no network.

Our solution:

// Offline action queue
class OfflineQueue {
  private queue: QueuedAction[] = [];
 
  async add(action: QueuedAction) {
    this.queue.push({
      ...action,
      timestamp: Date.now(),
      retryCount: 0,
    });
    await this.persist();
  }
 
  async sync() {
    const pending = [...this.queue];
    for (const action of pending) {
      try {
        await this.execute(action);
        this.queue = this.queue.filter((a) => a.id !== action.id);
      } catch (error) {
        if (action.retryCount >= 3) {
          // Move to dead letter queue for manual intervention
          await this.moveToDLQ(action);
          this.queue = this.queue.filter((a) => a.id !== action.id);
        } else {
          action.retryCount++;
        }
      }
    }
    await this.persist();
  }
}

Challenge 2: Background Location on iOS

Apple is strict about background location access. Getting background location working reliably on iOS requires:

  1. Adding the location background mode in app.json
  2. Providing a clear, specific purpose string explaining why you need background location
  3. Using startLocationUpdatesAsync with a foreground service notification
  4. Submitting a detailed App Store review explanation with a demo video

Our first submission was rejected because our purpose string was too vague ("to track shipments"). We rewrote it to specifically explain that drivers need continuous tracking during active deliveries so dispatchers can provide ETAs to customers. The second submission was approved.

Challenge 3: Handling Diverse Android Devices

Truxo's drivers use a wide range of Android devices — from flagship Samsungs to budget phones that cost under $100. Performance varies dramatically across this range.

Our approach:

Challenge 4: Compliance and Data Privacy

Trucking is a regulated industry. We deal with Hours of Service (HOS) regulations, FMCSA compliance, and sensitive business data (rates, contracts, financial information).

Our compliance approach:

Results and Impact

Since launching Truxo Tracker:

The most rewarding metric is driver feedback. Truck drivers are not known for being early adopters of technology. When a driver tells you the app actually makes their job easier, you know you got something right.

Technical Takeaways for Developers

If you are building a complex, real-world system, here is what I would emphasize:

1. Domain knowledge matters as much as technical skill. I spent my first month at Truxo learning the trucking industry — talking to drivers, dispatchers, and carriers. Understanding the domain shaped every technical decision.

2. Mobile apps for field workers have different constraints than consumer apps. Battery life, offline capability, diverse device support, and rugged use conditions are first-class concerns.

3. AI features need graceful fallbacks. Our document AI works well, but it is not 100% accurate. Every AI-processed field has a manual override. Users correct the AI, and those corrections feed back into model improvement.

4. Start with a monolith, extract services when you have a reason. Truxo started as a single FastAPI application. We extracted services as specific domains (auth, shipments, AI processing) grew complex enough to justify independent deployment.

5. Cross-functional collaboration is not a soft skill — it is a technical skill. I work directly with product, design, and operations teams. The best technical architecture is one that aligns with how the business actually operates.

Building Truxo has been the most technically challenging and professionally rewarding work of my career. If you are interested in the broader architecture patterns I use across projects, check out my full stack developer roadmap or visit my blog for more technical deep dives.

You can download Truxo Tracker on the App Store and Google Play, or learn more about the platform at truxo.ai.

For developers building similar real-time, data-intensive mobile applications, my React Native Expo production guide covers the foundational patterns that made Truxo Tracker possible.

Related Articles

MS
Manjodh Singh Saran

Full Stack Developer · Ludhiana, India

Read more articles · View portfolio