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.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β β β β β β
β βββββββββββββββββ ββββββββββββββββββ ββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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
| 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 |
| 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 |
| Field | Type | Notes |
|---|---|---|
operationId |
string | PK β Stellar operation ID |
processedAt |
DateTime | Default now() |
| Field | Type | Notes |
|---|---|---|
address |
string | PK β Stellar public key |
businessName |
string | |
email |
string? | |
phone |
string? | |
| Relationships | accountDetails, trackingSettings, escrows |
βββββββββββ
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 β
βββββββββββββ
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
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.
Both workers implement OnModuleInit / OnApplicationShutdown for clean startup and graceful shutdown via clearInterval.
- Query escrows in
DELIVEREDstate wheredeliveredAtβ€AUTO_RELEASE_WINDOW_HOURS(48 h) ago, no open dispute, noautoReleaseTxHash. The state must match whatmarkDeliveredwrites, or the query matches nothing (issue #395). - Call
markAutoReleaseSubmitting(id)β setsautoReleaseSubmittedAtas an optimistic lock. - Submit on-chain auto-release via
ContractService.submitAutoRelease(escrowId). - On success:
recordAutoReleaseSubmission(id, txHash)β records the hash, leaves the state atDELIVERED. Submission is not confirmation, so the worker does not finalise; theAutoReleasedchain event does, viamarkAutoReleasedβRELEASEDplusnotifyCompleted. - On failure:
clearAutoReleaseSubmitting(id)β unlocks for next cycle.
- Query all
SHIPPEDescrows with atrackingId. - Call
LogisticsService.getStatus(trackingId)for each. - On
DELIVEREDstatus:markDelivered(id)+ContractService.recordDelivery(escrowId). - Errors are caught per-escrow so one failure doesn't block the rest.
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 βββββββββββΊ
- Store: Redis (ioredis), configured via
REDIS_URL. - Scope:
GET /escrow/:idonly β single-record lookups under burst traffic. - Key format:
escrow:{uuid}β one key per escrow ID. - TTL: 60 seconds.
- Read path:
EscrowRepository.findByIdchecks Redis before hitting PostgreSQL. - Invalidation: Every
EscrowRepositorywrite method (markShipped,markCancelled,markDelivered,markAutoReleased,updateState, etc.) callscache.del(escrow:{id})immediately after the DB mutation. - Graceful degradation: If
REDIS_URLis not set,CacheServiceis a no-op β reads always hit PostgreSQL, no errors.
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.
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 } β
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 } ββββββββββββ
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 } ββββββββββββ
| 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.
| 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 |
| 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.
| 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 |