//Comment API gateway and server for LiquiFact, the global invoice liquidity network on Stellar. This repo provides the Express-based REST API for invoice uploads, escrow state, and future Stellar integration.
Part of the LiquiFact stack: frontend (Next.js) | backend (this repo) | contracts (Soroban).
- Node.js 20+ (LTS recommended)
- npm 9+
- Docker & Docker Compose (for local PostgreSQL)
-
Clone the repo
git clone <this-repo-url> cd liquifact-backend
-
Install dependencies
npm install
-
Configure environment
cp .env.example .env # Edit .env with your database configuration -
Start database services
docker-compose -f docker-compose.dev.yml up -d
-
Run database migrations
npm run db:migrate
Optional Sentry error tracking is supported through the SENTRY_DSN environment variable. When enabled, the server scrubs sensitive values before sending events, including:
- Invoice payload bodies and invoice-related fields
- Authorization headers and bearer tokens
- API keys and secret values
- Stellar XDR / Stellar-specific payloads
Environment variables:
SENTRY_DSN— Optional Sentry DSN. Example:https://<PUBLIC_KEY>@o<ORG_ID>.ingest.sentry.io/<PROJECT_ID>SENTRY_RELEASE— Optional release tag. Defaults to package version when available.SENTRY_ENVIRONMENT— Optional environment tag. Defaults toNODE_ENV.
Do not store secrets in source control. Use .env locally and deployment secrets in production.
The API enforces a strict matching between STELLAR_NETWORK and SOROBAN_RPC_URL at boot time. This prevents misconfiguration where a passphrase (network identity) is paired with an incompatible RPC endpoint, which would cause on-chain validation failures.
| Network | Passphrase | RPC URL |
|---|---|---|
| TESTNET | Test SDF Network ; September 2015 |
https://soroban-testnet.stellar.org |
| MAINNET | Public Global Stellar Network ; September 2014 |
https://soroban.stellar.org |
| FUTURENET | Test SDF Future Network ; October 2022 |
https://rpc-futurenet.stellar.org |
Set both variables in your .env:
STELLAR_NETWORK=TESTNET
SOROBAN_RPC_URL=https://soroban-testnet.stellar.orgDo NOT use custom RPC URLs. The validation will reject any deviation from the expected RPC for the selected network.
On startup, src/index.js calls validateStellarConfig() from src/config/stellar.js. If the network/RPC combination is invalid, the server fails to start with a clear error message:
Error: Mismatch: STELLAR_NETWORK=TESTNET requires SOROBAN_RPC_URL="https://soroban-testnet.stellar.org", but got "https://custom-rpc.example.com". This combination would cause on-chain validation failures.
- The validation is a hard fail - no partial or degraded operation is permitted.
- This ensures the backend never signs transactions with a mismatched network, which could result in fund loss.
- The passphrase is derived from the network constant and is not user-configurable.
| Command | Description |
|---|---|
npm run dev |
Start API with watch mode |
npm run dev:ts |
Start API with TS runtime (optional) |
npm run start |
Start API |
npm run typecheck |
Run TypeScript type checking (no emit) |
npm run build |
Compile src/ to dist/ |
npm run start:dist |
Start compiled output from dist/ |
npm run lint |
Run ESLint on src/ |
npm test |
Run load helper tests and structured error tests |
npm run db:migrate |
Run database migrations |
npm run db:rollback |
Rollback last migration |
npm run db:seed |
Run database seeds |
npm run db:migrate:down |
Rollback last migration |
npm run db:migrate:create <name> |
Create new migration file |
npm run db:migrate:reset |
Reset database (drop & re-run) |
npm run test:coverage |
Run helper/API tests with coverage |
npm run load:baseline |
Run the core endpoint load baseline suite |
Default port: 3001.
Escrow Redis cache is optional and disabled by default; set REDIS_ESCROW_CACHE_ENABLED=true with REDIS_URL to enable it.
REDIS_ESCROW_CACHE_TTL_SECONDS is strictly clamped to 5..300, and REDIS_ESCROW_LEDGER_GAP_THRESHOLD controls ledger-gap invalidation.
Incremental TypeScript setup and migration guidance lives in docs/typescript-plan.md.
This project uses node-pg-migrate for database schema management with PostgreSQL. The migration system provides:
- SQL-first migration control with rollback support
- Multi-tenant architecture with Row Level Security (RLS)
- Production-safe transaction handling
- Comprehensive audit logging
# Start PostgreSQL and Redis
docker-compose -f docker-compose.dev.yml up -d
# Run migrations
npm run db:migrate- Multi-tenant isolation with tenant-scoped data
- Soft deletes for data recovery
- Audit trail for compliance
- UUID primary keys for distributed systems
- JSONB metadata for schema flexibility
📖 Full documentation: See DB_MIGRATIONS.md for comprehensive migration guide, troubleshooting, and deployment procedures.
The API is documented using OpenAPI 3.0 specification.
- OpenAPI JSON:
GET /openapi.json- Machine-readable API specification - Interactive Docs:
GET /docs- Swagger UI for exploring and testing the API - Correlation Strategy: See
docs/invoice-correlation.mdfor details on howinvoiceIdcorrelates with on-chain Stellar and Soroban data.
The documentation covers all public endpoints including health checks, invoice management, escrow operations, and investment opportunities.
- Marketplace:
GET /api/marketplace- Search and sort invoices by yield, maturity, and funded ratio. Supports advanced filtering (yieldBpsMin,maturityDateTo,fundedRatioMin, etc.) and pagination.
Example:
curl -H "Authorization: Bearer <token>" \
"http://localhost:3001/api/marketplace?yieldBpsMin=500&sortBy=yield_bps&order=desc"Core routes currently covered:
- Health:
GET /health - API Info:
GET /api - Invoices:
GET /api/invoices(with optional status filter),GET /api/invoices/:id,POST /api/invoices - Escrow:
GET /api/escrow/:invoiceId,POST /api/escrow - Investment:
GET /api/invest/opportunities - SME Metrics:
GET /api/sme/metrics
liquifact-backend/
├── src/
│ └── index.js
├── tests/
│ └── load/
│ ├── config.js
│ ├── reporter.js
│ ├── run-baselines.js
│ └── *.test.js
├── .env.example
├── eslint.config.js
└── package.json
For the full end-to-end model (indexer → projection → GET /api/escrow, funding via escrowSubmit, reconciliation, signing modes, and env contracts), see docs/escrow-integration-overview.md.
The API supports invoice-to-escrow contract address resolution using environment-based configuration for early phases. This allows mapping invoice IDs to their corresponding Stellar escrow contract addresses without requiring on-chain registry lookups.
Configure escrow mappings using the ESCROW_ADDR_BY_INVOICE environment variable:
ESCROW_ADDR_BY_INVOICE='{"mappings":[{"invoiceId":"inv_demo_001","escrowAddress":"GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890ABCDEFGHIJKLM","environment":"development","isActive":true}],"defaultEnvironment":"development","allowlistEnabled":true,"cacheEnabled":true,"cacheTtlSeconds":300}'- Allowlist Validation: Only mapped invoices can be resolved
- Environment Separation: Different mappings for development, staging, production
- Address Validation: Ensures Stellar addresses are properly formatted
- Caching: In-memory caching with configurable TTL
- Input Validation: Strict validation of invoice IDs and addresses
The mapping system is automatically used by escrow endpoints. When resolving /api/escrow/:invoiceId, the system:
- Validates the invoice ID format
- Checks if the invoice is in the allowlist for the current environment
- Returns the corresponding Stellar escrow contract address
- Caches the result for subsequent requests
For production deployments:
- Environment Separation: Use different mappings per environment
- Key Rotation: Update mappings by modifying the environment variable
- Monitoring: Use health checks to validate mapping configuration
- Security: Only map invoices you own or have explicit permission to map
{
"mappings": [
{
"invoiceId": "inv_123",
"escrowAddress": "GABC...123",
"environment": "development",
"isActive": true
}
],
"defaultEnvironment": "development",
"allowlistEnabled": true,
"cacheEnabled": true,
"cacheTtlSeconds": 300
}The repo includes a focused load baseline suite for representative core endpoint reads:
GET /healthGET /api/invoicesGET /api/escrow/:invoiceId
The suite uses autocannon and captures:
- total requests
- throughput in requests per second
- average latency
- p50 latency
- p95 latency
- p99 latency
- error count
- non-2xx count
- timeout count
These are the canonical health, invoices, and escrow endpoints currently exposed by the backend. They provide a low-risk baseline for throughput and latency without introducing destructive writes.
The load suite is intentionally safe by default:
- it targets
http://127.0.0.1:3001 - it blocks remote targets unless
ALLOW_REMOTE_LOAD_BASELINES=true - it does not hardcode tokens or credentials
- it uses a placeholder escrow invoice id unless a fixture id is provided
Do not run the suite against production without explicit approval.
| Variable | Default | Purpose |
|---|---|---|
LOAD_BASE_URL |
http://127.0.0.1:3001 |
Base URL for the load target |
ALLOW_REMOTE_LOAD_BASELINES |
false |
Explicit opt-in for non-local targets |
LOAD_DURATION_SECONDS |
15 |
Duration per endpoint scenario |
LOAD_CONNECTIONS |
10 |
Concurrent connections per scenario |
LOAD_TIMEOUT_SECONDS |
10 |
Request timeout |
LOAD_AUTH_TOKEN |
unset | Optional bearer token for protected endpoints |
LOAD_ESCROW_INVOICE_ID |
placeholder-invoice |
Escrow fixture id |
LOAD_REPORT_DIR |
tests/load/reports |
Directory for generated reports |
-
Start the API locally:
npm run dev
-
In another terminal, run the baseline suite:
npm run load:baseline
-
Optional example with custom settings:
LOAD_DURATION_SECONDS=20 LOAD_CONNECTIONS=25 LOAD_ESCROW_INVOICE_ID=invoice-123 npm run load:baseline
The repository includes a reproducible one-command E2E smoke test script that uses Docker Compose to spin up a fully isolated environment including the API, a test Postgres database, and a mocked Soroban RPC server.
- Service health:
/health(verifies API, DB reachability, and Soroban mock integration). - Versioned API:
GET /v1/escrow/:invoiceId(verifies token authentication and Soroban mock state). - Backward compatibility:
GET /api/escrow/:invoiceId(verifies deprecation warning headers).
Ensure you have Docker and Docker Compose installed.
npm run e2e:apiThe script will:
- Build and start the
api,db, andmock-sorobanservices. - Wait for the API to report a healthy status.
- Run the Jest smoke test suite against the live containers.
- Clean up (shutdown and remove) the containers and volumes.
- Isolated Environment: Uses a dedicated
docker-compose.e2e.ymland a private network. - Mocked Dependencies: Points
SOROBAN_RPC_URLto a local mock server to ensure tests are fast, deterministic, and don't require external network access. - Fail-Fast Healthchecks: The API and DB services use Docker healthchecks to ensure dependent services only start when their dependencies are ready.
Each run generates:
- a JSON artifact
- a Markdown artifact
- a console summary
By default, reports are written to:
tests/load/reports/
│ ├── config/ │ │ └── cors.js # CORS allowlist parsing and policy │ ├── middleware/ │ │ ├── auth.js # JWT authentication middleware │ │ ├── audit.js # Immutable audit logging for mutations │ │ ├── deprecation.js # API deprecation notices │ │ ├── errorHandler.js # Centralized error handling │ │ ├── rateLimit.js # Rate limiting enforcement │ │ └── stacks.js # Composed middleware stacks (authenticatedTenantStack, adminStack) │ ├── services/ │ │ ├── invoiceService.js # Business logic and pagination │ │ └── soroban.js # Contract interaction wrappers │ ├── utils/ │ │ ├── asyncHandler.js # Express async error wrapper │ │ └── retry.js # Exponential backoff utility │ ├── app.js # Express app, middleware, routes │ └── index.js # Runtime bootstrap ├── tests/ │ ├── setup.js # Test configuration │ ├── helpers/ │ │ └── createTestApp.js # Test app factory │ ├── unit/ │ │ ├── asyncHandler.test.js │ │ └── errorHandler.test.js │ └── app.test.js ├── .env.example # Env template ├── eslint.config.js └── package.json
---
## Resiliency & Retries
### Security notes
- Remote load targets are blocked by default.
- Secrets and tokens must come from environment variables.
- The suite never prints auth tokens.
- If protected endpoints are added later, use least-privilege non-production credentials.
- The selected baseline endpoints are low-risk reads to avoid destructive behavior.
### Edge cases handled
- missing base URL falls back to a safe local default
- remote targets require explicit opt-in
- invalid concurrency, duration, or timeout values are rejected
- missing auth token is handled gracefully
- missing escrow fixture id falls back to a placeholder
- partial endpoint failures are still captured in the report
### Limitations
- This suite establishes baselines, not maximum capacity.
- Results depend on local machine resources and runtime conditions.
- The invoices and escrow endpoints are currently placeholders, so these baselines should be treated as early reference points rather than production sizing data.
---
## Structured API errors
All API failures now return a consistent structured error payload:
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Malformed JSON request body.",
"correlation_id": "req_f7d1b9f6c0f1459d8b3b7b6a",
"retryable": false,
"retry_hint": "Fix the JSON payload and try again."
}
}
code: stable machine-readable error codemessage: safe human-readable messagecorrelation_id: per-request identifier for debugging and supportretryable: whether the caller may safely retryretry_hint: safe retry guidance
VALIDATION_ERRORAUTHENTICATION_REQUIREDFORBIDDENNOT_FOUNDUPSTREAM_ERRORINTERNAL_SERVER_ERROR
- Every request receives a correlation ID.
- The API returns it in both the response body and the
X-Correlation-Idheader. - If a client sends
X-Correlation-Idand it matches the accepted pattern, the value is echoed back. - Invalid client-supplied IDs are ignored and replaced with a generated ID.
The centralized mapper covers:
- malformed JSON
- validation failures
- authorization and authentication failures
- not found responses
- upstream connection failures
- unexpected thrown errors
- non-
Errorthrown values
- Internal stack traces and raw exception details are never returned to clients.
- Correlation IDs are sanitized and do not expose internal state.
- Retry hints are generic and do not leak infrastructure details.
- Server-side logs include correlation context without returning sensitive internals in responses.
Validation error:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invoice payload must be a JSON object.",
"correlation_id": "req_d3b92b4d2d554f33b8d8b089",
"retryable": false,
"retry_hint": "Send a valid JSON object in the request body and try again."
}
}Unexpected error:
{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "An internal server error occurred.",
"correlation_id": "req_3d5d8c9e4ff34dd9aa73b946",
"retryable": false,
"retry_hint": "Do not retry until the issue is resolved or support is contacted."
}
}The backend now supports a database-backed append-only audit log for:
- admin actions (for example, KYC state transitions or key-rotation operations)
- webhook dispatch outcomes (success/failure with redacted payload fields)
Run SQL migrations in order:
migrations/202604260001_create_audit_log_events.sqlmigrations/202604260002_enforce_audit_log_append_only.sql
audit_log_events is enforced as append-only at the database layer via triggers that reject UPDATE and DELETE.
src/middleware/auditLog.jsattachesreq.audithelpers:req.audit.logAdminAction(...)req.audit.logWebhookDelivery(...)
- successful
POST|PUT|PATCH|DELETErequests under/api/admin/*are auto-logged - sensitive fields are redacted before persistence (
password,token,secret,apiKey,privateKey, etc.)
Admin action example:
curl -X POST http://localhost:3001/api/admin/kyc/cus_42/approve \
-H "Authorization: Bearer <admin-jwt>" \
-H "x-admin-action: kyc.approve" \
-H "x-audit-target-type: kyc_profile" \
-H "x-audit-target-id: cus_42" \
-H "Content-Type: application/json" \
-d '{"reason":"manual review","privateKey":"redacted-at-write-time"}'Webhook delivery logging is typically called internally from delivery workers/routes via req.audit.logWebhookDelivery(...).
The repo includes a focused load baseline suite for representative core endpoint reads:
GET /healthGET /api/invoicesGET /api/escrow/:invoiceId
The suite uses autocannon and captures:
- total requests
- throughput in requests per second
- average latency
- p50 latency
- p95 latency
- p99 latency
- error count
- non-2xx count
- timeout count
- targets
http://127.0.0.1:3001 - blocks remote targets unless
ALLOW_REMOTE_LOAD_BASELINES=true - does not hardcode tokens or credentials
- uses a placeholder escrow invoice id unless a fixture id is provided
Do not run the suite against production without explicit approval.
| Variable | Default | Purpose |
|---|---|---|
LOAD_BASE_URL |
http://127.0.0.1:3001 |
Base URL for the load target |
ALLOW_REMOTE_LOAD_BASELINES |
false |
Explicit opt-in for non-local targets |
LOAD_DURATION_SECONDS |
15 |
Duration per endpoint scenario |
LOAD_CONNECTIONS |
10 |
Concurrent connections per scenario |
LOAD_TIMEOUT_SECONDS |
10 |
Request timeout |
LOAD_AUTH_TOKEN |
unset | Optional bearer token for protected endpoints |
LOAD_ESCROW_INVOICE_ID |
placeholder-invoice |
Escrow fixture id |
LOAD_REPORT_DIR |
tests/load/reports |
Directory for generated reports |
npm run dev
npm run load:baseline- Remote load targets are blocked by default.
- Secrets and tokens must come from environment variables.
- The suite never prints auth tokens.
- The selected baseline endpoints are low-risk reads.
All API failures return a structured error payload:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Malformed JSON request body.",
"correlation_id": "req_f7d1b9f6c0f1459d8b3b7b6a",
"retryable": false,
"retry_hint": "Fix the JSON payload and try again."
}
}VALIDATION_ERRORAUTHENTICATION_REQUIREDINVALID_TOKENFORBIDDENNOT_FOUNDRATE_LIMITEDUPSTREAM_ERRORINTERNAL_SERVER_ERROR
- Internal stack traces and raw exception details are never returned to clients.
- Correlation IDs are sanitized.
- Retry hints are generic and do not leak infrastructure details.
The repo includes a focused negative security test suite for middleware hardening.
- unauthorized requests with no
Authorizationheader - malformed
Authorizationheader formats - invalid or tampered Bearer tokens
- rate-limited abuse against a representative protected endpoint
- non-leakage checks for error bodies and headers
- public-route behavior when malformed auth headers are present
GitHub Actions runs on push and pull requests to main:
- Lint:
npm run lint - Build check:
node --check src/index.js
- Fork the repo and clone your fork.
- Create a branch from
main. - Run
npm install. - Make focused changes and keep style consistent.
- Run
npm run lint,npm test, and any relevant local checks. - Push your branch and open a pull request.
We welcome docs improvements, bug fixes, and new API endpoints aligned with LiquiFact product goals.
See CONTRIBUTING.md for branch naming, local checks, testing expectations, CI behavior, and pull request guidance.
MIT (see root LiquiFact project for full license).