Component overview
Opinix Trade is built using a microservices-inspired architecture with specialized components for different responsibilities.
Client (Next.js frontend)
The frontend is a Next.js 14 application using the App Router with TypeScript.
Key features
Server Actions Direct server-side functions for data fetching without API routes
Real-time UI Live order book updates via WebSocket connections
Wallet Management Integrated Cashfree payment gateway for deposits/withdrawals
Portfolio Tracking Track gains/losses based on position values
Order placement
The client submits orders to the backend via the /api/placeorder route:
// From apps/client/app/api/placeorder/route.ts
const data = {
event_id: eventId ,
l1_expected_price: price ,
l1_order_quantity: quantity ,
offer_type: side , // "yes" or "no"
userid: userId
};
await fetch ( '/order/initiate' , {
method: 'POST' ,
body: JSON . stringify ( data )
});
Project structure
apps/client/
├── app/
│ ├── (lobby)/ # Main app routes
│ │ ├── events/ # Event listing & details
│ │ ├── portfolio/ # User portfolio
│ │ └── wallet/ # Deposit/withdraw
│ ├── api/ # API routes
│ └── components/ # Shared components
├── actions/ # Server actions
└── components/ # UI components
Backend API (Express)
The backend is an Express.js server that validates requests and enqueues orders for processing.
Server setup
// From apps/server/src/index.ts:1
import express from "express" ;
import { logger } from "@opinix/logger" ;
import morgan from "morgan" ;
import { orderRouter } from "./router/orderRouter" ;
import { eventRouter } from "./router/eventRouter" ;
const app = express ();
const morganFormat = ":method :url :status :response-time ms" ;
app . use ( express . json ());
app . use ( morgan ( morganFormat , {
stream: {
write : ( message ) => {
const [ method , url , status , responseTime ] = message . split ( " " );
const formattedLog = ` ${ method } - ${ url } - ${ status } - ${ responseTime ?. trim () } ms` ;
logger . info ( formattedLog );
},
},
}));
app . use ( "/events" , eventRouter );
app . use ( "/order" , orderRouter );
app . listen ( 3001 , () => {
logger . info ( `SERVER | Listening on port 3001` );
});
Order endpoint
The /order/initiate endpoint validates the order and pushes it to Redis:
// From apps/server/src/controllers/order/index.ts:16
export const placeHandler = AsyncWrapper (
async ( req : Request <{}, {}, TPlaceOrderReq >, res ) => {
const { event_id , l1_expected_price , l1_order_quantity , offer_type , userid } = req . body ;
// TODO: check Authorize the user
const data = {
type: uuid4 (),
data: {
market: event_id ,
price: l1_expected_price ,
type: "CREATE_ORDER" ,
quantity: l1_order_quantity ,
side: offer_type ,
userId: userid . toString (),
},
};
await addToOrderQueue ( data );
let response = new SuccessResponse ( "Order placed successfully" , 201 );
return res . status ( 201 ). json ( response );
}
);
The API responds immediately after enqueuing the order. The actual order execution happens asynchronously in the worker.
Routes
Places a new order in the queue. Request body: {
"event_id" : "bitcoin-event" ,
"l1_expected_price" : 5.5 ,
"l1_order_quantity" : 10 ,
"offer_type" : "yes" ,
"userid" : "user123"
}
Response: {
"message" : "Order placed successfully" ,
"statusCode" : 201
}
Returns all active trading events.
Returns details for a specific event including the order book.
Worker (Matching engine)
The worker consumes orders from Redis and executes them using the matching engine.
Engine initialization
// From services/engine/src/trade/Engine.ts:26
constructor () {
let snapshot = null ;
try {
if ( process . env . WITH_SNAPSHOT ) {
snapshot = fs . readFileSync ( "./snapshot.json" );
}
} catch ( error ) {
console . log ( "No snapshot found" );
}
if ( snapshot ) {
const parsedSnapShot = JSON . parse ( snapshot . toString ());
this . orderbooks = parsedSnapShot . orderbook . map (
( o : any ) =>
new Orderbook ( o . bids , o . asks , o . lastTradeId , o . currentPrice , o . event )
);
this . balances = new Map ( parsedSnapShot . balance );
} else {
const lastTradeId = 1 ;
this . orderbooks = [ new Orderbook ([], [], lastTradeId , 0 , EXAMPLE_EVENT )];
this . setBaseBalances ();
}
setInterval (() => {
this . saveSnapshot ();
}, 1000 * 3 );
}
The engine can restore state from a snapshot file, making it resilient to crashes. Snapshots are saved every 3 seconds.
Order processing
The engine processes different message types:
// From services/engine/src/trade/Engine.ts:59
processOrders ({
message ,
clientId ,
}: {
message: MessageFromApi ;
clientId : string ;
}) {
switch ( message . type ) {
case CREATE_ORDER :
try {
const { executedQty , fills , orderId } = this . createOrders (
message . data . market ,
message . data . price ,
message . data . quantity ,
message . data . side ,
message . data . userId
);
RedisManager . getInstance (). sendToApi ( clientId , {
type: "ORDER_PLACED" ,
payload: { orderId , executedQty , fills },
});
} catch ( error ) {
RedisManager . getInstance (). sendToApi ( clientId , {
type: "ORDER_CANCELLED" ,
payload: { orderId: "" , executedQty: 0 , remainingQty: 0 },
});
}
break ;
case CANCEL_ORDER :
// Cancel order logic
break ;
case GET_DEPTH :
// Return order book depth
break ;
case GET_OPEN_ORDERS :
// Return user's open orders
break ;
}
}
WebSocket server
The WebSocket server handles real-time communication with clients using the ws library.
Server initialization
// From services/wss/src/index.ts:1
import { WebSocketServer } from "ws" ;
import { config } from "dotenv" ;
import { UserManager } from "./classes/UserManager" ;
config ();
const port = process . env . PORT as unknown as number ;
const wss = new WebSocketServer ({ port: port });
wss . on ( "listening" , () => {
console . log ( `WebSocket server is running on port ws://localhost: ${ wss . options . port } ` );
});
wss . on ( "connection" , ( ws ) => {
UserManager . getInstance (). addUser ( ws );
});
Subscription management
The SubscriptionManager handles user subscriptions efficiently:
// From services/wss/src/classes/SubscriptionManager.ts:20
export class SubscriptionManager {
private static myInstance : SubscriptionManager ;
private subscriptions : Map < string , string []> = new Map ();
private reverseSubscriptions : Map < string , string []> = new Map ();
private redisClient : RedisClientType ;
subscribe ( userId : string , subscription : string ) {
if ( this . subscriptions . get ( userId )?. includes ( subscription )) {
return ;
}
const newSubscription = ( this . subscriptions . get ( userId ) || []). concat ( subscription );
this . subscriptions . set ( userId , newSubscription );
const newRevSubscription = ( this . reverseSubscriptions . get ( subscription ) || []). concat ( userId )
this . reverseSubscriptions . set ( subscription , newRevSubscription )
if ( this . reverseSubscriptions . get ( subscription )?. length === 1 ) {
this . redisClient . subscribe ( subscription , this . redisCallbackHandler );
}
}
private redisCallbackHandler = ( message : string , channel : string ) => {
const parsedMessage = JSON . parse ( message );
this . reverseSubscriptions . get ( channel )?. forEach ( s =>
UserManager . getInstance (). getUser ( s )?. emitMessage ( parsedMessage )
);
}
}
The reverse subscriptions map is critical for performance. When a message arrives, we can quickly find all users subscribed to that channel without iterating through all users.
Redis manager
The RedisManager provides a centralized interface for Redis operations:
// From packages/order-queue/src/classes/RedisManager.ts:4
export class RedisManager {
private client : RedisClientType ;
private static instance : RedisManager ;
constructor () {
this . client = createClient ();
this . client . connect ();
}
public static getInstance () {
if ( ! this . instance ) {
this . instance = new RedisManager ();
}
return this . instance ;
}
public pushMessage ( message : DbMessage ) {
this . client . lPush ( "db_processor" , JSON . stringify ( message ));
}
public publishMessage ( channel : string , message : WsMessage ) {
this . client . publish ( channel , JSON . stringify ( message ));
}
public sendToApi ( clientId : string , message : MessageToApi ) {
this . client . publish ( clientId , JSON . stringify ( message ));
}
}
Shared packages
@opinix/types
Shared TypeScript types used across all services:
MessageFromApi - Messages from backend to engine
MessageToApi - Messages from engine to backend
WsMessage - WebSocket message types
Order - Order structure
Fill - Trade fill data
@opinix/logger
Centralized logging utility using Winston or similar.
@repo/order-queue
Queue management package that handles:
Adding orders to Redis queue
Processing orders from queue
Redis pub/sub for real-time updates
All packages use TypeScript for type safety and are compiled before being used by apps and services.