Skip to content

Latest commit

Β 

History

History
451 lines (385 loc) Β· 29.7 KB

ARCHITECTURE.md

File metadata and controls

451 lines (385 loc) Β· 29.7 KB

Trust-Link Backend β€” Architecture

Overview

Trust-Link Backend is a NestJS application that manages escrow transactions on the Stellar blockchain. Vendors create escrow agreements, buyers fund them via Stellar payments, and the system automatically tracks shipments, releases funds, and handles disputes.


High-Level Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          External Clients                                β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚  Web / Mobile β”‚   β”‚  Stellar Horizon  β”‚   β”‚  Background Workers    β”‚  β”‚
β”‚   β”‚    Clients    β”‚   β”‚ (webhook retries) β”‚   β”‚ (in-process timers)    β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚ HTTPS              β”‚ POST /webhooks/stellar  β”‚ Node.js setInterval
           β–Ό                    β–Ό                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         NestJS HTTP Layer                                β”‚
β”‚                                                                          β”‚
β”‚  Global Middleware          Global Guards          Global Filters         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ SecurityMiddlewareβ”‚   β”‚  ThrottlerGuard    β”‚   β”‚GlobalException   β”‚   β”‚
β”‚  β”‚ LoggerMiddleware  β”‚   β”‚  (per-route caps)  β”‚   β”‚Filter            β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          Feature Modules                                  β”‚
β”‚                                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚    Escrow    β”‚  β”‚    Vendor    β”‚  β”‚    Dispute   β”‚  β”‚   Webhooks  β”‚  β”‚
β”‚  β”‚   Module     β”‚  β”‚   Module     β”‚  β”‚   Module     β”‚  β”‚   Module    β”‚  β”‚
β”‚  β”‚              β”‚  β”‚              β”‚  β”‚              β”‚  β”‚             β”‚  β”‚
β”‚  β”‚ POST /escrow β”‚  β”‚ POST /vendor β”‚  β”‚ POST /:id/   β”‚  β”‚ POST        β”‚  β”‚
β”‚  β”‚ GET  /:id    β”‚  β”‚   /profile   β”‚  β”‚   dispute    β”‚  β”‚ /webhooks/  β”‚  β”‚
β”‚  β”‚ PATCH/:id/   β”‚  β”‚ GET  /vendor β”‚  β”‚ GET  /:id/   β”‚  β”‚   stellar   β”‚  β”‚
β”‚  β”‚   ship|cancelβ”‚  β”‚   /escrows   β”‚  β”‚   dispute    β”‚  β”‚             β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚         β”‚                 β”‚                  β”‚                  β”‚         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚                       Admin Modules                               β”‚    β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚    β”‚
β”‚  β”‚  β”‚  AdminStats β”‚  β”‚  AdminDispute  β”‚  β”‚    QueueDashboard     β”‚ β”‚    β”‚
β”‚  β”‚  β”‚ GET /admin/ β”‚  β”‚ GET /admin/    β”‚  β”‚  GET /admin/queues    β”‚ β”‚    β”‚
β”‚  β”‚  β”‚   stats     β”‚  β”‚   disputes     β”‚  β”‚  (BullMQ placeholder) β”‚ β”‚    β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       Shared Infrastructure Layer                         β”‚
β”‚                                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚  PrismaService  β”‚   β”‚   CacheService   β”‚   β”‚  NotificationsService  β”‚ β”‚
β”‚  β”‚  (PostgreSQL)   β”‚   β”‚   (Redis / noop) β”‚   β”‚  (SendGrid + Twilio)   β”‚ β”‚
β”‚  β”‚                 β”‚   β”‚                  β”‚   β”‚                        β”‚ β”‚
β”‚  β”‚  @Global()      β”‚   β”‚  @Global()       β”‚   β”‚  Optional clients      β”‚ β”‚
β”‚  β”‚  In-memory mock β”‚   β”‚  Graceful no-op  β”‚   β”‚  with retry logic      β”‚ β”‚
β”‚  β”‚  for dev/tests  β”‚   β”‚  when REDIS_URL  β”‚   β”‚  (MAX_ATTEMPTS=3)      β”‚ β”‚
β”‚  β”‚                 β”‚   β”‚  not set         β”‚   β”‚                        β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚           β”‚                     β”‚                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                    Auth & Config                                    β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚
β”‚  β”‚  β”‚   ConfigModule       β”‚   β”‚         Sep10Module              β”‚   β”‚   β”‚
β”‚  β”‚  β”‚   @Global() + Joi    β”‚   β”‚  SEP-10 JWT challenge/verify     β”‚   β”‚   β”‚
β”‚  β”‚  β”‚   validation         β”‚   β”‚  JwtGuard for protected routes   β”‚   β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          External Systems                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  PostgreSQL   β”‚   β”‚     Redis      β”‚   β”‚   Stellar Network        β”‚  β”‚
β”‚  β”‚               β”‚   β”‚                β”‚   β”‚                          β”‚  β”‚
β”‚  β”‚  Escrow       β”‚   β”‚  escrow:{id}   β”‚   β”‚  Horizon API             β”‚  β”‚
β”‚  β”‚  Dispute      β”‚   β”‚  keys, 60s TTL β”‚   β”‚  Smart contracts         β”‚  β”‚
β”‚  β”‚  Notification β”‚   β”‚                β”‚   β”‚  SEP-10 auth             β”‚  β”‚
β”‚  β”‚  VendorProfileβ”‚   β”‚                β”‚   β”‚  Webhook callbacks       β”‚  β”‚
β”‚  β”‚  Processed    β”‚   β”‚                β”‚   β”‚                          β”‚  β”‚
β”‚  β”‚  WebhookEvent β”‚   β”‚                β”‚   β”‚                          β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Module Dependency Map

AppModule
β”œβ”€β”€ ConfigModule (@Global)       β€” Joi-validated env config, available everywhere
β”œβ”€β”€ PrismaModule (@Global)       β€” PostgreSQL via in-memory mock; real Prisma in prod
β”œβ”€β”€ LoggerModule                 β€” Structured JSON logger (JsonLoggerService)
β”œβ”€β”€ CacheModule (@Global)        β€” Redis CacheService; no-op when REDIS_URL unset
β”œβ”€β”€ Sep10Module                  β€” SEP-10 Stellar auth challenge + JWT issuance
β”œβ”€β”€ EscrowModule
β”‚   β”œβ”€β”€ PrismaModule
β”‚   β”œβ”€β”€ NotificationsModule      β€” SendGrid/Twilio dispatch with 3-attempt retry
β”‚   └── DisputeModule            β€” Dispute repository and resolution logic
β”œβ”€β”€ StellarModule                β€” ContractService: auto-release, delivery recording
β”œβ”€β”€ VendorModule                 β€” Vendor profile CRUD
β”œβ”€β”€ AdminStatsModule             β€” Aggregated platform statistics
β”œβ”€β”€ AdminDisputeModule           β€” Admin dispute review and resolution
β”œβ”€β”€ QueueDashboardModule         β€” BullMQ/ioredis queue metrics (placeholder)
β”œβ”€β”€ WebhooksModule               β€” Stellar Horizon webhook receiver
β”‚   └── EscrowModule (imported)
└── WorkersModule
    β”œβ”€β”€ EscrowModule
    β”œβ”€β”€ DisputeModule
    └── StellarModule

Data Models

Escrow (core entity)

Field Type Notes
id UUID Primary key
itemName string 3–100 chars
itemRef string Vendor-scoped dedup key
amount Decimal(18,8) Stellar asset amount
currency string 3–12 uppercase, e.g. USDC
buyerAddress string Stellar public key
vendorAddress string Stellar public key
state EscrowState See state machine below
trackingId string? Set when shipped
shippedAt DateTime?
deliveredAt DateTime? Set by TrackingPollWorker
autoReleaseSubmittedAt DateTime? Optimistic lock for auto-release
autoReleaseTxHash string? Horizon transaction hash
disputeId string? FK to Dispute

Dispute

Field Type Notes
id UUID Primary key
escrowId UUID FK β†’ Escrow (cascade delete)
reason string
description string
evidenceUrls string[]
status DisputeState OPEN β†’ UNDER_REVIEW β†’ RESOLVED

ProcessedWebhookEvent (cursor persistence β€” issue #77)

Field Type Notes
operationId string PK β€” Stellar operation ID
processedAt DateTime Default now()

VendorProfile

Field Type Notes
address string PK β€” Stellar public key
businessName string
email string?
phone string?
Relationships accountDetails, trackingSettings, escrows

Escrow State Machine

              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    create ──►│  FUNDED │──── cancel ───────────────────► CANCELLED
              β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
                   β”‚ ship (vendor)
                   β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚ SHIPPED β”‚
              β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
           β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
           β”‚ TrackingPollWorker              β”‚ buyer opens dispute
           β”‚ (every 10 min)                  β–Ό
           β–Ό                           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚ DISPUTED β”‚
       β”‚ DELIVERED β”‚                   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
       β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜                        β”‚ admin resolves
             β”‚ AutoReleaseWorker             β”‚
             β”‚ (48 h after delivery,    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
             β”‚  every 5 min)            β–Ό              β–Ό
             β–Ό                      RELEASED       REFUNDED
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚ COMPLETED β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Request Lifecycle

HTTP Request
  β”‚
  β”œβ”€ SecurityMiddleware          Sets CSP / HSTS headers
  β”œβ”€ LoggerMiddleware            Emits structured request log
  β”œβ”€ ThrottlerGuard (global)     Enforces per-route + per-IP limits (Redis storage when REDIS_URL is set, else in-memory)
  β”œβ”€ JwtGuard (route-level)      Validates SEP-10 JWT, injects AuthUser
  β”‚
  β”œβ”€ Controller                  Parses + validates DTO (ValidationPipe)
  β”œβ”€ EscrowService               Business logic, state validation
  β”‚    β”œβ”€ CacheService           Read: check Redis (escrow:{id}, 60s TTL)
  β”‚    β”œβ”€ EscrowRepository       DB read/write via PrismaService
  β”‚    β”‚    └─ CacheService      Write: invalidate Redis key after every mutation
  β”‚    └─ NotificationsService   Fire-and-forget email/SMS
  β”‚
  └─ GlobalExceptionFilter       Maps exceptions to RFC 7807 JSON responses

Standard Error Responses

All HTTP errors are normalized by GlobalExceptionFilter before they leave the API. Controllers and services should throw Nest HttpException subclasses (for example BadRequestException, UnauthorizedException, NotFoundException) and let the global filter write the response.

Every error response uses this envelope:

{
  "statusCode": 400,
  "message": "Validation failed",
  "error": "BadRequestException",
  "timestamp": "2026-06-27T12:00:00.000Z",
  "path": "/escrow",
  "requestId": "1a1d98a2-12c2-48a9-9b4d-36e568c89b5d"
}

message may be a string array for class-validator failures. requestId is resolved by RequestIdMiddleware from x-request-id or generated when absent, then echoed in both logs and error responses. In development only, the filter may also include details for diagnostics. Swagger registers the same contract as ErrorResponseDto and exposes a reusable StandardError response component.


Background Workers

Both workers implement OnModuleInit / OnApplicationShutdown for clean startup and graceful shutdown via clearInterval.

AutoReleaseWorker β€” every 5 minutes

  1. Query escrows in DELIVERED state where deliveredAt ≀ AUTO_RELEASE_WINDOW_HOURS (48 h) ago, no open dispute, no autoReleaseTxHash. The state must match what markDelivered writes, or the query matches nothing (issue #395).
  2. Call markAutoReleaseSubmitting(id) β€” sets autoReleaseSubmittedAt as an optimistic lock.
  3. Submit on-chain auto-release via ContractService.submitAutoRelease(escrowId).
  4. On success: recordAutoReleaseSubmission(id, txHash) β€” records the hash, leaves the state at DELIVERED. Submission is not confirmation, so the worker does not finalise; the AutoReleased chain event does, via markAutoReleased β†’ RELEASED plus notifyCompleted.
  5. On failure: clearAutoReleaseSubmitting(id) β€” unlocks for next cycle.

TrackingPollWorker β€” every 10 minutes

  1. Query all SHIPPED escrows with a trackingId.
  2. Call LogisticsService.getStatus(trackingId) for each.
  3. On DELIVERED status: markDelivered(id) + ContractService.recordDelivery(escrowId).
  4. Errors are caught per-escrow so one failure doesn't block the rest.

Stellar Webhook Flow (issue #76 + #77)

Horizon ──POST /webhooks/stellar──► StellarWebhookController
                                         β”‚
                                    verifySignature()
                                    (HMAC-SHA256 with STELLAR_WEBHOOK_SECRET)
                                         β”‚
                                    processedWebhookEvent.findUnique(operationId)
                                    ─── already seen? return {skipped: true} ───►
                                         β”‚ first time
                                    processedWebhookEvent.create(operationId)
                                    (DB-persisted cursor β€” survives restarts)
                                         β”‚
                                    processEvent(dto)
                                    ─── type === 'payment'? ──►
                                         β”‚
                                    escrowRepository.findByBuyer(dto.to)
                                    filter state === FUNDED
                                    escrowRepository.updateState(id, FUNDED)
                                         β”‚
                                    ─── error? delete cursor, rethrow ──────────►

Caching Strategy (issue #103)

  • Store: Redis (ioredis), configured via REDIS_URL.
  • Scope: GET /escrow/:id only β€” single-record lookups under burst traffic.
  • Key format: escrow:{uuid} β€” one key per escrow ID.
  • TTL: 60 seconds.
  • Read path: EscrowRepository.findById checks Redis before hitting PostgreSQL.
  • Invalidation: Every EscrowRepository write method (markShipped, markCancelled, markDelivered, markAutoReleased, updateState, etc.) calls cache.del(escrow:{id}) immediately after the DB mutation.
  • Graceful degradation: If REDIS_URL is not set, CacheService is a no-op β€” reads always hit PostgreSQL, no errors.

Authentication

SEP-10 Auth Flow

SEP-10 (Stellar Ecosystem Proposal 10) provides wallet-native authentication: the client proves ownership of a Stellar private key by signing a server-generated challenge transaction rather than submitting a password.

Challenge Request

Client                                Server (Sep10Controller)
  β”‚                                        β”‚
  │── POST /auth/challenge ──────────────► β”‚
  β”‚   { publicKey: "G..." }                β”‚
  β”‚                                        β”‚  1. WebAuth.buildChallengeTx()
  β”‚                                        β”‚     (serverKeypair signs the tx)
  β”‚                                        β”‚  2. Hash tx β†’ nonce
  β”‚                                        β”‚  3. prisma.nonce.create()
  β”‚                                        β”‚     { nonce, walletAddress,
  β”‚                                        β”‚       challenge (XDR), used=false,
  β”‚                                        β”‚       expiresAt = now + 900s }
  │◄── { transaction: "<XDR>",  ──────────│
  β”‚      network_passphrase }              β”‚

Verification and JWT Issuance

Client                                Server (Sep10Service)
  β”‚                                        β”‚
  β”‚  (client signs the XDR with its        β”‚
  β”‚   Stellar private key)                 β”‚
  β”‚                                        β”‚
  │── POST /auth ──────────────────────── β–Ίβ”‚
  β”‚   { transaction: "<signed XDR>" }      β”‚
  β”‚                                        β”‚  1. WebAuth.readChallengeTx()
  β”‚                                        β”‚     β†’ extracts clientAccountID
  β”‚                                        β”‚  2. Hash tx β†’ look up nonce record
  β”‚                                        β”‚  3. Reject if: not found | used
  β”‚                                        β”‚     | expired
  β”‚                                        β”‚  4. WebAuth.verifyChallengeTxSigners()
  β”‚                                        β”‚     confirms client signature
  β”‚                                        β”‚  5. nonce.update({ used: true })
  β”‚                                        β”‚  6. issueJwt(sub=clientAccountID)
  β”‚                                        β”‚     HS256, exp=now+3600s
  β”‚                                        β”‚     role="admin" if sub===ADMIN_ADDRESS
  β”‚                                        β”‚  7. Generate 32-byte refresh token,
  β”‚                                        β”‚     store HMAC-SHA256 hash in DB
  │◄── { token, refreshToken } ───────────│

Refresh Token Rotation

Client                                Server (Sep10Service)
  β”‚                                        β”‚
  │── POST /auth/refresh ────────────────►│
  β”‚   { refreshToken: "<hex>" }            β”‚
  β”‚                                        β”‚  1. Hash token β†’ look up in DB
  β”‚                                        β”‚  2. If revoked β†’ revokeTokenFamily()
  β”‚                                        β”‚     (revoke ALL tokens for userId)
  β”‚                                        β”‚     β†’ 401 Refresh token reuse detected
  β”‚                                        β”‚  3. If expired β†’ 401
  β”‚                                        β”‚  4. Mark old token revoked=true
  β”‚                                        β”‚  5. Generate new token pair,
  β”‚                                        β”‚     parentTokenId = old token id
  │◄── { token, refreshToken } ───────────│

Security Considerations

Threat Mitigation
Nonce reuse Each challenge nonce is stored with used=false; set to true after one successful verify. Replaying the same signed XDR is rejected.
Expired challenges expiresAt stored with the nonce (default 900 s). Server rejects if now > expiresAt.
Refresh token theft Rotation: each use issues a new token and revokes the old one. If a stolen token is replayed, the revoked=true check triggers full session revocation via revokeTokenFamily().
Token family escalation parentTokenId links child tokens to their parent so the entire chain can be revoked on reuse detection.
Privilege escalation JWT role: "admin" is only embedded when sub === ADMIN_ADDRESS (set via env var, not user-supplied).

Protected endpoints use @UseGuards(JwtGuard) + @CurrentUser() to extract the verified Stellar address from the JWT.


Security Controls

Layer Mechanism
Transport HTTPS enforced in production
Headers SecurityMiddleware: CSP, HSTS, X-Frame-Options
CORS ALLOWED_ORIGINS allowlist; blocks all in production if unset
Auth SEP-10 JWT on protected routes
Rate limiting ThrottlerGuard (@nestjs/throttler) with per-route @Throttle limits; Redis-backed storage when REDIS_URL is set
Webhook integrity HMAC-SHA256 signature verification (X-Stellar-Signature)
Input validation ValidationPipe (whitelist, transform, forbid extra fields)
SQL injection Parameterised queries via Prisma ORM

Environment Configuration Summary

Variable Required Default Purpose
DATABASE_URL Yes β€” PostgreSQL connection string
SEP10_JWT_SECRET Yes β€” JWT signing secret (min 32 chars)
ADMIN_ADDRESS Yes β€” Admin Stellar public key
PORT No 3000 HTTP listen port
NODE_ENV No development Runtime mode
STELLAR_NETWORK No TESTNET TESTNET or MAINNET
STELLAR_HORIZON_URL No Horizon default Horizon API base URL
STELLAR_WEBHOOK_SECRET No β€” HMAC secret for webhook verification
REDIS_URL No β€” Redis URL; caching disabled if absent
ALLOWED_ORIGINS No β€” Comma-separated CORS allowlist
SENDGRID_API_KEY No β€” Email notifications
TWILIO_ACCOUNT_SID No β€” SMS notifications
TWILIO_AUTH_TOKEN No β€” SMS notifications
LOG_LEVEL No info traceβ”‚debugβ”‚infoβ”‚warnβ”‚errorβ”‚fatal

See .env.example for format descriptions and example values.


Key File Locations

Path Purpose
src/app.module.ts Root module β€” all imports and global middleware
src/main.ts Bootstrap β€” CORS, ValidationPipe, graceful shutdown
src/config/ Joi-validated env config (ConfigModule, ConfigService)
src/escrow/ Core escrow CRUD, state machine, dispute endpoints
src/stellar/ Stellar contract calls (auto-release, delivery, dispute resolution)
src/webhooks/ Horizon webhook receiver with HMAC verification and DB cursor
src/workers/ AutoReleaseWorker (5 min) and TrackingPollWorker (10 min)
src/cache/ CacheService (Redis) and CacheModule
src/prisma/ PrismaService β€” in-memory mock for dev/tests
src/notifications/ SendGrid + Twilio dispatch with retry
src/auth/ SEP-10 challenge, JWT issuance, JwtGuard
src/common/ Guards, filters, middleware, logger
prisma/schema.prisma Prisma data model
prisma/migrations/ Versioned SQL migrations