The brief was straightforward: a trucking company needed a platform to manage their fleet, assign jobs to drivers, and give clients visibility into their deliveries. One platform to replace WhatsApp, a spreadsheet, and two different apps they'd been stitching together for three years.

What actually happened over the following four months is a more interesting story.

What we built

The final product has four components: a web dashboard for the operations team (fleet overview, job assignment, driver management), a mobile app for drivers (job details, navigation, delivery confirmation with photo upload), a client portal (shipment tracking, history, document download), and an admin backend for billing and reporting.

All of it runs on a single Node.js backend with a PostgreSQL database, deployed on AWS. The mobile app is React Native — one codebase for both iOS and Android. The web apps are React with Tailwind.

It's live. It works. Drivers use it every day.

The three decisions that changed everything

Decision 1: offline-first mobile app. We initially built the driver app assuming reasonable mobile connectivity. About three weeks into development, we discovered that several of the client's routes go through areas with patchy or no signal. A driver who can't confirm a delivery because the app won't load is worse than no app at all.

We rebuilt the mobile data layer with offline support — jobs sync when connected, confirmations queue locally and upload when signal returns. This added two weeks to the timeline. It was the right call.

The lesson: always ask about connectivity conditions for any mobile app that will be used outside an office. The answer will change your architecture.

Decision 2: separate client portal from the ops dashboard. Our original design had clients logging into a restricted view of the main dashboard. This seemed efficient. It turned out to be a maintenance headache and a security concern — every ops UI change needed to be reviewed for client visibility implications.

We separated them into two distinct applications sharing the same API. More code, cleaner boundaries, much easier to iterate on each independently. Clients see exactly what they need and nothing more.

Decision 3: build the billing integration last. The client had a specific invoicing software they needed to integrate with. We scoped this for mid-project. We should have scoped it for last.

The billing integration turned out to be more complex than expected — the target system had an inconsistent API and required several workarounds. By doing it last, we didn't let it block the core product. The core shipped on time. The billing integration followed two weeks later.

What the data looks like now

Before the platform, the ops team were making 40–60 WhatsApp messages per day just to coordinate jobs. That's now zero. Client calls asking for status updates dropped from several per day to near zero — clients check the portal instead. Driver arrival confirmations, which were previously verbal and sometimes disputed, are now timestamped with photo evidence.

None of these outcomes were explicitly in the brief. They emerged from replacing a fragmented system with a single source of truth.

What we'd tell someone starting a similar project

Map every existing communication channel before you design anything. WhatsApp groups, phone calls, emails, spreadsheets — these are all signs of missing system functionality. Your job is to understand why each one exists, not just to replicate what you can see.

And build the mobile app for the worst connectivity you'll encounter, not the best.