Express.js backend server for YieldVault Stellar RWA platform with rate limiting and health monitoring.
- Health Check Endpoint (
/health) - Real-time service health status - Readiness Endpoint (
/ready) - Dependency status for deployment orchestration - Rate Limiting - Per-IP and per-API-key rate limiting to prevent abuse
- Dependency Monitoring - Checks for cache and Stellar RPC availability
- Admin Audit Logs - Tracks privileged admin actions via
/admin/audit-logs - Event Replay System - Recovers from polling gaps by replaying missed on-chain events
- Background Job Dashboard - Monitoring views at
/admin/jobs/dashboardand/admin/jobs/dashboard/view - Prisma Runtime Tuning - Configurable pooling and query timeouts
- Error Handling - Consistent JSON error responses
- TypeScript - Full type safety with TypeScript
# Install dependencies
npm install
# Create environment file
cp .env.example .env
# Create/update the local Prisma database
npx prisma migrate dev# Start development server with auto-reload
npm run devThe server will start on http://localhost:3000.
For the default local workflow, PostgreSQL and Redis are optional:
- Prisma uses the SQLite datasource in
prisma/schema.prismawhenDATABASE_URLis not set. - Redis-backed features fall back to in-memory behavior when
REDIS_URLis not configured.
Minimum local environment values:
PORT=3000
NODE_ENV=development
STELLAR_RPC_URL=https://soroban-testnet.stellar.org
STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
VAULT_CONTRACT_ID=For the full monorepo bootstrap order, see docs/LOCAL_DEVELOPMENT_QUICKSTART.md.
# Build TypeScript
npm run build
# Start production server
npm startRate limiting and other settings are configurable via environment variables:
| Variable | Default | Description |
|---|---|---|
PORT |
3000 | Server port |
NODE_ENV |
development | Environment mode |
RATE_LIMIT_WINDOW_MS |
900000 | Global rate limit window (15 min) |
RATE_LIMIT_MAX_REQUESTS |
100 | Global requests per window |
API_RATE_LIMIT_WINDOW_MS |
60000 | API rate limit window (1 min) |
API_RATE_LIMIT_MAX_REQUESTS |
30 | API requests per window |
RATE_LIMIT_AUTH_MAX |
5 | Auth tier request limit per window (1 min) |
RATE_LIMIT_AUTH_WINDOW_MS |
60000 | Auth tier window duration in ms |
DEPOSITS_RATE_LIMIT_MAX |
10 | Deposit/transfer tier request limit per window (1 min) |
DEPOSITS_RATE_LIMIT_WINDOW_MS |
60000 | Deposit/transfer tier window duration in ms |
STELLAR_RPC_URL |
https://soroban-testnet.stellar.org | Stellar RPC endpoint |
DATABASE_URL |
local PostgreSQL URL | Primary PostgreSQL connection string (required in production) |
DATABASE_REPLICA_URL |
primary database | Optional read-replica connection string |
DATABASE_POOL_SIZE |
10 | Maximum connections per PostgreSQL pool |
PRISMA_POOL_MAX |
10 | Prisma connection pool max size |
PRISMA_POOL_TIMEOUT_MS |
10000 | Prisma pool wait timeout in ms |
PRISMA_QUERY_TIMEOUT_MS |
5000 | Max Prisma query time in ms |
ADMIN_AUDIT_LOG_STORAGE |
hybrid | Audit log storage mode (memory, prisma, hybrid) |
EVENT_POLL_INTERVAL_MS |
10000 | Event polling interval (10 seconds) |
EVENT_REPLAY_BATCH_SIZE |
100 | Batch size for event replay (ledgers per batch) |
GET /health
Returns service health status with dependency checks.
Response (200 OK):
{
"status": "healthy",
"timestamp": "2026-03-26T10:30:00.000Z",
"uptime": 3600.5,
"environment": "development",
"checks": {
"api": "up",
"cache": "up",
"stellarRpc": "up"
}
}GET /ready
Returns service readiness state. Checks all critical dependencies before reporting ready.
Response (200 OK - Ready):
{
"ready": true,
"timestamp": "2026-03-26T10:30:00.000Z",
"dependencies": {
"cache": true,
"stellarRpc": true
}
}GET /admin/audit-logs
Authorization: ApiKey <admin-key>
Returns recent admin activities with optional filters: action, actor, statusCode, and limit.
Protected routes (POST /api/v1/auth/login, vault deposits/withdrawals) accept single-use server nonces:
POST /api/v1/auth/noncewith{ walletAddress, action }→{ nonce, message, expiresAt, expiresIn }- Sign
messagewith the wallet (Ed25519 in production, HMAC in dev/test) - Submit the action with
{ walletAddress, nonce, signature, ... }
| Error | HTTP | code |
|---|---|---|
| Missing nonce/signature | 400 | SIGNED_ACTION_REQUIRED |
| Unknown/mismatched nonce | 401 | NONCE_NOT_FOUND / NONCE_ACTION_MISMATCH |
| Expired nonce | 401 | NONCE_EXPIRED |
| Reused nonce | 401 | NONCE_REPLAY |
| Bad signature | 401 | SIGNATURE_INVALID |
| Active nonce cap reached | 429 | NONCE_LIMIT_EXCEEDED |
Configure via WALLET_NONCE_ENFORCEMENT (strict in production) and WALLET_SIGNATURE_MODE (stellar | hmac).
Nonce allocation and consumption are atomic. With REDIS_URL configured, the
backend enforces single-use consumption and the per-wallet active nonce cap
across all replicas. Without Redis those guarantees apply only within one
backend process, so multi-instance production deployments must configure the
shared Redis store.
All /admin/* routes require Authorization: ApiKey <key>. Keys are assigned one of four roles (least → most privileged):
| Role | Capabilities |
|---|---|
viewer |
Read-only admin endpoints (metrics, audit logs, config snapshots) |
operator |
Viewer + operational writes (maintenance, cache, allowlist, webhooks, jobs, exports) |
admin |
Operator + privileged webhook url/secret updates and API key lifecycle |
super-admin |
Admin + impersonation, global idempotency flush, minting super-admin keys |
Forbidden requests return 403 with requiredPermission in the JSON body. Maintenance and webhook PATCH bodies are validated so privileged parameters (enabled, url, secret, etc.) require the matching permission tier.
GET /admin/jobs/dashboard
GET /admin/jobs/dashboard/view
Authorization: ApiKey <admin-key>
Exposes dead-letter metrics, recurring failures, job runtime telemetry, and health status.
Response (503 Unavailable - Not Ready):
{
"ready": false,
"timestamp": "2026-03-26T10:30:00.000Z",
"dependencies": {
"cache": false,
"stellarRpc": false
}
}Status: 429 Too Many Requests
{
"error": "Too many requests",
"status": 429,
"message": "Rate limit exceeded. Please try again later.",
"retryAfter": 1711432200000
}Applied to all requests except /health and /ready:
- Window: 15 minutes (configurable)
- Max: 100 requests per window (configurable)
- Per: IP address
Stricter limits for API endpoints (e.g., /api/vault/summary):
- Window: 1 minute (configurable)
- Max: 30 requests per window (configurable)
- Per: API key (from
x-api-keyheader) or IP address
# Apply native PostgreSQL migrations
npm run db:migrate
# Verify applied migration checksums and required tables
npm run db:check-drift
# Run all tests
npm test
# Run tests in watch mode
npm test -- --watch
# Run with coverage
npm test -- --coverageCommitted JSON snapshots under schema-snapshots/ describe the response shape of critical public endpoints. CI fails when a required field is removed or changes type.
Guarded endpoints: GET /health, GET /ready, GET /api/v1/vault/summary, GET /api/v1/transactions
# Verify snapshots are backward-compatible (CI)
npm run snapshots:check
# Regenerate after an intentional breaking API change
npm run snapshots:writeWhen bumping snapshots intentionally:
- Update the Zod schema in
src/apiContractSnapshots.ts - Run
npm run snapshots:writeand commitschema-snapshots/*.json - Align OpenAPI annotations and run
npm run generate:openapi
Wallet alias identity groups are persisted in Prisma and loaded into the
walletAliasService cache on startup. New aliases registered through auth or
/api/v1/wallet-aliases/link survive backend restarts and continue to resolve
for referral attribution.
Environments that relied on the previous in-memory-only alias registry do not have a durable source to backfill from after a restart. If those aliases matter, export or re-register the live mappings before deploying/restarting; otherwise the persistent tables will start empty and only new registrations will be preserved.
- ✅ Global rate limiting per IP
- ✅ Per-user/API-key rate limiting
- ✅ Configurable via environment variables
- ✅ Clear 429 responses with retry information
- ✅ Tests included for rate limiting behavior
- ✅
/healthendpoint for service health - ✅
/readyendpoint for deployment readiness - ✅ Dependency health checks (cache, RPC)
- ✅ CI smoke test setup via npm scripts
- ✅ Consistent response formats
# Build and start server
npm run test:smoke
# The server will start in background, ready for health checks
# Call: curl http://localhost:3000/health
# Call: curl http://localhost:3000/readyExample Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))"
CMD ["npm", "start"]Headers returned in responses:
RateLimit-Limit- Request limitRateLimit-Remaining- Requests remainingRateLimit-Reset- Reset timestamp
Example:
RateLimit-Limit: 100
RateLimit-Remaining: 95
RateLimit-Reset: 1711432200
MIT