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.
Incoming webhook deliveries must identify a configured endpoint and contain a
valid schema version, event type, delivery ID, and ISO-8601 sentAt timestamp.
The HMAC-SHA256 signature is checked before replay state is recorded. Invalid
signatures, malformed envelopes, unknown endpoints, missing secrets, stale
timestamps, and repeated delivery IDs are rejected without application
processing.
Outbound delivery attempts use exponential backoff with jitter. After
WEBHOOK_MAX_ATTEMPTS failures, the delivery is marked failed and copied to
the webhook dead-letter queue. Operators can inspect it through
GET /admin/webhooks/dead-letter and explicitly retry it with
POST /admin/webhooks/dead-letter/:id/retry. A retry creates a new delivery
attempt while retaining the original failure record for auditability.
The replay timestamp window is controlled by
WEBHOOK_SIGNATURE_MAX_SKEW_MS (default: 300000 ms). Consumers should return
a non-2xx response for invalid webhook requests; the sender treats non-2xx and
network/time-out failures as retryable until the dead-letter threshold is
reached.
- ✅ 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