API and services for the Credence economic trust protocol. Provides health checks, trust score and bond status endpoints (to be wired to Horizon and a reputation engine).
This service is part of Credence. It supports:
- Public query API (trust score, bond status, attestations)
- Horizon listener for bond withdrawal events
- Redis-based caching layer
- Configurable lock timeouts – Prevents indefinite waits on locked rows with policy-based timeouts and automatic retry
- Horizon listener / identity state sync – Reconciles DB with on-chain bond state (see Identity state sync).
- Reputation engine (off-chain score from bond data) (future)
- Node.js 18+
- npm or pnpm
- Redis server (for caching)
- Stellar Horizon server (for blockchain events)
- @stellar/stellar-sdk (Stellar blockchain integration)
- Docker & Docker Compose (for containerised dev)
npm install
# Set Redis URL in environment
export REDIS_URL=redis://localhost:6379
# Set Horizon URL for blockchain events
export HORIZON_URL=https://horizon-testnet.stellar.org
# Set Stellar network passphrase
export STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
cp .env.example .env
# Edit .env with your actual valuesThe server fails fast on startup if any required environment variable is missing or invalid. See Environment Variables below.
Development (watch mode):
npm run devProduction:
npm run build
npm startAPI runs at http://localhost:3000. The frontend proxies /api to this URL.
The project ships with Dockerfile, docker-compose.yml, and an example env file so you can spin up the full stack (API + PostgreSQL + Redis) in one command.
# 1. Create your local env file
cp .env.example .env
# 2. Build and start all services
docker compose up --build
# 3. Verify health
curl http://localhost:3000/api/health
# → {"status":"ok","service":"credence-backend"}| Service | Port | Description |
|---|---|---|
backend |
3000 | Express / TypeScript API |
postgres |
5432 | PostgreSQL 16 |
redis |
6379 | Redis 7 |
All ports are configurable via .env (see .env.example).
Drop any .sql files into the init-db/ directory. PostgreSQL will execute them once when the data volume is first created. A placeholder file (init-db/001_schema.sql) is included as a starting point.
To re-run init scripts, remove the volume and restart:
docker compose down -v # removes data volumes
docker compose up --build# Stop all services
docker compose down
# Stop and remove volumes (reset DB/Redis data)
docker compose down -v
# View logs
docker compose logs -f backend
# Rebuild only the backend image
docker compose build backend
# Open a psql shell
docker compose exec postgres psql -U credenceAll configuration is driven by environment variables. Copy .env.example to .env and adjust as needed. Key variables:
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Backend listen port |
POSTGRES_USER |
credence |
PostgreSQL user |
POSTGRES_PASSWORD |
credence |
PostgreSQL password |
POSTGRES_DB |
credence |
PostgreSQL database name |
POSTGRES_PORT |
5432 |
Host-exposed PG port |
REDIS_PORT |
6379 |
Host-exposed Redis port |
DATABASE_URL |
(composed) | Full PG connection string |
REDIS_URL |
(composed) | Full Redis connection URL |
| Command | Description |
|---|---|
npm run dev |
Start with tsx watch |
npm run build |
Compile TypeScript |
npm start |
Run compiled dist/ |
npm run lint |
Run ESLint |
npm test |
Run test suite (vitest) |
npm run test:watch |
Run tests in watch mode |
npm run test:coverage |
Run tests with coverage |
npm run migrate:create |
Create new migration in src/migrations/ |
npm run migrate:dev |
Build and run pending migrations (local) |
npm run migrate |
Run pending migrations (CI/production) |
npm run migrate:down |
Rollback last migration |
| Method | Path | Description |
|---|---|---|
| GET | /api/health |
Health check |
| GET | /api/health/cache |
Redis cache health check |
| GET | /api/trust/:address |
Trust score from reputation engine |
| GET | /api/bond/:address |
Bond status |
| GET | /api/attestations/:address |
List attestations for address |
| POST | /api/attestations |
Create attestation |
| GET | /api/verification/:address |
Verification proof (stub) |
| GET | /api/analytics/summary |
Aggregated analytics from materialized view |
Invalid input returns 400 with { "error": "Validation failed", "details": [{ "path", "message" }] }. See docs/VALIDATION.md.
Full request/response documentation, cURL examples, and import instructions: docs/api.md
docs/openapi.yaml
Render with npx @redocly/cli preview-docs docs/openapi.yaml or paste into editor.swagger.io.
docs/credence.postman_collection.json
Import via File → Import in Postman or Insomnia. See docs/api.md for step-by-step instructions and Newman CLI usage.
The health API reports status per dependency (database, Redis, optional external) without exposing internal details.
- Readiness (
GET /api/healthorGET /api/health/ready): Returns200when all configured critical dependencies (DB, Redis) are up; returns503if any critical dependency is down. WhenDATABASE_URLorREDIS_URLare not set, those dependencies are reported asnot_configuredand do not cause503. - Liveness (
GET /api/health/live): Returns200when the process is running (no dependency checks). Use for Kubernetes/orchestrator liveness probes.
Response shape (readiness):
{
"status": "ok",
"service": "credence-backend",
"dependencies": {
"db": { "status": "up" },
"redis": { "status": "up" }
}
}status may be ok, degraded (optional external down), or unhealthy (critical dependency down). Each dependency status is up, down, or not_configured. Optional env: DATABASE_URL, REDIS_URL to enable DB and Redis checks.
Health endpoints are covered by unit and route tests. Run:
npm test
npm run test:coverageScenarios covered: all dependencies up, DB down (503), Redis down (503), both down (503), only external down (200 degraded), liveness always 200, and no dependencies configured (200 ok).
The identity state sync listener keeps database identity and bond state in sync with on-chain state (reconciliation or full refresh). Use it to correct drift from missed events or for recovery.
- Location:
src/listeners/identityStateSync.ts - Reconciliation by address:
sync.reconcileByAddress(address)– fetches current state from the contract, diffs with DB, and updates the store if there is drift. - Full resync:
sync.fullResync()– reconciles all known identities (union of store and contract addresses). Use for recovery or bootstrap.
You supply:
- ContractReader – Fetches current bond/identity state from chain (e.g. Horizon or contract reads). Implement
getIdentityState(address)and optionallygetAllIdentityAddresses(). - IdentityStateStore – Your persistence layer (e.g. DB). Implement
get,set, andgetAllAddresses.
State shape is IdentityState: address, bondedAmount, bondStart, bondDuration, active. See src/listeners/types.ts.
Tests cover: no drift (no update), single drift (one address corrected), full resync (multiple drifts), chain missing, store-only addresses, and error handling.
Comprehensive monitoring with Prometheus and Grafana is available. See docs/monitoring.md for:
- Metrics instrumentation guide
- Grafana dashboard setup
- Prometheus configuration
- Alert rules
- Deployment instructions
Quick start:
# Install metrics dependency
npm install prom-client
# Start monitoring stack
docker-compose up -d
# Access services
# - Prometheus: http://localhost:9090
# - Grafana: http://localhost:3001 (admin/admin)
# - Metrics endpoint: http://localhost:3000/metricsThe Grafana dashboard includes:
- HTTP metrics (request rate, latency, error rate, status codes)
- Infrastructure health (DB, Redis status and check duration)
- Business metrics (reputation calculations, identity verifications, bulk operations)
The service includes a Horizon withdrawal events listener that:
- Monitors Stellar blockchain for withdrawal transactions affecting bonds
- Updates bond states (amount, active status) based on on-chain events
- Creates score history snapshots for significant withdrawals
- Maintains consistency between on-chain and database states
- Handles errors gracefully with automatic retry and recovery
See docs/horizon-listener.md for detailed documentation.
The service includes a Redis-based caching layer with:
- Connection management - Singleton Redis client with health monitoring
- Namespacing - Automatic key namespacing (e.g.,
trust:score:0x123) - TTL support - Set expiration times on cached values
- Health checks - Built-in Redis health monitoring
- Graceful fallback - Continues working when Redis is unavailable
See docs/caching.md for detailed documentation.
A TypeScript/JavaScript SDK is available at src/sdk/ for programmatic access to the API. See docs/sdk.md for full documentation.
The config module (src/config/index.ts) centralizes all environment handling:
- Loads
.envfiles via dotenv for local development - Validates all environment variables at startup using Zod
- Fails fast with a clear error message listing every invalid or missing variable
- Exports a fully typed
Configobject consumed by the rest of the application
import { loadConfig } from './config/index.js'
const config = loadConfig()
console.log(config.port) // number
console.log(config.db.url) // string
console.log(config.features) // { trustScoring: boolean, bondEvents: boolean }For testing, use validateConfig() which throws a ConfigValidationError instead of calling process.exit:
import { validateConfig, ConfigValidationError } from './config/index.js'
try {
const config = validateConfig({ DB_URL: 'bad' })
} catch (err) {
if (err instanceof ConfigValidationError) {
console.error(err.issues) // Zod issues array
}
}| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
No | 3000 |
Server port (1–65535) |
NODE_ENV |
No | development |
development, production, or test |
LOG_LEVEL |
No | info |
debug, info, warn, or error |
DB_URL |
Yes | — | PostgreSQL connection URL |
REDIS_URL |
Yes | — | Redis connection URL |
JWT_SECRET |
Yes | — | JWT signing secret (≥ 32 chars) |
JWT_EXPIRY |
No | 1h |
JWT token lifetime |
ENABLE_TRUST_SCORING |
No | false |
Enable trust scoring feature |
ENABLE_BOND_EVENTS |
No | false |
Enable bond event processing |
HORIZON_URL |
No | — | Stellar Horizon API URL |
CORS_ORIGIN |
No | * |
Allowed CORS origin |
ANALYTICS_REFRESH_CRON |
No | */5 * * * * |
Refresh cadence for analytics materialized view |
ANALYTICS_STALENESS_SECONDS |
No | 300 |
Max acceptable analytics staleness before marked stale |
Analytics endpoints are backed by PostgreSQL materialized views to reduce response latency on aggregate queries.
- View source:
analytics_metrics_mv - Refresh mode:
REFRESH MATERIALIZED VIEW CONCURRENTLY - Default cadence: every 5 minutes (
ANALYTICS_REFRESH_CRON) - Freshness window: 300 seconds (
ANALYTICS_STALENESS_SECONDS)
The endpoint response includes staleness metadata:
asOf: timestamp of snapshot used in the responseageSeconds: age of snapshot when servedfresh: whether snapshot age is within tolerated windowrefreshStatus:ok,stale, orfailed_recently
When a refresh fails, the API keeps serving the last successful snapshot and marks the response with degraded freshness metadata.
The project uses node-pg-migrate for PostgreSQL database migrations with versioning and rollback support.
- PostgreSQL database
DATABASE_URLenvironment variable set (e.g.,postgres://user:password@localhost:5432/credence)
Development (recommended):
# Build TypeScript and run all pending migrations
npm run migrate:devProduction/CI:
# Requires dist/ to be built first
npm run build
npm run migrateCreate a new TypeScript migration file:
npm run migrate:create my_migration_nameThis creates a timestamped .ts file in src/migrations/.
Development:
# Build and run all pending migrations
npm run migrate:dev
# Check which migrations would run (dry run)
npm run migrate:dev -- --dry-runProduction/CI (requires build first):
npm run build
npm run migrate
npm run migrate:downRollback:
# Development (builds first)
npm run migrate:dev -- migrate:down
# Production (requires dist/ built)
npm run migrate:downimport { MigrationBuilder } from 'node-pg-migrate'
export async function up(pgm: MigrationBuilder): Promise<void> {
// Apply changes (create tables, add columns, etc.)
pgm.createTable('users', {
id: 'id',
email: { type: 'varchar(255)', notNull: true },
})
}
export async function down(pgm: MigrationBuilder): Promise<void> {
// Reverse changes
pgm.dropTable('users')
}| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | Required |
MIGRATIONS_TABLE |
Table name for tracking migrations | pgmigrations |
MIGRATIONS_SCHEMA |
Schema for migrations table | public |
Run migrations in CI/CD pipelines (requires build first):
# Dockerfile or CI script
npm ci
npm run build
DATABASE_URL=postgres://... npm run migrate
npm startThe first migration (src/migrations/001_initial_schema.ts) creates:
identities- Identity and bond stateattestations- Attestation recordsreputation_scores- Cached reputation scores
After running npm run build, migrations are executed from dist/migrations/.
- Always test both
up()anddown()before committing - Keep migrations idempotent - safe to run multiple times
- Use transactions - enabled by default for atomicity
- Don't modify existing migrations after they've been applied
- Create new migrations for schema changes
- Back up production database before running migrations
- Node.js
- TypeScript
- Express
- PostgreSQL (with migrations via node-pg-migrate)
- Prometheus (metrics)
- Grafana (visualization)
- Redis (caching layer)
- @stellar/stellar-sdk (Stellar blockchain integration)
- Vitest (testing)
- Zod (env validation)
- dotenv (.env file support)
Extend with additional Horizon event ingestion when implementing the full architecture.
- Adapter implementation:
src/clients/soroban.ts - Integration notes:
docs/stellar-integration.md - Tests:
src/clients/soroban.test.ts
Repository integration tests are under tests/integration/ and execute against real PostgreSQL.
- Use Docker/Testcontainers automatically:
npm run test:integration - Use an existing DB in CI:
TEST_DATABASE_URL=postgresql://... npm run test:integration - Coverage report:
npm run coveragedummy