AAMLabs (now Forge Labs) · Full-stack engineer

Pedeja Delivery

A delivery SaaS with seven event-driven NestJS microservices behind a gateway, serving two clients: a web backoffice and a mobile app.

Client
AAMLabs (now Forge Labs)
Role
Full-stack engineer
Period
2024 to present

The problem

A delivery SaaS has to serve three audiences with very different rhythms: the restaurant, the driver, and the end customer. Treating that as a monolith means coupling the lunch order peak, the constant delivery tracking, and the payment spike at closing time into the same codebase and the same deploy. The goal was to separate the domains cleanly, so each part can scale and evolve without one stepping on the other’s toes.

Architecture: seven services behind a gateway

The backend is seven NestJS services, each owning its own database and contract, behind an API gateway that is the single door for the clients. Two ends consume that door: the web backoffice and the mobile app.

   web backoffice (React)        mobile app (Expo / React Native)
            └───────────────┬────────────────┘
                       API gateway

   ┌──────── 7 NestJS services, each with its own database ────────┐
   auth          sessions, refresh, token rotation
   user          profiles, addresses, roles
   catalog       restaurants, menus, items (Redis cache)
   order         the order state machine (Postgres + MongoDB)
   payment       providers, settlement, refunds
   notification  email, push, in-app
   delivery      driver assignment, ETA, proof of delivery

   events over Kafka · order status in real time over WebSocket

Async communication between services is event-driven, over Apache Kafka. When an order changes state or a payment is processed, the service publishes an event to a topic (order.status.changed, payment.processed, and so on), and whoever cares consumes it. That decouples the services in time: payment does not need to know who reacts to it.

The order service is the most interesting one. The order state lives in Postgres as the source of truth (PENDING → CONFIRMED → PREPARING → READY → PICKED_UP → IN_DELIVERY → DELIVERED, with CANCELLED and REFUNDED), and the full history of transitions goes to MongoDB, which is better for an append-only audit timeline. On top of that, a WebSocket gateway with Socket.io listens to the Kafka events and pushes the status change in real time to whoever is following the order. Each service exposes REST documented with Swagger.

The lesson is about boundaries: the event is what keeps the services genuinely independent. The moment one service has to call another synchronously for a side effect, the coupling comes back in through the back door.

The web backoffice

The admin panel is a product in its own right, and it is where I spent a good part of the time. It is a React 19 SPA with Vite and TanStack Router, with file-based routing and automatic code splitting. The organization is sliced by domain: each feature (orders, restaurants, drivers, payments, promotions, users, support, settings, and more) is self-contained, with its own pages, components, API calls, schemas, and store. Twelve modules cover the entire operation.

The dashboard uses Recharts for revenue, orders, driver performance, and payment methods. The UI primitives are shadcn/ui over Radix, with Tailwind v4 tokens and a light and dark theme. A route only loads after an authentication guard (beforeLoad), and Axios injects the JWT on every request and logs out on a 401.

The decision that paid off the most day to day was building a mock layer with MSW. The whole backoffice boots and exercises the entire interface with no backend running, which let the frontend evolve at its own pace, decoupled from the progress of the services. The lesson: a mock faithful to the contract is not just a testing convenience, it is what lets both ends move forward in parallel.

The customer app

The third surface is the end-customer app, in Expo with React Native and expo-router, the same file-based routing as the backoffice, now on mobile. The UI uses NativeWind, which is Tailwind on React Native, so the styling vocabulary is the same as the other ends. The flow covers the order path: authentication, browsing by categories and search with filters, picking an address on the map with react-native-maps and expo-location, and following orders.

The state is leaner than the panel’s: instead of TanStack Query and stores, the app leans on React context for authentication and preferences, talking to the backend through a single base URL. That single base is the point of the architecture: both the app and the backoffice see one address, the gateway, not the seven services behind it. Changing the backend’s internal routing touches no client.

Where it stands

The project is in build, with the three surfaces standing. The seven services run locally via Docker Compose with Postgres, MongoDB, and Kafka, with events and the WebSocket working; the web backoffice is complete on top of the mocks; and the mobile app covers the main customer flow. The piece that stitches it all together for production is the API gateway, the single door that centralizes authentication and routing. The shape, though, is already decided: each service evolves and deploys at its own pace, and each client talks to a single address.

Numbers

  • 7

    Microservices

  • 12

    Backoffice modules

Stack

  • NestJS 11
  • Prisma 6
  • PostgreSQL
  • MongoDB
  • Apache Kafka
  • Socket.io
  • Redis
  • React 19
  • Vite 7
  • TanStack Router
  • TanStack Query
  • Tailwind 4
  • shadcn/ui
  • Recharts
  • MSW
  • Expo
  • React Native
  • expo-router
  • NativeWind
  • Zod