How We Built a Courier Management SaaS: The ShipAsaan Architecture Breakdown
- published
- updated
- reading time
- 3 min read
- topic
- Web / SaaS
ShipAsaan is a courier management platform we built end to end. A NestJS and PostgreSQL core serves seven product surfaces: a merchant portal, an internal operations portal, customer and courier Flutter apps, WooCommerce and Shopify integrations, and a public REST API. The architecture bet was a modular monolith built around one canonical shipment lifecycle. Every surface is a different view over the same state machine, and that's what kept seven surfaces shippable by a small founder-led team.
On this page · 6 sections
What does a courier platform actually have to do?
Courier operations before ShipAsaan ran on spreadsheets, phone calls, and disconnected tools. There was no single view of a parcel from booking to delivery, tariff and billing calculations were manual, rider incentives were hand-computed, and merchant onboarding was scattered across chat threads. The product brief reduced to one sentence. Make every parcel, rupee, and role visible in one system.
Why a modular monolith instead of microservices?
With shipments, billing, incentives, dispatch, support, and integrations all transacting against the same lifecycle, splitting into services early would have bought us distributed-systems problems before we had scale problems. We chose a modular NestJS monolith over PostgreSQL, with clear module boundaries (shipments, tariffs, incentives, portals, integrations), one transactional core, and the option to peel modules into services later. That decision is why seven surfaces launched instead of three.
How does the shipment lifecycle drive everything?
The heart of the system is a state machine covering booking, pickup, hub transfers, out-for-delivery, and then delivered, returned, or exception, with consignment notes carrying QR and barcodes. Every other feature hangs off state transitions. Real-time notifications fire on them, the billing engine prices them (zones, weight slabs, COD handling, company-specific rates), the incentive engine accrues rider and sales payouts from them, and audit logs record who moved what and when. New features stayed cheap because they subscribe to the lifecycle instead of reimplementing it.
What did the mobile and integration layer look like?
- Two Flutter apps built from shared patterns: a customer app for tracking and notifications, and a courier app for delivery workflows and status updates. One team, both stores.
- Firebase push notifications wired to lifecycle transitions instead of polled.
- A WooCommerce plugin and a Shopify integration, so merchant stores create shipments automatically.
- A public REST API, the same contract our own portals use, so third-party merchants can integrate without special-casing.
Which tradeoff mattered most?
Automated billing was the riskiest module. Tariffs vary by zone, weight slab, service type, COD, insurance, and per-company negotiated rates. The temptation was a rules engine with a UI. The discipline was to version tariff configurations as data, compute deterministically, and log every calculation input alongside the output. When a merchant disputes an invoice, operations can replay exactly how a number was produced. Billing disputes went from arguments to lookups.
Frequently Asked Questions
When a specific module has scaling, deployment, or team-ownership needs the rest of the system doesn't share. ShipAsaan's modules have clear boundaries around one transactional core, so a module can be extracted later without a rewrite. Splitting before that point adds network calls, distributed transactions, and more infrastructure without solving a real problem.
One codebase covers both iOS and Android, and the two apps share patterns for authentication, notifications, and shipment status. That let one team ship and maintain both apps across both stores. Push notifications are triggered by shipment lifecycle transitions on the server, so the apps don't need to poll for updates.
Tariffs are stored as versioned configuration data, and every calculation logs its inputs next to its output: zone, weight slab, service type, COD, insurance, and the merchant's negotiated rate. When a merchant questions an invoice, operations can replay exactly how the charge was produced instead of recalculating by hand.
Because the portals are the API's first and heaviest users. Any bug or missing field shows up in Bitlogs' own frontends before a third-party merchant hits it, and there's only one set of endpoints to document, version, and secure. Merchants integrating directly get the same capabilities the portals have.