Architecture for developers

One monorepo (Turborepo + pnpm for TypeScript, Melos for Flutter), one NestJS backend organised as a modular monolith, and seven clients generated against its OpenAPI contract.

System architecture Four web surfacesstorefront · seller · dispatch · admin Three Flutter appsbuyer · seller · driver API (NestJS)modules: identity, catalog, cart, checkout, order, payment, delivery… Workeroutbox relay + scheduled jobs PostgreSQLdata + outbox Meilisearch S3 storage Redis SMTP · FCM · SMS REST + Socket.IO
The API and the worker are two processes built from the same code base and sharing one PostgreSQL database.

Stack

LayerChoice
BackendNestJS 11, Node 22+, TypeScript strict — modular monolith
DatabasePostgreSQL 16 with Prisma; raw SQL where Prisma is not expressive enough (search indexes, ledger aggregates)
SearchMeilisearch behind a SearchProvider interface (an in-memory engine for tests)
StorageS3-compatible (MinIO locally, SeaweedFS in production) behind a StorageProvider interface; imgproxy for image resizing (not deployed in production yet, so originals are served)
Cache / queueRedis 7 is provisioned; no job queue — background jobs are database pollers
PaymentsPaymentProvider interface: cash on delivery, Stripe, Paymob (stub), sandbox card (dev)
AuthJWT access (15 min) + rotating refresh tokens, RBAC, TOTP MFA, OTP by email and SMS, Google and Apple OAuth
WebNext.js 15 storefront; React + Vite for seller, dispatch and admin; Tailwind; TanStack Query
MobileFlutter with Riverpod, go_router, Dio; shared packages in packages_dart/
RealtimeSocket.IO gateway on /realtime for order tracking and fleet updates

Modules

Each lives in apps/api/src/modules/<module>:

identity · customer · seller · catalog · search · media · inventory · pricing · promotion · cart · checkout · order · payment · ledger · shipping · delivery · realtime · engagement · content · platform · outbox · audit · privacy · cache

The rules that keep it correct

  • Module boundaries. A module never imports another module's repository or entity; it calls the other module's public service or reacts to its domain events. This keeps the option to split services out later.
  • Outbox. Every side effect (search indexing, notifications, analytics, webhooks, delivery task creation) is an event written in the same transaction as the state change, then relayed by the worker.
  • Idempotency. Money and order writes require an Idempotency-Key; the stored response is replayed on retry.
  • Double-entry ledger. Every financial event is a balanced transaction; balances are derived and reconciled hourly.
  • Stock reservations with expiry. Availability is on-hand minus reserved, computed under a row lock; reservations expire rather than rely on a compensating write.
  • Explicit state machines for orders, delivery tasks and sellers; nothing writes a status directly.
  • Tenancy enforced by test. Every query on a seller-owned model must be scoped to the caller; a test walks the source tree and fails the build on an unscoped query.
  • Error catalogue. Every error carries a code from packages/contracts/src/errors.ts.
  • Money. Integer minor units + ISO currency; strings on the wire; basis points for rates.
  • IDs. UUID v7, generated in the database.
  • Migrations are forward-only; zero-downtime changes follow expand/contract.

The data model in five decisions

  1. Product → Variant → Offer: shared listing, buyable SKU, one seller's price and stock.
  2. Order → OrderGroup: one purchase and payment, split per seller for fulfilment and settlement.
  3. Double-entry ledger: seller balances, platform revenue and driver cash are derived.
  4. Stock = on hand − reservations, with a time-to-live on reservations.
  5. Every side effect through the outbox.

The schema spans about 135 tables across files such as identity, catalog, order, payment and delivery under apps/api/prisma/schema/. See docs/domain-model.md.

Repository map

apps/api            NestJS API + worker, Prisma schema and migrations
apps/storefront     Next.js buyer website
apps/seller-web     Seller console (React + Vite)
apps/delivery-web   Dispatch console (React + Vite)
apps/admin          Admin console (React + Vite)
apps/mobile/{buyer,seller,driver}   Flutter apps
packages/contracts  Error catalogue, Money, pagination, shared schemas
packages/sdk-ts     Generated TypeScript API client
packages_dart/      Shared Flutter packages (API client, UI, auth, l10n)
infra/              Docker Compose, Dockerfile, observability, Terraform notes
docs/               ADRs, domain model, endpoints, runbooks, this site

Running it locally

pnpm install
pnpm infra:up                         # Postgres, Redis, Meilisearch, MinIO, Mailpit, imgproxy
pnpm --filter @mp/api prisma:migrate
pnpm --filter @mp/api seed
pnpm --filter @mp/api dev             # API + worker
pnpm --filter @mp/storefront dev      # and any other surface

The API listens on port 3001 with Swagger at /docs. Quality gates: pnpm lint && pnpm typecheck && pnpm test; Flutter: melos run analyze and melos run test.