Blog
ENPL

Trading dashboard FastAPI + Next.js — architecture of my setup

I run an internal trading dashboard: FastAPI backend + Next.js frontend, deployed as Docker compose, exposed via Cloudflare tunnel with Zero Trust auth. Showing the structure and key decisions.

·4 min read
Trading dashboard FastAPI + Next.js — architecture of my setup

My trading dashboard is an internal web app for monitoring positions, alerts and trading analytics. Stack: Python FastAPI backend + Next.js frontend + Postgres. Codename "Kain" (the team wanted a name). Showing how it's built.

High-level

[Cloudflare tunnel + Zero Trust auth]

[forge:3002 — Next.js frontend]
    ↓ /api/*
[forge:8000 — FastAPI backend]

[Postgres (Proxmox VM)]
    ↓ async
[Trading exchange APIs + scrapers]

Frontend and backend run on the forge PC (RTX 3070 Ti, because some analytics use GPU for ML inference). Postgres on the hypervisor. Channel: HTTPS via tunnel, JWT auth.

Backend: FastAPI

Why Python: the ML/data ecosystem is uniquely rich. pandas, numpy, scikit-learn, native PyTorch. FastAPI adds async + auto-generated OpenAPI.

# main.py structure
from fastapi import FastAPI, Depends
from pydantic import BaseModel
 
app = FastAPI(title="Kain Trading API")
 
class Position(BaseModel):
    symbol: str
    side: str  # long/short
    entry_price: float
    current_price: float
    pnl: float
 
@app.get("/positions", response_model=list[Position])
async def get_positions(user_id: int = Depends(verify_jwt)):
    return await db.fetch_positions(user_id)

Pydantic models = automatic validation + auto-generated TS for the frontend. The JSON contract is typed end-to-end.

Frontend: Next.js

Next.js 16 with App Router, server components for data fetching, client components for interactivity. I use React Query for caching API responses.

// app/positions/page.tsx — server component
import { fetchPositions } from "@/lib/api";
 
export default async function PositionsPage() {
  const positions = await fetchPositions();
  return <PositionsTable initialData={positions} />;
}
 
// PositionsTable — client component with React Query
"use client";
import { useQuery } from "@tanstack/react-query";
 
export function PositionsTable({ initialData }) {
  const { data } = useQuery({
    queryKey: ["positions"],
    queryFn: fetchPositions,
    initialData,
    refetchInterval: 5000,  // 5s polling
  });
 
  return <Table data={data} />;
}

Server component does the initial fetch (SSR), client component polls updates. SEO and interactivity together.

Auth: JWT + Cloudflare Zero Trust

Two layers of authorization:

1. Cloudflare Zero Trust, first gate. Only someone logged in via Google on my email can even reach the app.

2. JWT after Zero Trust, second gate. App has multiple users (me, trading partner). JWT identifies which user.

async def verify_jwt(authorization: str = Header(...)):
    token = authorization.replace("Bearer ", "")
    try:
        payload = jwt.decode(token, SECRET, algorithms=["HS256"])
        return payload["user_id"]
    except jwt.InvalidTokenError:
        raise HTTPException(401, "Invalid token")

Cloudflare gives the first layer for free. JWT is standard boilerplate.

Database: Postgres

Three main tables:

  • positions, open positions (~1000 rows usually)
  • trades, executed trades (~500k historic rows)
  • alerts, triggered alerts (~10k rows)

Indexes on user_id, symbol, timestamp. Queries usually under 50ms.

Key detail: Postgres isn't on forge, but on the hypervisor (Proxmox VM). Forge can crash (GPU OOM during ML inference) without losing data.

# docker-compose.yml on forge
services:
  backend:
    build: ./backend
    environment:
      DATABASE_URL: postgres://kain:[email protected]:5432/kain
    ports:
      - "127.0.0.1:8000:8000"

Real-time updates

Some positions need sub-second updates (when a trade is hot). WebSocket from backend to frontend:

# Backend FastAPI WebSocket
@app.websocket("/ws/positions")
async def positions_ws(websocket: WebSocket):
    await websocket.accept()
    async for update in pg_listen("positions_updates"):
        await websocket.send_json(update)
// Frontend
const ws = new WebSocket("wss://kain.kamilkaletka.dev/ws/positions");
ws.onmessage = (event) => {
  const update = JSON.parse(event.data);
  queryClient.setQueryData(["positions"], (old) =>
    old.map(p => p.id === update.id ? update : p)
  );
};

Postgres LISTEN/NOTIFY as a message bus. Trade execution inserts a row, the trigger sends notify, backend forwards via WebSocket.

Traps that cost me

1. Python rebuild vs restart. I edit .py, run docker compose restart backend, code is still stale. You need up -d --build. Forgetting cost me an hour of "why doesn't my fix work" debugging.

2. SOCKS5 + internal DNS. Backend hits hostname proxmox-pg.local. SOCKS5 proxy on host tries to resolve externally and fails. Add proxmox-pg.local to NO_PROXY.

3. WebSocket through Cloudflare tunnel. Works, but latency +30ms vs direct. Fine for most UI, marginal for trade execution alerts.

4. JSON serialization of Decimal. Postgres returns Decimal, JSON doesn't handle natively. Custom encoder in FastAPI:

class DecimalEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Decimal):
            return float(obj)
        return super().default(obj)

Otherwise every position fetch = error.

Stats

The dashboard has been live ~14 months. Numbers:

  • ~500k recorded trades
  • ~12k unique historic positions
  • Avg API response time: 35ms
  • Uptime: 99.7% (downtime mostly during Postgres updates)

It's not a 10k req/s trading engine. It's an internal dashboard where my partner and I keep a tab open. The architecture matches the scale.


FastAPI + Next.js is a great combo for internal tools. End-to-end type safety, ML/data ecosystem on Python's side, modern UX on React's side. Cloudflare tunnel and Zero Trust give enterprise-grade auth without infra. Setup takes a weekend, works for years.