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.
Stack
| Layer | Choice |
|---|---|
| Backend | NestJS 11, Node 22+, TypeScript strict — modular monolith |
| Database | PostgreSQL 16 with Prisma; raw SQL where Prisma is not expressive enough (search indexes, ledger aggregates) |
| Search | Meilisearch behind a SearchProvider interface (an in-memory engine for tests) |
| Storage | S3-compatible (MinIO locally, SeaweedFS in production) behind a StorageProvider interface; imgproxy for image resizing (not deployed in production yet, so originals are served) |
| Cache / queue | Redis 7 is provisioned; no job queue — background jobs are database pollers |
| Payments | PaymentProvider interface: cash on delivery, Stripe, Paymob (stub), sandbox card (dev) |
| Auth | JWT access (15 min) + rotating refresh tokens, RBAC, TOTP MFA, OTP by email and SMS, Google and Apple OAuth |
| Web | Next.js 15 storefront; React + Vite for seller, dispatch and admin; Tailwind; TanStack Query |
| Mobile | Flutter with Riverpod, go_router, Dio; shared packages in packages_dart/ |
| Realtime | Socket.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
- Product → Variant → Offer: shared listing, buyable SKU, one seller's price and stock.
- Order → OrderGroup: one purchase and payment, split per seller for fulfilment and settlement.
- Double-entry ledger: seller balances, platform revenue and driver cash are derived.
- Stock = on hand − reservations, with a time-to-live on reservations.
- 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.