Monorepo skeleton for a trading platform backend and frontend.
- Node.js 20+
- MongoDB running as a replica set
- Redis running locally or reachable through the configured URL
npm install
Copy-Item backend/.env.example backend/.env
npm run buildThe Vite/React frontend now provides a dark trading terminal at the root route. It is organized as:
frontend/src/context/SocketContext.tsx— singleton Socket.io connection, JWT auth, and connection statefrontend/src/components/OrderBook.tsx— live bid/ask depth with volume barsfrontend/src/components/MarketTrades.tsx— scrolling execution tapefrontend/src/components/OrderForm.tsx— authenticated limit order submission and live balance updatesfrontend/src/components/AccountSection.tsx— account popover with identity, expiry, balances, and logoutfrontend/src/components/UserOrders.tsx— REST-hydrated and Socket.io-updated personal ordersfrontend/src/dashboard.tsx— responsive dashboard layout and chart surfacefrontend/src/styles.css— responsive dark terminal design system
This repository uses Vite rather than Next.js, so the equivalent public environment variables use the VITE_ prefix. Copy frontend/.env.example to frontend/.env if you need to change the default API or WebSocket URL.
From the repository root, start Docker services and both workspaces:
docker compose up -d
npm run devOpen the Vite URL shown in the terminal. Use Sign in to register or log in directly inside the dashboard. The dashboard joins market:BTC-USD; queued orders, order-book changes, executions, order updates, and balance updates stream through Socket.io.
The market-maker script at scripts/simulateMarket.ts registers and funds simulated users, creates resting bids and asks every 50-200ms, creates aggressive crossing orders every 1-3 seconds, and cancels stale orders after 25 seconds. It uses the real REST order endpoint, so traffic passes through BullMQ, matching, settlement, and Socket.io.
Run it from the repository root while Docker and the backend are running:
npm install
npm run simulate:marketThe requested command is:
npx ts-node scripts/simulateMarket.tsHowever, this repository uses native ES modules with Node.js 22, and the installed ts-node 10 runner fails with ERR_UNKNOWN_FILE_EXTENSION. The verified working command is npm run simulate:market, which uses tsx.
Optional configuration:
$env:SIM_API_URL="http://localhost:4000"
$env:MONGODB_URI="mongodb://localhost:27017/execution_engine?replicaSet=rs0"
npm run simulate:marketOpen the frontend in another terminal and watch BTC-USD depth and executions update live. Stop the simulator with Ctrl+C.
Set an email as an admin before registering that account:
ADMIN_EMAILS=admin@example.com
Email: admin@arc.io
Password: Admin123!Restart the backend, register or log in as admin@example.com, and open the Admin visualizer button in the navbar. Only JWTs with role: "ADMIN" can join admin:telemetry; regular users can still receive public market events but never receive queue, heap, or transaction telemetry.
The visualizer includes a forced rollback test button, which emits a ROLLED_BACK telemetry record to the admin room without changing trading balances.
Update backend/.env with the local MongoDB replica set, Redis host/port, and JWT secrets, then start both applications:
npm run devThe backend health endpoint is available at http://localhost:4000/health.
The backend now includes the core schema and authentication layer:
- Models:
User,Account,Order, andTradeare defined underbackend/src/models/ - Auth routes:
POST /auth/register,POST /auth/login, andPOST /auth/refreshlive underbackend/src/routes/ - Protected user route:
GET /api/meis guarded byrequireAuthinbackend/src/middleware/ - Controllers: request handling lives in
backend/src/controllers/ - Security tokens: JWT signing/verification is managed in
backend/src/services/
- Access token: short-lived JWT in the JSON response body, approx. 15 minutes
- Refresh token: 7-day HTTP-only cookie, stored in Redis for immediate invalidation
- Protected routes require the Authorization header:
Bearer <accessToken>
- Register or log in using
/auth/registeror/auth/login - Receive
accessTokenin the JSON body - Receive
refreshTokencookie set by the server - Call
GET /api/mewithAuthorization: Bearer <accessToken> - Refresh expired access tokens using
POST /auth/refresh
backend/src/models/— Mongoose schemas and exported TypeScript interfacesbackend/src/controllers/— auth and user request handlersbackend/src/middleware/— auth enforcement middlewarebackend/src/routes/— route registrationbackend/src/services/— JWT and Redis token logicbackend/src/types/— Express request augmentation forreq.user
The order flow is now available under /api/orders:
backend/src/matching/MatchingEngine.ts— in-memory price-time matching and heap removalbackend/src/services/settlementService.ts— MongoDB session transaction for balance transfers, order fills, and tradesbackend/src/controllers/orderController.ts— authenticated order validation and placement/list/cancel handlersbackend/src/routes/orderRoutes.ts— protected/api/ordersroute definitions
POST /api/orders— validates balances, creates a limit order, matches it, and settles any tradesGET /api/orders— lists the authenticated user's ordersDELETE /api/orders/:id— removes an active order from the in-memory book and marks itCANCELLED
Settlement splits a pair such as BTC-USD into base asset BTC and quote currency USD. Every matched trade transfers the quote amount from buyer to seller and the base quantity from seller to buyer inside one MongoDB transaction. If any account, order, or trade write fails, the transaction is aborted.
- Register two users with
POST http://localhost:4000/auth/register. Registration creates test balances of1 BTC,10 ETH, and100000 USD. - Log in each user with
POST http://localhost:4000/auth/loginand copy each access token. - Create a sell order for the seller:
{
"pair": "BTC-USD",
"side": "SELL",
"price": 100,
"quantity": 0.25
} Send it to `POST http://localhost:4000/api/orders` with `Authorization: Bearer <seller-access-token>`.
- Create a crossing buy order for the buyer:
{
"pair": "BTC-USD",
"side": "BUY",
"price": 105,
"quantity": 0.25
} Send it with the buyer token. It executes at the resting sell price of `100` and returns the trade in `trades`.
- Use
GET http://localhost:4000/api/orderswith either token to inspect that user's orders. - Create a non-crossing order, copy its returned order id, then call
DELETE http://localhost:4000/api/orders/<id>with the same user's token.
Use Content-Type: application/json for POST requests. Keep the backend running with npm run dev -w backend and MongoDB/Redis running through Docker Compose.
The asynchronous order path is organized as follows:
backend/src/config/redis.ts— shared ioredis client and Redis connection options fromREDIS_HOSTandREDIS_PORTbackend/src/queues/orderQueue.ts— BullMQorder-queuedefinition and retry defaultsbackend/src/workers/orderWorker.ts— worker that loadsPENDINGorders, matches, settles, and persists resultsbackend/src/server.ts— starts the worker after MongoDB and Redis connect, then stops it during shutdown
POST /api/orders now returns 202 Accepted immediately with a PENDING order. The worker later changes it to OPEN, PARTIALLY_FILLED, or FILLED.
- Start Docker services and the backend:
docker compose up -d
npm run dev -w backend- Submit an authenticated order through Postman or Bruno.
- Confirm the immediate response is
202and the order status isPENDING. - Watch the backend terminal for
Processed order ...andOrder job ... completed. - Call
GET /api/ordersagain to see the worker-updated status.
You can inspect Redis directly with:
docker exec local_redis redis-cli KEYS "bull:order-queue:*"
docker exec local_redis redis-cli LLEN "bull:order-queue:wait"Completed jobs are removed after the configured retention threshold, while failed jobs are retained for inspection. Database errors are retried with exponential backoff up to five attempts.
Socket.io is attached to the HTTP server in backend/src/server.ts through backend/src/config/socket.ts.
market:joinjoins a public room such asmarket:BTC-USDmarket:leaveleaves a public market room- authenticated sockets automatically join
user:<userId> - access tokens are supplied through the Socket.io
auth.tokenfield or an Authorization header
The server emits:
orderbook:updatetomarket:<pair>after orders are added, filled, or cancelledtrade:executedtomarket:<pair>after settlement commitsorder:updatedto the affected user roombalance:updatedto the affected user room
Run this in a browser page or Node.js script after installing socket.io-client:
import { io } from 'socket.io-client';
const socket = io('http://localhost:4000', {
auth: { token: '<ACCESS_TOKEN>' },
});
socket.on('connect', () => {
console.log('connected', socket.id);
socket.emit('market:join', 'BTC-USD');
});
socket.on('orderbook:update', (update) => console.log('order book', update));
socket.on('trade:executed', (trade) => console.log('trade', trade));
socket.on('order:updated', (order) => console.log('private order', order));
socket.on('balance:updated', (balance) => console.log('private balance', balance));
socket.on('connect_error', (error) => console.error('socket error', error.message));Submit an order through POST /api/orders after connecting. The HTTP response is still 202 PENDING; the worker then produces the Socket.io updates.
The live execution-flow test is located at backend/tests/integration/executionFlow.test.ts. It uses Supertest against the real Express app, the real MongoDB replica set, the real Redis-backed BullMQ queue, the worker, the matching engine, and the settlement transaction.
The test uses these isolated resources:
- MongoDB database:
execution_engine_test - BullMQ queue:
order-queue-test - Redis: the local Docker Redis instance on
127.0.0.1:6379
Start the infrastructure first:
docker compose up -dThen run the complete end-to-end verification from the repository root:
npm run test:integrationThe test registers Trader A and Trader B, seeds Trader A with $100,000 USD and Trader B with 2 BTC, submits the $50,000 sell and buy orders, verifies the PENDING response and Redis job payload, waits for the sell order to rest in the ask book, and asserts final account balances, filled orders, and the created trade. The $100,000 seed is necessary because the requested $50,000 order cannot be funded from only $10,000.
The root scripts are:
npm run dev— starts backend/worker and frontend concurrentlynpm run test:integration— runs the isolated live execution-flow testnpm run build— builds both workspacesnpm run lint— lints both workspaces
For a full local check:
docker compose up -d
npm run lint
npm test -w backend
npm run test:integration
npm run build