NestJS API for milestone-based supply chain escrow on Stellar
This is Repo 2 of 3 in the ChainSettle project:
| Repo | Description |
|---|---|
chainsetttle-contract |
Soroban smart contract (Rust) |
chainsetttle-backend β you are here |
NestJS REST API + event poller |
chainsetttle-frontend |
React + Freighter wallet UI |
The backend is the bridge between the Stellar blockchain and the frontend. It does NOT hold user funds or sign transactions on behalf of users β all fund movements are handled by the on-chain contract. The backend:
- Stores off-chain metadata about shipments and users (PostgreSQL via Prisma)
- Polls Stellar RPC every 5 seconds for contract events and updates local state
- Sends in-app and email notifications to relevant parties when milestones change
- Provides a clean REST API for the frontend to query shipment state
- Issues JWT tokens via a Stellar address signature (no passwords)
- Exposes Swagger docs at
/docs
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NestJS Application β
β β
β βββββββββββ ββββββββββββ ββββββββββββ ββββββββββ β
β β Auth β βShipments β βMilestonesβ β Events β β
β β Module β β Module β β Module β β Module β β
β βββββββββββ ββββββββββββ ββββββββββββ ββββββββββ β
β β β
β Cron (5s poll) β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββ β
β β PrismaService β β StellarService β β
β β (PostgreSQL) β β (Soroban RPC client) β β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
PostgreSQL DB Stellar Testnet/Mainnet
(ChainSettle Contract)
| Module | Responsibility |
|---|---|
AuthModule |
Stellar address challenge-response auth β JWT |
ShipmentsModule |
CRUD for shipment records, sync from chain |
MilestonesModule |
Milestone state updates, proof hash storage |
EventsModule |
Stellar event poller (cron), event dispatch |
NotificationsModule |
In-app + email notifications via Nodemailer |
HealthModule |
/health endpoint for DB + service liveness |
PrismaModule |
Shared global DB client (PostgreSQL) |
StellarModule |
Shared global Stellar RPC client + utilities |
All endpoints are prefixed with /api/v1. Protected routes require Authorization: Bearer <JWT>.
| Method | Path | Description |
|---|---|---|
GET |
/auth/nonce?address=G... |
Get challenge nonce for a Stellar address |
POST |
/auth/login |
Submit signed nonce, receive JWT |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/shipments |
β | Register on-chain shipment in DB |
GET |
/shipments |
β | List shipments (filter by buyer, supplier, status) |
GET |
/shipments/:id |
β | Full shipment detail + milestones + events |
POST |
/shipments/:id/sync |
β | Force sync shipment from Stellar chain |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/shipments/:id/milestones |
β | List all milestones for a shipment |
GET |
/shipments/:id/milestones/:index |
β | Get single milestone |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/events |
β | List chain events (filter by shipmentId) |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/notifications |
β | Get user notifications |
PATCH |
/notifications/:id/read |
β | Mark notification as read |
PATCH |
/notifications/read-all |
β | Mark all as read |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/health |
β | Database + service health check |
chainsetttle-backend/
βββ .env.example β copy to .env and fill in values
βββ .gitignore
βββ nest-cli.json
βββ package.json
βββ tsconfig.json
βββ README.md
β
βββ prisma/
β βββ schema.prisma β Database schema (User, Shipment, Milestone, etc.)
β
βββ src/
βββ main.ts β App bootstrap (Swagger, CORS, pipes, guards)
βββ app.module.ts β Root module
β
βββ common/
β βββ prisma/
β β βββ prisma.module.ts
β β βββ prisma.service.ts β PrismaClient wrapper
β βββ stellar/
β β βββ stellar.module.ts
β β βββ stellar.service.ts β RPC client, event fetching, utilities
β βββ filters/
β β βββ http-exception.filter.ts β Standardised error responses
β βββ interceptors/
β β βββ transform.interceptor.ts β Wraps all responses in { success, data, timestamp }
β βββ guards/
β β βββ jwt-auth.guard.ts
β βββ decorators/
β βββ current-user.decorator.ts
β
βββ modules/
βββ auth/
β βββ auth.module.ts
β βββ auth.controller.ts
β βββ auth.service.ts β Nonce generation + JWT issuance
β βββ jwt.strategy.ts
β βββ dto/login.dto.ts
β
βββ shipments/
β βββ shipments.module.ts
β βββ shipments.controller.ts
β βββ shipments.service.ts
β βββ shipments.service.spec.ts β Unit tests
β βββ dto/create-shipment.dto.ts
β
βββ milestones/
β βββ milestones.module.ts
β βββ milestones.controller.ts
β βββ milestones.service.ts β DB updates triggered by chain events
β
βββ events/
β βββ events.module.ts
β βββ events.controller.ts
β βββ events.service.ts β Stellar RPC poller (cron every 5s)
β
βββ notifications/
β βββ notifications.module.ts
β βββ notifications.controller.ts
β βββ notifications.service.ts β In-app + email via Nodemailer
β
βββ health/
βββ health.module.ts
βββ health.controller.ts
- Node.js v20+
- pnpm (recommended) or npm
- PostgreSQL 15+
- Stellar CLI (only needed if deploying the contract)
npm install
# or
pnpm installcp .env.example .envEdit .env with your values:
DATABASE_URLβ your PostgreSQL connection stringJWT_SECRETβ a long random stringCHAINSETTTLE_CONTRACT_IDβ the deployed contract ID fromchainsetttle-contractSMTP_*β email credentials (use Gmail app password or any SMTP)
# Create and apply migrations
npx prisma migrate dev --name init
# Generate Prisma client
npx prisma generate
# (Optional) seed initial data
# npx prisma db seednpm run start:devAPI available at: http://localhost:3000/api/v1
Swagger docs at: http://localhost:3000/docs
# Unit tests
npm run test
# Unit tests with coverage
npm run test:cov
# Watch mode
npm run test:watchChainSettle uses a Sign-In With Stellar pattern β no passwords:
1. Frontend β GET /auth/nonce?address=GABC...
β { nonce: "chainsetttle:GABC...:1234567890:abc123" }
2. User signs the nonce with Freighter wallet
(Keypair.sign on the frontend)
3. Frontend β POST /auth/login
{ stellarAddress, signedNonce, signature }
β { accessToken: "eyJ..." }
4. All subsequent requests:
Authorization: Bearer eyJ...
The backend verifies the signature against the public key, then issues a JWT. Wire up the Keypair.verify() call in auth.service.ts before production.
The EventsService runs a cron job every 5 seconds using @nestjs/schedule. It:
- Calls
stellar.fetchContractEvents(lastProcessedLedger)via the Soroban RPC - Routes each event to the correct handler (e.g.
handleMilestoneConfirmed) - Updates Prisma DB records to reflect the new state
- Triggers notifications to relevant Stellar addresses
- Saves the raw event to
chain_eventsfor audit trail - Advances
lastProcessedLedgercursor
For production, persist lastProcessedLedger in the DB (or Redis) so it survives restarts.
All responses are wrapped by the global TransformInterceptor:
{
"success": true,
"data": { ... },
"timestamp": "2026-05-17T12:00:00.000Z"
}Errors follow a standardised format from HttpExceptionFilter:
{
"success": false,
"statusCode": 404,
"timestamp": "2026-05-17T12:00:00.000Z",
"path": "/api/v1/shipments/SHIP-999",
"message": "Shipment SHIP-999 not found"
}- Set
NODE_ENV=production - Use a strong
JWT_SECRET(32+ random chars) - Swap in-memory nonce store for Redis
- Persist
lastProcessedLedgerin DB (not memory) for crash recovery - Enable HTTPS (reverse proxy β nginx or Caddy)
- Set up Prisma connection pooling (PgBouncer)
- Wire up real Stellar
Keypair.verify()inauth.service.ts - Set
CORS_ORIGINto your production frontend URL - Add rate limiting tuning for production traffic
- Deploy via Docker (Dockerfile not included β straightforward to add)
| Variable | Required | Description |
|---|---|---|
NODE_ENV |
Yes | development or production |
PORT |
No | API port (default: 3000) |
DATABASE_URL |
Yes | PostgreSQL connection string |
JWT_SECRET |
Yes | Secret for signing JWTs |
JWT_EXPIRES_IN |
No | Token expiry (default: 7d) |
STELLAR_NETWORK |
Yes | testnet or mainnet |
STELLAR_RPC_URL |
Yes | Soroban RPC endpoint |
CHAINSETTTLE_CONTRACT_ID |
Yes | Deployed contract address |
USDC_TOKEN_ADDRESS |
Yes | USDC SAC address |
SMTP_HOST |
No | Email SMTP host |
SMTP_USER |
No | SMTP username |
SMTP_PASS |
No | SMTP password |
CORS_ORIGIN |
No | Allowed frontend origin |
EVENT_POLLING_INTERVAL_MS |
No | Cron interval in ms (default: 5000) |
MIT