7.7 KiB
title, tags, created
| title | tags | created | |||
|---|---|---|---|---|---|
| System Architecture |
|
2026-05-23 |
System Architecture
End-to-end architecture for the Amn escrow marketplace platform — a two-repo system (frontend + backend) that brokers crypto-escrowed transactions between buyers and sellers.
[!info] Read Backend Architecture and Frontend Architecture for component-level detail. Read Infrastructure for deployment topology.
1. High-level topology
flowchart LR
Browser[Browser / PWA]
CDN[Static CDN]
Nginx[Nginx Reverse Proxy<br/>:80/:443]
FE[Next.js Frontend<br/>standalone server<br/>:8083]
BE[Express Backend<br/>+ Socket.IO<br/>:5001]
Mongo[(MongoDB 8)]
Redis[(Redis 8)]
SHK[SHKeeper<br/>Crypto Gateway]
SMTP[SMTP<br/>Nodemailer]
OAI[OpenAI API]
BC[Blockchain RPC<br/>Alchemy / WalletConnect]
Browser -->|HTTPS + WSS| CDN
Browser -->|HTTPS| Nginx
Nginx --> FE
Nginx --> BE
FE -->|REST /api/*| BE
FE -.->|Socket.IO| BE
BE --> Mongo
BE --> Redis
BE -->|Pay-in / Pay-out| SHK
SHK -.->|Webhook HMAC| BE
BE --> SMTP
BE --> OAI
FE -->|Wallet Connect| BC
BE -->|Tx verify| BC
2. Request lifecycle (typical authenticated REST call)
sequenceDiagram
actor U as User
participant FE as Frontend (Next.js)
participant AX as Axios Client
participant BE as Backend (Express)
participant MW as Middleware Chain
participant SVC as Service Layer
participant DB as MongoDB
participant SK as Socket.IO
U->>FE: Interact with UI
FE->>AX: useMutation(...).mutate(payload)
AX->>AX: Attach Authorization: Bearer <jwt>
AX->>BE: POST /api/marketplace/requests
BE->>MW: helmet, cors, body-parser
MW->>MW: authMiddleware → verify JWT
MW->>MW: validation middleware
MW->>SVC: controller invokes service
SVC->>DB: Mongoose create / update
DB-->>SVC: document
SVC->>SK: emitToRoom("user-{id}", "request:created", payload)
SVC-->>BE: response payload
BE->>AX: 200 { success, data }
AX->>FE: React Query cache update
FE-->>U: UI re-render
Concurrent realtime path:
sequenceDiagram
participant FE as Frontend
participant BE as Backend
FE->>BE: socket.connect() w/ auth token
BE-->>FE: socket connected
FE->>BE: join-user-room <userId>
BE-->>FE: joined user-{userId}
Note over BE,FE: long-lived connection
BE-->>FE: emit "notification:received"
FE->>FE: update notification badge
3. Deployment topology
Production runs as a single Docker Compose stack (backend/docker-compose.production.yml) behind an internal Nginx that proxies to both the frontend and backend containers. Watchtower watches the container registry (git.manko.yoga/manawenuz/escrow-backend) and auto-pulls new images tagged latest.
| Layer | Service | Image | Port | Purpose |
|---|---|---|---|---|
| Edge | Nginx | nginx:alpine |
8083 | Reverse proxy + static assets |
| App | Frontend | nickapp-frontend:latest |
8083 (internal) | Next.js standalone |
| App | Backend | nickapp-backend:latest |
5001 (internal) | Express + Socket.IO |
| Data | MongoDB | mongo:8.0 |
27017 (internal) | Primary store |
| Data | Redis | redis:8-alpine |
6379 (internal) | Cache + sessions + rate-limit counters |
External SSL termination, DNS, and CDN are assumed to live in front of Nginx (CloudFlare / nginx-proxy / similar).
Note
Dev uses
backend/docker-compose.dev.yml(no Nginx, no frontend container — developer runs FE locally withyarn dev). See Docker Setup for full compose breakdown.
4. Network ports
| Port | Process | Visibility | Notes |
|---|---|---|---|
| 8083 | Frontend (Docker) | Public via Nginx | next start standalone |
| 3000 | Frontend (local dev) | localhost | yarn dev — hot reload |
| 5001 | Backend (Express + Socket.IO) | Public via Nginx /api & /socket.io |
One process, two protocols |
| 27017 | MongoDB | Internal | Mapped to host only in dev |
| 6379 | Redis | Internal | Mapped to host only in dev |
| 222 | Gitea SSH | Public | Used for git clone |
5. Data flow patterns
5.1 Read path
REST GET requests are cache-able. React Query on the frontend de-duplicates concurrent requests, retries on failure (exponential backoff), and treats data as stale after staleTime (default 60s, varies per query). Backend reads use Mongoose lean queries where possible for performance.
5.2 Write path
Mutations follow optimistic-then-confirm:
- Frontend
useMutationissues the request, often withonMutateupdating cache optimistically. - Backend validates → writes to MongoDB → emits affected socket rooms → returns canonical response.
- Frontend invalidates related query keys; React Query refetches.
- Other connected clients receive the socket event and refetch on their side.
5.3 Webhook path (inbound)
External services (SHKeeper) POST to /api/payment/shkeeper/webhook. The backend verifies HMAC signature, updates the Payment document, advances any linked PurchaseRequest/SellerOffer state, and emits Socket.IO events to both buyer and seller rooms.
sequenceDiagram
participant SHK as SHKeeper
participant BE as Backend
participant DB as MongoDB
participant Buyer
participant Seller
SHK->>BE: POST /api/payment/shkeeper/webhook<br/>X-Signature: HMAC-SHA256
BE->>BE: verifySignature(body, header, SHKEEPER_WEBHOOK_SECRET)
BE->>DB: Payment.updateOne({providerPaymentId}, {status:"completed"})
BE->>DB: PurchaseRequest.updateOne(..., {status:"funded"})
BE-->>Buyer: socket emit "payment:status-updated"
BE-->>Seller: socket emit "request:funded"
BE-->>SHK: 200 OK
See Payment Flow - SHKeeper for the full sequence.
6. Scaling considerations
| Concern | Current state | Scale-out path |
|---|---|---|
| Backend stateless? | Yes — JWT-only auth, no in-memory session | Run N replicas behind LB; use Redis pub/sub adapter for Socket.IO |
| MongoDB | Single-node | Replica set → sharding by buyerId |
| Redis | Single-node | Cluster mode; separate cache vs session DBs |
| Socket.IO | Single process | @socket.io/redis-adapter for multi-node fan-out |
| File uploads | Local uploads/ mount |
S3 / R2; multer-s3 adapter |
| Logs | Container stdout | Loki / ELK |
| Errors | Sentry (@sentry/nextjs on FE) |
Add Sentry backend SDK |
Warning
Before horizontal-scaling backend, switch Socket.IO to the Redis adapter — otherwise users on different nodes will not receive each other's events.
7. Cross-cutting concerns
- Auth — Bearer JWT on REST, same token also passed via Socket.IO
authpayload on connect. See Security Architecture & Authentication Flow. - i18n — Frontend supports en/fa/ar/fr/cn/vi via
i18next. RTL handled bystylis-plugin-rtlper direction. See Internationalization & RTL. - Realtime — All notification/chat/payment state changes broadcast over Socket.IO. See Real-time Layer.
- Errors — Standardized envelope
{ success: false, error: { code, message, details } }viabackend/src/shared/utils/response-handler.ts. Sentry on frontend. - Idempotency — Payment webhooks idempotent by
providerPaymentId+ status. Pay-out uses a client-suppliedreferencestring.
8. Related documents
- Backend Architecture — module-level walk-through of the Express app
- Frontend Architecture — Next.js App Router & section organization
- Infrastructure — Docker, compose, registry, Watchtower
- Real-time Layer — Socket.IO setup, rooms, events
- Security Architecture — auth, hashing, rate-limit, webhook HMAC
- Tech Stack — exact versions & purpose of every dependency
- Payment Flow - SHKeeper — end-to-end crypto pay-in flow