Creditra is a decentralized, risk-priced credit protocol on Stellar/Soroban. Loans are priced from on-chain behavioral signals — no overcollateralization. This repository is the off-chain backend: the API, the signal collector, the Horizon indexer, the reconciliation worker, and the webhook fan-out that hold the protocol together.
The differentiator is not "another DeFi lender." It is the signal pipeline: behavioral inputs from the chain are normalized, weighted, and handed to the on-chain underwriting contract so that interest rates and limits scale with risk rather than with collateral. The backend's job is to make that pipeline trustworthy — every signal accounted for, every event reconciled against chain truth, every webhook delivered exactly once.
- System Overview
- Quick Start
- Feature Inventory
- Engineering Principles
- Project Layout
- Documentation
- Operational Notes
- Testing
- Contributing
- License
flowchart LR
subgraph Clients
FE[Frontend / Wallet]
ADM[Admin Console]
SUB[Webhook Subscribers]
end
subgraph CreditraBackend[Creditra Backend]
API[Express API<br/>routes/credit · routes/risk<br/>routes/webhook · routes/health]
MW[Middleware<br/>auth · adminAuth · validate<br/>rateLimit · requestLogger · errorHandler]
SVC[Service Layer<br/>CreditLineService · RiskEvaluationService<br/>ReconciliationService · drawWebhookService]
REPO[Repository Layer<br/>Postgres · InMemory]
IDX[HorizonListener<br/>cursor + reorg-safe poller]
JOBS[JobQueue + ReconciliationWorker]
end
DB[(PostgreSQL<br/>borrowers · credit_lines<br/>risk_evaluations · transactions · events)]
HORIZON[(Stellar Horizon)]
SOROBAN[(Soroban RPC)]
RISK[Pluggable Risk Provider<br/>rules · static · external API]
FE -->|X-API-Key| API
ADM -->|X-Admin-Api-Key| API
API --> MW --> SVC
SVC --> REPO --> DB
SVC --> RISK
IDX --> HORIZON
IDX --> SVC
JOBS --> SOROBAN
JOBS --> SVC
SVC -->|HMAC-signed POST| SUB
classDef store fill:#1f2937,stroke:#9ca3af,color:#f9fafb;
class DB,HORIZON,SOROBAN store;
A request enters through the Express router, is authenticated (X-API-Key or X-Admin-Api-Key), validated by Zod schemas, and rate-limited per IP or per key. The service layer talks to repositories (Postgres in production, in-memory for tests) and to the on-chain world through Soroban RPC. The HorizonListener polls Stellar Horizon for contract events and writes them back through the same service layer. The ReconciliationWorker periodically diffs DB state against on-chain state and emits drift alerts. Confirmed events are HMAC-signed and delivered to webhook subscribers with exponential-backoff retry.
- Node.js >= 20
- npm
- (Optional) Docker 24+ and Docker Compose v2 for the containerised dev loop
- (Optional) k6 for load testing
git clone https://github.com/Creditra/Creditra-Backend.git
cd Creditra-Backend
npm install
cp .env.example .env # then fill in DATABASE_URL, API_KEYS, etc.npm run dev # tsx watch on src/index.ts → http://localhost:3000
npm run build # tsc + copies openapi.yaml into dist/
npm start # node dist/index.jsAPI base: http://localhost:3000. Swagger UI: http://localhost:3000/docs. Raw OpenAPI JSON: http://localhost:3000/docs.json.
npm run db:migrate # runs migrations/*.sql in order, idempotent
npm run db:validate # asserts tables/columns/indexes match the expected schemaMigrations live in migrations/ and are tracked in the schema_migrations table by filename version. See docs/data-model.md and docs/schema-validation.md.
npm test # vitest --run
npm run test:coverage # v8 coverage, lcov + text
npm run test:watch # vitest in watch mode
npm run lint # eslint src/
npm run typecheck # tsc --noEmitnpm run load:smoke # k6 — 10 VUs, smoke
npm run load:stress # k6 — up to 100 VUs, stress
npm run load:spike # k6 — 200 VU spike, recoverydocker compose up --build # API + Postgres, hot-reload via tsx watch
docker compose exec api npm run db:migrateEvery entry below is grounded in real files in this repo.
| Surface | Path prefix | Mounted in |
|---|---|---|
| Health & readiness | GET /health |
src/routes/health.ts |
| Credit lines (CRUD) | /api/credit/lines |
src/routes/credit.ts |
| Credit lines by wallet | /api/credit/wallet/:walletAddress/lines |
src/routes/credit.ts |
| Transactions | /api/credit/lines/:id/transactions |
src/routes/credit.ts |
| Draw / repay | POST /api/credit/lines/:id/{draw,repay} |
src/routes/credit.ts |
| Admin suspend / close | POST /api/credit/lines/:id/{suspend,close} (admin auth) |
src/routes/credit.ts |
| Risk evaluation | POST /api/risk/evaluate, history endpoints |
src/routes/risk.ts |
| Webhook config & test | /api/webhooks/* |
src/routes/webhook.ts |
| Reconciliation trigger / status | /api/reconciliation/* (admin) |
src/routes/reconciliation.ts |
| OpenAPI docs | GET /docs, GET /docs.json |
src/index.ts |
Full machine-readable spec: src/openapi.yaml. Human-readable inventory: docs/API.md.
CreditLineService— repository-backed CRUD plusdrawandrepay. Status transitions enforced; basis-point interest rates clamped to0..10000.RiskEvaluationService— caches evaluations for 24 hours, refreshes onforceRefresh, derivescreditLimitandinterestRateBpsfrom a normalized 0–100 score using inverse risk weighting (higher score → lower rate, larger limit).ReconciliationService/ReconciliationWorker— periodic diff of DB credit lines vs on-chain state. Severity levels:critical(identity, limit, status) andwarning(available credit, rate). Runs every hour by default (RECONCILIATION_INTERVAL_MS).HorizonListener— Stellar Horizon poller with cursor persistence, exponential backoff + jitter, gap recovery, and SHA-256 idempotency cache (10k entries, LRU). Metrics exposed viagetMetrics().SorobanRpcClient— read/submit wrapper with AbortController timeouts, retry budget, and Stellar key sanitization in error messages.drawWebhookService— multi-URL HMAC-SHA256 webhook fan-out with retry/backoff and connectivity probe.jobQueue— in-process at-least-once queue with visibility timeout, attempt tracking, and dead-letter list.
Implementation files: src/services/, entry composition in src/container/Container.ts.
Selected via RISK_PROVIDER env var, factory in src/services/providers/providerFactory.ts:
rules(default) — deterministicRulesEngineRiskProvidercombining address entropy, hash spread, and prefix score.static—StaticRiskProviderfor tests/local dev.external—ExternalApiRiskProviderHTTP-pluggable provider with timeout & bearer auth.
See docs/SIGNALS_INGEST.md for the end-to-end signal pipeline.
- PostgreSQL schema in
migrations/001_initial_schema.sql(+ 002 forinterest_rate_bps). - Repository interfaces in
src/repositories/interfaces/with bothpostgres/andmemory/implementations selected byDATABASE_URL+NODE_ENV. - Schema is asserted at boot via
src/db/validate-schema.ts— missing tables, columns, or critical indexes fail fast.
- Structured JSON logs via Pino (
src/utils/logger.ts) with per-requestrequestIdpropagated through thex-request-idheader (src/middleware/requestLogger.ts). - Stellar addresses redacted in logs via
src/utils/logRedact.ts. - Health endpoint surfaces DB and Horizon dependency status with timeouts.
- Listener and worker maintain in-memory metrics counters.
- Constant-time API key check via
crypto.timingSafeEqual(src/middleware/auth.ts). - Admin endpoints gated by a separate header (
X-Admin-Api-Key). - Request body capped at 100 kB; non-JSON mutating requests rejected with 415.
- Per-route token-bucket rate limit emitting
X-RateLimit-*headers (src/middleware/rateLimit.ts). - HMAC-SHA256 webhook signatures (
X-Webhook-Signature: sha256=…). - Outbound HTTP guarded by
src/utils/fetchWithTimeout.ts.
Full model: docs/SECURITY.md and SECURITY.md.
- Dependency inversion at every seam. Routes depend on services; services depend on repository interfaces; the
Containerwires concrete implementations at boot. The same service code drives Postgres in production and in-memory stores in tests, with no branching. - Schema before runtime. Every external input is validated by a Zod schema in
src/schemas/before it reaches a handler. Every DB boot validates that the expected tables, columns, and indexes exist. - Idempotency by construction. Webhook events carry a SHA-256
eventId. Domain events have a uniqueidempotency_keycolumn. The Horizon listener deduplicates events across restarts. - Time-bound everything. Every outbound HTTP call has a connect + read timeout. Health checks have per-dependency timeouts. Graceful shutdown has
SHUTDOWN_TIMEOUT_MSceiling. - No silent drift. The reconciliation worker is the source of truth for "does the DB still match the chain?". Mismatches are graded and surfaced.
- One envelope. Every JSON response uses
{ data, error }fromsrc/utils/response.ts— no ambiguous shapes. - Secrets out of logs. The Pino logger pipes through
logRedactso Stellar pubkeys never appear in clear.
Creditra-Backend/
├── src/
│ ├── index.ts # Server bootstrap, swagger, graceful shutdown
│ ├── app.ts # Minimal app factory (used by some tests)
│ ├── openapi.yaml # Source of truth API spec
│ ├── config/ # env / apiKeys / cors / rateLimit loaders
│ ├── container/ # DI container
│ ├── db/ # pg client, migration runner, schema validator
│ ├── middleware/ # auth, adminAuth, rateLimit, validate, errorHandler, requestLogger
│ ├── models/ # Domain types (CreditLine, RiskEvaluation, Transaction)
│ ├── repositories/ # interfaces/ + memory/ + postgres/
│ ├── routes/ # credit / risk / webhook / reconciliation / health
│ ├── schemas/ # Zod validation schemas
│ ├── services/ # Domain services + providers/
│ └── utils/ # logger, response envelope, redaction, time, …
├── migrations/ # *.sql ordered, run by db:migrate
├── tests/ # route + integration tests
├── scripts/load/ # k6 smoke / stress / spike
├── docs/ # architecture, signals, security, indexer, observability, testing
├── Dockerfile, docker-compose.yml
├── jest.config.ts, vitest.config.ts, tsconfig.json, .eslintrc.cjs
└── package.json
| Document | Purpose |
|---|---|
docs/ARCHITECTURE.md |
Backend system design, request lifecycle, component topology |
docs/API.md |
Endpoint inventory, request/response shapes, error envelope, pagination |
docs/SIGNALS_INGEST.md |
Behavioral signal pipeline → on-chain underwriting |
docs/SECURITY.md |
Auth model, RBAC, validation, rate limiting, HMAC, secrets |
docs/INDEXER.md |
Horizon listener cursor model, reorg/gap handling, reconciliation |
docs/OBSERVABILITY.md |
Logs, metrics, health probes, tracing strategy |
docs/TESTING.md |
Test pyramid, file counts, integration vs unit |
CONTRIBUTING.md |
Commit conventions, PR template, review checklist |
docs/data-model.md |
Per-table column reference |
docs/HORIZON_LISTENER_CONFIG.md |
Env-var reference for the listener |
docs/reconciliation.md |
Reconciliation job details |
docs/cursor-pagination.md |
Cursor pagination contract |
docs/error-envelope.md |
{ data, error } envelope reference |
docs/http-timeouts.md |
Outbound HTTP timeout policy |
docs/schema-validation.md |
Boot-time schema validator |
docs/load-testing.md |
k6 scripts and thresholds |
docs/security-checklist-backend.md |
Pre-deploy security checklist |
docs/security-pentest-checklist.md |
Pentest prep checklist |
docs/REPOSITORY_ARCHITECTURE.md |
Repository / DIP layout |
docs/troubleshooting.md |
Common failure modes |
- Graceful shutdown.
SIGTERMandSIGINTclose the HTTP server, stop the reconciliation worker, drain the job queue, and close the DB pool — bounded bySHUTDOWN_TIMEOUT_MS(default 30s). - Hot key rotation.
loadApiKeys()is invoked per request via a resolver closure, soAPI_KEYSmay be rotated without restart (e.g. via secret manager). - Body limits.
express.json({ limit: '100kb' }). Oversize requests are converted into a413via the global error handler. - CORS. Production deployments must set
CORS_ORIGINSto a comma-separated allowlist; dev/test falls back to loopback origins (src/config/cors.ts).
- 67 test files across
tests/,src/__tests__/, andsrc/__test__/. - Unit, integration (Supertest + Express), and route-level coverage; coverage threshold 95% on Node 20 for touched modules (
.github/workflows/backend-ci.yml). - Full pyramid description in
docs/TESTING.md.
See CONTRIBUTING.md for branch model, commit conventions, and PR review checklist. Migrations follow strict additive-only discipline: see the section in CONTRIBUTING.md.
UNLICENSED — internal repository for the Creditra protocol. See LICENSE.