A robust NestJS backend for StellarSwipe, integrating Stellar's blockchain infrastructure with Soroban smart contract support.
src/
├── config/ # Configuration modules
│ ├── stellar.config.ts # Stellar blockchain configuration
│ ├── stellar.service.ts # Stellar configuration service
│ ├── database.config.ts # Database and Redis configuration
│ └── app.config.ts # Application configuration
├── common/ # Shared utilities
│ ├── constants/ # Application constants
│ ├── decorators/ # Custom decorators (IsPublic, RateLimit)
│ ├── filters/ # Global exception filter
│ └── interceptors/ # Logging and transform interceptors
├── main.ts # Application bootstrap
└── app.module.ts # Root module
- Node.js 18+
- npm 9+
- Docker & Docker Compose (optional, for containerized development)
- PostgreSQL (or use Docker)
- Redis (or use Docker)
- Clone the repository:
git clone <repository-url>
cd stellarswipe-backend- Install dependencies:
npm install- Configure environment variables:
cp .env.example .env
# Edit .env with your configurationKey environment variables:
NODE_ENV: development | productionPORT: Server port (default: 3000)HOST: Server host (default: 0.0.0.0)LOG_LEVEL: debug | info | warn | error
DATABASE_HOST: Database hostDATABASE_PORT: Database port (default: 5432)DATABASE_USER: Database userDATABASE_PASSWORD: Database passwordDATABASE_NAME: Database name
REDIS_HOST: Redis hostREDIS_PORT: Redis port (default: 6379)REDIS_PASSWORD: Redis password (optional)
STELLAR_NETWORK: testnet | mainnet (default: testnet)STELLAR_HORIZON_URL: Horizon API URLSTELLAR_SOROBAN_RPC_URL: Soroban RPC endpointSTELLAR_NETWORK_PASSPHRASE: Network passphrase for signingSTELLAR_API_TIMEOUT: API timeout in ms (default: 30000)STELLAR_MAX_RETRIES: Max API retries (default: 3)
CORS_ORIGIN: Comma-separated allowed originsCORS_CREDENTIALS: Enable credentials (default: true)
Start development server:
npm run start:devThe API will be available at http://localhost:3000/api/v1
Start all services:
docker-compose up -dThis starts:
- PostgreSQL: Port 5432
- Redis: Port 6379
- NestJS App: Port 3000
View logs:
docker-compose logs -f appStop services:
docker-compose down# Development
npm run start:dev # Watch mode
npm run start:debug # Debug mode
npm start # Production mode
# Build
npm run build # Compile TypeScript
# Code Quality
npm run lint # Run ESLint
npm run lint:fix # Fix linting issues
npm run format # Format code with Prettier
# Testing
npm test # Run tests
npm run test:watch # Watch mode
npm run test:cov # Coverage report
npm run test:debug # Debug testshttps://horizon-testnet.stellar.org
https://soroban-testnet.stellar.org:443
Testnet (default):
- Network Passphrase:
Test SDF Network ; September 2015 - Horizon:
https://horizon-testnet.stellar.org - Soroban RPC:
https://soroban-testnet.stellar.org:443
Mainnet (when ready):
- Network Passphrase:
Public Global Stellar Network ; September 2015 - Horizon:
https://horizon.stellar.org - Soroban RPC:
https://soroban-mainnet.stellar.org:443
- Global Prefix:
/api/v1 - Exception Filter: Catches all errors and formats responses
- Interceptors: Logging and response transformation
- Validation: Automatic DTO validation
Success response:
{
"success": true,
"data": {
/* response data */
},
"timestamp": "2026-01-19T12:00:00.000Z"
}Error response:
{
"statusCode": 400,
"message": "Error message",
"timestamp": "2026-01-19T12:00:00.000Z",
"path": "/api/v1/endpoint"
}- TypeScript strict mode enabled
- No unused variables allowed
- Prettier integration for automatic formatting
- Semi-colons enabled
- Single quotes
- 80 character line width
- 2-space indentation
Check formatting:
npm run lint
npm run format- Configured with PostgreSQL
- Migrations support
- Automatic synchronization in development
- SSL support for production
Create migration:
npm run typeorm migration:create src/migrations/CreateUsersTableRun migrations:
npm run typeorm migration:run- TypeScript support via
ts-jest - Coverage reporting
Run tests:
npm test
npm run test:watch
npm run test:covnpm run builddocker build -t stellarswipe-backend:latest .
docker run -p 3000:3000 --env-file .env stellarswipe-backend:latest- Set
NODE_ENV=production - Configure all required environment variables
- Use strong database password
- Set Redis password
- Configure CORS appropriately
- Ensure PostgreSQL is running on configured host/port
- Ensure Redis is running and accessible
- Check network connectivity to Stellar endpoints
npm install
npm run build# Change PORT in .env or kill existing process
lsof -i :3000
kill -9 <PID>Async tasks are processed via BullMQ/Bull backed by Redis. Three priority tiers exist:
| Queue | Priority | Use case |
|---|---|---|
critical-queue |
1 (highest) | Market order execution, stop-loss triggers |
priority-queue |
10–100 | Limit orders, notifications, webhooks |
low-priority-queue |
1000 (lowest) | Analytics, leaderboard updates |
dead-letter |
— | Failed jobs after all retries exhausted |
notifications |
100 (NORMAL) | Async notification delivery |
PriorityQueueWorker consumes the priority tiers with a separate concurrency ceiling per workload category, so saturating one category never takes worker slots from the other:
| Category | Queues | Env var | Default |
|---|---|---|---|
| Latency-sensitive | critical-queue, priority-queue |
QUEUE_CONCURRENCY_LATENCY_SENSITIVE |
10 |
| Background | low-priority-queue |
QUEUE_CONCURRENCY_BACKGROUND |
2 |
Each ceiling is the maximum number of jobs of that category running at once per instance (shared across the category's queues). Both values must be integers between 1 and 1000 and are validated at startup. Feature modules register a handler per job type with PriorityQueueWorker.registerHandler(type, handler); jobs without a handler fail permanently and go to the DLQ.
All queued jobs use exponential backoff with 3 attempts by default:
attempts: 3, backoff: { type: 'exponential', delay: 2000ms }
Fatal errors (unauthorized, not found, validation failed) skip retries and go directly to the DLQ.
Jobs that exhaust all retries land in the dead-letter queue. Operators can:
# List dead-lettered jobs
GET /api/v1/admin/dead-letter
# Retry a specific DLQ entry
POST /api/v1/admin/dead-letter/:id/retry
# Discard a DLQ entry permanently
DELETE /api/v1/admin/dead-letter/:idJobs can outlive a deployment, so priority-queue payloads carry a schemaVersion (see PRIORITY_JOB_SCHEMA in src/queue/priority-queue.service.ts). Processors read job data through PriorityQueueService.readJobData(job), which migrates older payloads one version at a time to the current schema before the handler sees them. Payloads enqueued before versioning have no schemaVersion and are treated as version 1.
Changing the payload shape:
- Bump the schema's current version and add a migration from the previous version (
migrations[n]upgrades versionnton + 1). - Keep existing migrations so jobs from every earlier supported version still upgrade.
Unknown versions (newer than the running build, malformed, or with no migration path) fail with UnsupportedPayloadVersionError. readJobData discards the job from further retries, so it fails after a single attempt with that error as its failure reason. Because the error is a PermanentError, JobErrorHandler treats it as fatal and moves the job to the DLQ with an alert. The starvation sweep leaves such jobs in place instead of promoting them. After deploying a build that supports the version, retry the job from the DLQ.
# View all registered cron jobs and last execution status
GET /api/v1/jobs/status
# View execution history for a job
GET /api/v1/jobs/:name/history
# Trigger a job immediately
POST /api/v1/jobs/:name/trigger
# Pause / resume a job
POST /api/v1/jobs/:name/pause
POST /api/v1/jobs/:name/resumeEvery replica registers the same cron jobs, so each run takes a Redis lease (stellarswipe:lock:scheduled-job:<name>) first. Only the replica holding the lease executes the job; the others skip that tick. The lease is renewed while the job runs and released when it finishes. If a replica crashes mid-run, the lease expires after lockTtlMs (default 5 minutes, set per job in JobDefinition) and the next tick runs normally. If Redis is unreachable, the run is skipped rather than executed on every replica.
Queue connectivity is included in the readiness probe:
GET /api/v1/health/queue # queue-only check
GET /api/v1/health/ready # full readiness (DB + Redis + queue + blockchain)A 503 on /health/queue means Redis is unreachable (Bull uses Redis). Restore Redis connectivity — Bull reconnects automatically.
All cron schedules can be overridden via environment variables without code changes. See JobSchedulerService.register() for the cronEnvKey per job.
StuckJobDetectorService runs every 5 minutes and quarantines active jobs that exceed their registered maxDurationMs. Quarantined jobs are moved to failed state and logged with type: job_quarantined.
- Create feature branch:
git checkout -b feature/new-feature - Commit changes using the Conventional Commits format, e.g.
git commit -m 'feat(trades): add new feature' - Push to branch:
git push origin feature/new-feature - Submit Pull Request
See CONTRIBUTING.md for the full commit message format, allowed types, and examples.
Husky git hooks are installed automatically
when you run npm install (via the prepare script):
commit-msg— runs commitlint to reject any commit message that doesn't follow the Conventional Commits format.pre-push— runsnpm run lintandnpm run test:smokebefore a push is allowed.
You can bypass hooks in an emergency with git commit --no-verify /
git push --no-verify.
This project follows Conventional Commits (feat:, fix:, chore:, docs:, refactor:, perf:, test:, build:, ci:, etc.) for commit messages. CHANGELOG.md is generated automatically from this commit history using conventional-changelog-cli.
The .github/workflows/release.yml workflow runs semantic-release automatically on every push to main. It:
- Analyzes commit messages since the last tag to determine the next semantic version (
patch/minor/major). - Bumps
package.jsonversion and prepends an entry toCHANGELOG.md. - Creates a git tag (e.g.
v1.2.0) and a GitHub Release with generated notes. - Commits the updated
package.jsonandCHANGELOG.mdback tomain.
Required repository secret: GITHUB_TOKEN (provided automatically by GitHub Actions).
To verify what semantic-release would do without publishing anything:
npx semantic-release --dry-runSet CI=true in your shell if running outside of a CI environment. The dry-run prints the computed next version and release notes without creating tags, commits, or a GitHub Release.
When cutting a release manually:
-
Make sure commit messages since the last release follow the Conventional Commits format. Use a
BREAKING CHANGE:footer (or a!after the type/scope, e.g.feat!:) for any breaking change — these are surfaced in their own dedicated section of the changelog. -
Run:
npm run changelog
This parses commits since the last git tag (or the full history if no tag exists yet) and prepends a categorized entry (Features, Bug Fixes, Breaking Changes, etc.) to the top of
CHANGELOG.md. IfCHANGELOG.mddoes not exist, it will be created. -
Review the generated entry, then commit it together with the version bump:
git add CHANGELOG.md git commit -m "chore(release): update changelog" -
Tag the release (e.g.
git tag v1.2.0) so the next changelog run only picks up commits made after this point.
To regenerate the changelog for the entire commit history from scratch (e.g. to fix formatting), use:
npm run changelog:initThis rewrites CHANGELOG.md from the very first commit instead of appending only the new entries.
The .github/workflows/audit.yml workflow runs npm audit --audit-level=high on every PR and push to main, and on a daily schedule. The build fails when any high or critical severity vulnerability is introduced.
- Fix it — upgrade the affected package or its parent dependency.
- Accept the risk — if no fix is available (e.g. a transitive dependency you don't control), add the advisory ID to
.auditignorewith a mandatory comment explaining the reason and a review-by date. See.auditignorefor the required format.
Do not use --force or blanket-ignore the entire scan. Every suppression must be justified individually.
Regenerate the NestJS module dependency graph at any time:
npm run docs:module-graphThis scans all *.module.ts files under src/, extracts @Module imports arrays, and writes a Mermaid diagram to docs/module-graph.md.
The script exits with code 1 and prints the offending cycles if circular module dependencies are detected. Circular nodes are highlighted in red in the generated diagram.
- Look at the printed cycle (e.g.
A → B → C → A). - Extract the shared logic into a new
CommonModulethat both modules can import without creating a cycle. - Re-run
npm run docs:module-graphto confirm the cycle is gone.
The docker-compose.yml defines profiles so you can start only the services a given module needs instead of the full stack.
| Profile | Services started | Use case |
|---|---|---|
| (none — default) | app + postgres + redis | Full local development |
db-only |
postgres | DB-focused module tests |
cache-only |
redis | Cache / session module tests |
# Full stack (default)
docker-compose up -d
# Postgres only
docker-compose --profile db-only up -d
# Redis only
docker-compose --profile cache-only up -d- @nestjs/common: NestJS core functionality
- @nestjs/config: Environment configuration management
- @stellar/stellar-sdk: Stellar blockchain SDK
- @soroban-js/stellar-sdk: Soroban smart contract SDK
- typeorm: ORM for database operations
- ioredis: Redis client
- class-validator: DTO validation
- typescript: 5.3.3
- @nestjs/cli: NestJS CLI tools
- eslint: Code linting
- prettier: Code formatting
- jest: Testing framework
MIT
- #1092: Implement structured request ID correlation logging
- #1069: NestJS: Centralize authorization policies in guards
- #1147: Define transaction boundaries for multi-step service commands
- #1149: Introduce DataLoader batching for GraphQL resolvers
- #1160: Enforce resource authorization with reusable NestJS guards
- #1162: Add dead-letter handling for failed background jobs
- #1167: Add optimistic concurrency control for mutable resources
- #1237: Add Redis failover behavior tests for cache-backed services
- #1238: Add correlation context to scheduled and queued task logs
- #1303: Add request locale negotiation guardrails
- #1304: Add GraphQL query complexity limits by authenticated role
- #1305: Add GraphQL request depth limit configuration
- #1306: Add HTTP request timeout handling for long-running endpoints
- #1281: Prevent duplicate withdrawal submissions under concurrency
- #1282: Add withdrawal destination allowlist management
- #1283: Add transaction memo policy validation by operation type
- #1284: Handle Stellar transaction fee changes before submission
- #1285: Add configurable maximum age for Stellar transaction envelopes
- #1286: Validate Soroban authorization expiration before submission
- #1287: Add Stellar asset registry change audit events
- #1288: Add asset registry cache invalidation on updates
- #1200: Add contract tests for generated OpenAPI and SDK types
- #1204: Add wallet ownership verification before signing requests
- #1205: Implement wallet unlinking with active-position safeguards
- #1206: Validate Stellar network selection across transaction endpoints
- #1207: Add Soroban simulation result caching with ledger-aware invalidation
- #1208: Persist Horizon cursor checkpoints for resumable ingestion
- #1214: Redact sensitive fields from audit event payloads
- #1216: Enforce tenant context propagation in asynchronous jobs
- #992: Implement a scheduled expiry job for stale, un-swiped Trade Signals
- #1194: Integrate slippage calculations with live market data
- #1198: Add retry and dead-letter handling to search indexing
- #1199: Add idempotency keys to payout endpoints
- #1217: Add request-level authorization tests for admin mutations
- #1218: Prevent mass assignment in user profile updates
- #1271: Implement provider onboarding state machine
- #1272: Add provider compliance document expiry tracking
- #1273: Require verified email before API key creation
- #1274: Add API key rotation with overlap window
- #1275: Enforce API key expiration during authentication
- #1276: Add API key last-used tracking without excessive writes
- #1297: Add batch limits and validation to portfolio imports
- #1298: Implement asynchronous portfolio import processing
- #1289: Support provider profile change notifications
- #1290: Add provider suspension workflow and access enforcement
- #1291: Add signal publication limits per provider
- #1292: Add signal expiration policy validation