Case study

Nova Prism

A trading platform in four apps: a landing site with live prices, a client portal for leveraged trading, a host panel, and the API under them. Fastify and Socket.IO, a trading engine, contracts checked against OpenAPI, and 58 test files.

  • Fastify
  • Socket.IO
  • OpenAPI
  • Drizzle
  • SQLite
  • React
  • Astro
  • TanStack Query
  • Vitest
  • Playwright
  • Docker

Context

Nova Prism is a trading platform in one pnpm monorepo. A landing site shows live prices. In the client portal, people watch markets, open leveraged positions, set alerts and talk to support. In the host panel, the operator manages clients, balances and live positions. Under all three is one API on Node 24: REST, WebSocket, a trading engine and price feeds.

The problem

A trading screen is only as good as its numbers. Price, P/L, margin level and stop-out have to agree in the engine, the portal and the host panel, while prices move every second. Four apps on one API also drift apart fast: a renamed field becomes a blank column somewhere else.

Nova Prism's landing page: the headline "Five markets. One account." beside a panel of five prices, over a scrolling ticker.
The landing site. A local run, with simulated prices.
The client portal's trading screen: a BTC/USD candle chart, the account's margin figures across the top, and an order form at 10x leverage showing the margin it needs.
The trading screen, on a seeded sample account.
Ten seconds of ticks. Equity, P/L and margin level move with every one.

Decisions

  1. 01

    The contract comes first, and a test enforces it

    One OpenAPI file describes all 70 REST operations, and one typed file describes all 25 Socket.IO events. The API registers its routes from the contract and refuses one that isn't in it. Outside production, it checks every response against it. A test fails unless every operation is registered and has been called successfully. The cost: every endpoint starts as YAML, and production skips the response check.

  2. 02

    One copy of the trading math, in decimals

    Margin, P/L and liquidation math live in one package. The engine settles trades with it, and the client portal draws its numbers with the same code. Money is a decimal string from the database to the screen, never a float. The cost: decimal.js in the portal's bundle, and a change to the math changes every app at once.

  3. 03

    Settle every tick in one process

    Open positions are held in memory. Each price tick checks stop-loss, take-profit and margin for the positions on that symbol. Below the stop-out level, the engine closes the worst-losing position, then checks again. SQLite through better-sqlite3 is synchronous, so a settlement can't interleave with an order. The cost: one API process and one database file. Scaling out means moving the engine out first.

Outcome

The four apps start with one command on a seeded local database, and ship as two Docker images behind Caddy. The API tests boot the real Fastify app on an in-memory database and replay recorded Binance data. Playwright runs the portal and the landing site at three widths, in light and dark, with axe, and fails on a console error or a sideways scroll.

apps in one monorepo
4
REST operations, each called in tests
70
typed Socket.IO events
25
test files
58

What I'd do differently

I'd split the engine from the API before adding anything else. Today the price feed, the engine, the sockets and the REST API share one Node process and one SQLite file. That kept it simple to build and to test. It's also the first thing to move if it ever needs a second machine.