Skip to main content

Backend Overview

Location: apps/backend/
Runtime: Node.js 20 LTS
Framework: Express.js
Database: PostgreSQL 16 via Prisma ORM
Queue: BullMQ + Redis
The backend serves two roles:

REST API

Exposes vault, analytics, and leaderboard data to the frontend via Express.js routes on port 4000.

BullMQ Workers

Consume jobs from Redis queues produced by the indexer and persist results to PostgreSQL.

Entry Point

apps/backend/src/index.ts bootstraps the Express server and imports all workers:
import './workers/index'; // Activates all BullMQ workers on startup

app.use('/api/vaults', vaultRoutes);
app.use('/api/analytics', analyticsRoutes);
app.use('/api/leaderboard', leaderboardRoutes);

app.get('/health', (req, res) => res.json({ status: 'ok' }));
app.listen(4000);

Data Models (Prisma)

Defined in packages/db/prisma/schema.prisma:

User

FieldTypeNotes
idStringUUID primary key
walletAddressStringUnique, checksummed EVM address
argusScoreIntReputation score from Argus Engine (default 0)
vaultsLedVault[]Vaults this user leads
subscriptionsFollowerSubscription[]Vaults this user follows

Vault

FieldTypeNotes
idStringUUID primary key
contractAddressStringUnique, on-chain proxy address
leaderIdStringFK → User.id
nameStringVault display name
baseAssetStringERC-20 address for deposits
tvlStringTotal value locked (stored as string for BigInt safety)
roiFloatReturn on investment %
drawdownFloatMaximum drawdown %
riskScoreIntArgus-derived risk score
statusEnumACTIVE | PAUSED | LIQUIDATED
createdAtDateTimeVault creation timestamp

Trade

FieldTypeNotes
idStringUUID primary key
txHashStringUnique, on-chain transaction hash
vaultIdStringFK → Vault.id
assetInStringToken sold (ERC-20 address)
assetOutStringToken received (ERC-20 address)
amountInStringAmount of assetIn (BigInt string)
amountOutStringAmount of assetOut (BigInt string)
isProfitBooleanWhether this trade closed profitably
pnlAmountStringPnL amount (BigInt string)
executedAtDateTimeBlock timestamp

FollowerSubscription

FieldTypeNotes
idStringUUID primary key
vaultIdStringFK → Vault.id
userIdStringFK → User.id
depositedAmountStringCurrent deposited balance (BigInt string)
subscribedAtDateTimeFirst subscription timestamp
Unique constraint: @@unique([vaultId, userId])

Worker Architecture

The workers run inside the same backend process. They are imported by workers/index.ts which is itself imported in index.ts.
workers/
├── index.ts          # Registers all workers (imports side-effects)
├── trade.worker.ts   # Consumes TradeProcessingQueue
├── snapshot.worker.ts # Consumes SnapshotCalculationQueue
├── vault.worker.ts   # Consumes VaultDeployedQueue
└── follower.worker.ts # Consumes FollowerEventQueue
See Workers for detailed worker documentation. For Sidiora perpetual mirroring controls, risk policy, and the queue/API contract, see Sidiora Mirroring Spec.

Configuration

apps/backend/src/config/redis.ts — ioredis connection shared by all workers:
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL ?? 'redis://127.0.0.1:6379', {
    maxRetriesPerRequest: null, // Required by BullMQ
});