How bhupeshv29/perps-exchange-platform works
1. This is a monorepo for a simplified perpetual futures exchange. The system is split into independently deployed services: a Next.js frontend, an HTTP backend API, a matching engine, a DB worker, a WebSocket gateway, and a price worker. Shared packages provide common market math, Redis helpers, database access, UI components, and config.
2. The frontend lives in apps/frontend and is the user-facing trading app. It uses Next.js app routes, NextAuth-style auth plumbing, TanStack/React Query-style data fetching, local state stores, and a WebSocket layer for realtime market/account updates. The trade page is composed from specialized panels for order entry, depth, fills, positions, balances, and history.
3. The backend in apps/backend is the request gateway for authenticated REST actions such as account, auth, market, and order operations. Its key role is orchestration: validate input, authenticate the user, publish a request into Redis, wait for the engine response, and return the HTTP result. It does not own balances, positions, orders, or orderbook state.
4. The matching engine in apps/engine is the source of truth for trading state. It keeps balances, orders, orderbooks, positions, and mark prices in memory, performs order matching with price-time priority, computes fills and PnL, handles liquidation, and generates depth updates. It also persists periodic snapshots to disk for recovery after restart.
5. The engine receives inputs from two Redis-backed channels: trading requests and price updates. Requests come from the backend through Redis Streams; mark price updates come from the price worker through another stream. After applying state transitions, the engine emits two kinds of outputs: durable events for database persistence and realtime events for user/client updates.
6. Persistence is isolated in apps/dbWorker. That service consumes engine-generated database events from Redis Streams and writes them to PostgreSQL via Prisma. The schema layer in packages/db defines the relational history the platform keeps, such as users, balances, orders, fills, and closed positions. Notably, open positions are not stored in Postgres; they exist only in the engine’s runtime state.
7. Realtime fanout is handled by apps/wsGateway. It subscribes to Redis Pub/Sub events produced by the engine and broadcasts them over WebSocket to connected clients. The gateway also authenticates socket connections using a JWT cookie and attaches a user identity so it can distinguish private events from market-wide events.
8. The price worker in apps/priceWorker is the external market-data ingress for mark-price updates. Its output is fed into Redis Streams and consumed by the engine for liquidation and unrealized PnL calculations. This worker is part of the risk loop because it drives the engine’s view of current market value.
9. Redis is the core integration backbone. Streams are used for ordered, reliable processing between services: engine requests, engine responses, DB events, and price updates. Pub/Sub is used only for low-latency websocket fanout. The architecture intentionally separates these two patterns so critical state changes are durable while UI updates stay fast.
10. The shared packages define the common contract surface. packages/common contains market utilities, constants, formatting, and stream/type helpers; packages/redis centralizes Redis client and stream/pubsub primitives; packages/ui provides reusable frontend components; packages/eslint-config and packages/typescript-config standardize tooling across apps.
11. The main business flow is: frontend submits an order or account action to the backend, backend writes a request to Redis, engine consumes it and mutates trading state, engine emits persistence and websocket events, DB worker writes history to Postgres, and WebSocket gateway pushes live updates back to subscribed clients. This is the central architecture boundary: the engine owns live state, while the other services are thin adapters around it.
12. Startup recovery is a first-class concern. The engine periodically snapshots its in-memory state to a local file and reloads that snapshot on restart before resuming stream consumption. That means the system can recover trading state without reconstructing open orders or positions solely from the database.
13. Deployment is containerized per service. Each app has its own production Dockerfile, with a root docker-compose setup for local or production-style orchestration. The repo also includes docs for installation and e2e testing, which suggests the system is intended to be run as a multi-process distributed exchange rather than a single web server.
Main entry points: engine Engine · backend Backend API · redis Redis pkg · common Common
Components 20
Apps
Deployable services that make up the exchange
- Frontend · User-facing trading interface with routes, data fetching, local state, and realtime updates
apps/frontend - Trade page · Composed view for entering orders and monitoring market/account state
apps/frontend/app/trade/[marketId]/page.tsx - Providers · Wires query, websocket, market, and account state for the frontend
apps/frontend/providers - WebSocket layer · Client-side subscription and event handling for live market and account updates
apps/frontend/providers/WebSocketProvider.tsx - Backend API · Authenticated REST orchestration layer that forwards requests into Redis and returns engine results
apps/backend - API routes · Account, auth, market, and order request handlers
apps/backend/src/routes - Middleware · Auth, rate limiting, validation, and error handling around HTTP requests
apps/backend/src/middleware - Backend Redis · Publishes trading requests and waits for loopback responses
apps/backend/src/redis.ts - Engine · Source of truth for balances, orders, orderbooks, positions, matching, liquidation, and snapshots
apps/engine - State core · In-memory balances, orders, positions, orderbooks, and mark prices
apps/engine/src/state/state.ts - Matching · Executes order matching and depth updates
apps/engine/src/orderbook/match.ts - Publishers · Produces durable DB events and realtime websocket events after state transitions
apps/engine/src/publish - Snapshots · Loads and saves engine state snapshots for restart recovery
apps/engine/src/snapshot - DB worker · Consumes engine persistence events and writes exchange history to PostgreSQL
apps/dbWorker - WS gateway · Authenticates socket clients and fans out realtime events from Redis Pub/Sub
apps/wsGateway - Price worker · Feeds external mark-price updates into Redis Streams for engine risk and PnL calculations
apps/priceWorker
Shared packages
Cross-cutting contracts and libraries
- DB schema · Relational history for users, balances, orders, fills, and closed positions
packages/db/prisma/schema.prisma - Common · Shared market math, constants, formatting, and stream/type helpers
packages/common - Redis pkg · Centralized Redis client, stream, and pubsub primitives
packages/redis - UI kit · Reusable frontend UI components
packages/ui
Connections 28
- Frontend → Providers: uses · Frontend composition depends on shared providers and app-level state
- Providers → WebSocket layer: includes · WebSocket provider supplies realtime transport to the app
- Frontend → Trade page: renders · Trade route assembles the main trading workspace
- Trade page → UI kit: uses · Trading UI reuses shared visual components
- Trade page → Backend API: calls · User actions and queries go through the backend API
- Backend API → API routes: exposes · Route modules define the REST surface
- Backend API → Middleware: wraps · Requests are authenticated, validated, and rate-limited
- Backend API → Backend Redis: publishes · Backend sends authenticated commands into Redis
- Backend Redis → Redis pkg: uses · Backend relies on shared Redis primitives
- Engine → State core: owns · Engine maintains live trading state in memory
- Engine → Matching: matches · Order matching updates books, fills, and depth
- Engine → Publishers: emits · State transitions produce persistence and realtime events
- Engine → Snapshots: recovers · Snapshots are saved and restored across restarts
- Engine → Common: uses · Engine shares market math and stream contracts
- Engine → Redis pkg: uses · Engine consumes and publishes through Redis helpers
- Engine → Price worker: consumes prices from · Mark-price updates arrive from the price worker stream
- Backend API → Engine: request loop · HTTP actions are forwarded to engine and responses loop back
- DB worker → Publishers: consumes · Database worker persists engine-generated durable events
- DB worker → DB schema: writes · Prisma schema defines stored exchange history
- DB worker → Redis pkg: uses · DB worker listens on Redis streams
- WS gateway → Publishers: subscribes · Gateway receives realtime events from Redis Pub/Sub
- WS gateway → Redis pkg: uses · WebSocket server shares Redis client and pubsub helpers
- WS gateway → WebSocket layer: feeds · Broadcast events are delivered to connected clients
- Price worker → Redis pkg: publishes · Price updates are written into Redis Streams
- Price worker → Engine: drives risk · Mark prices affect liquidation and unrealized PnL
- Common → Backend API: shared by · Backend uses shared contracts and utilities
- Common → Frontend: shared by · Frontend uses shared market helpers and helpers
- Common → Engine: shared by · Engine uses common market math and stream helpers