Common Architecture

Common Architecture

Go backend · Next.js 14 · Kafka · PostgreSQL · Redis — multi-broker copy trading platform

1

Full System Map

flowchart TB classDef browser fill:#E6F1FB,stroke:#185FA5,color:#0C447C classDef nextjs fill:#E6F1FB,stroke:#185FA5,color:#0C447C classDef goservice fill:#EAF3DE,stroke:#3B6D11,color:#27500A classDef kafka fill:#FAEEDA,stroke:#854F0B,color:#633806 classDef db fill:#FCEBEB,stroke:#A32D2D,color:#791F1F classDef external fill:#EEEDFE,stroke:#534AB7,color:#3C3489 classDef monitor fill:#F1EFE8,stroke:#5F5E5A,color:#444441 BROWSER["🖥️ Browser\nNext.js :3000"]:::browser subgraph NEXTLAYER["Next.js API Layer"] direction TB NA["NextAuth\n/api/auth/[...nextauth]\n▸ credentials provider\n▸ stores goToken in session"]:::nextjs NACCTS["Account Routes\n/api/accounts/:id\n▸ proxies to Go /accounts\n▸ attaches Bearer token"]:::nextjs NPXAUTH["ProjectX Sync\n/api/projectx/auth\n▸ server-side auth\n▸ parallel account import"]:::nextjs end subgraph GOLAYER["Go Backend"] direction TB subgraph GW["Gateway :8080 REST | :8081 WS"] GWREST["REST Handlers\n/auth/* /accounts/*\n/orders/* /dashboard/*\n/health /metrics"]:::goservice GWWS["WebSocket Hub\nthread-safe client registry\nbroadcast + user-scoped routing"]:::goservice GWBRIDGE["Kafka→WS Bridge\n5 consumer goroutines\nEXECUTION|POSITION|ACCOUNT\nERROR|HEARTBEAT envelopes"]:::goservice end IN["📡 Ingestion :9100\n▸ connects to master broker\n▸ normalizes fills → TradeSignal\n▸ publishes to trade.signals\n▸ retries w/ 30s backoff on startup"]:::goservice RO["⚡ Router :9101\n▸ consumes trade.signals\n▸ loads enabled followers from DB\n▸ applies scaling rules\n▸ fan-out with 2.5s deadline\n▸ circuit breaker per broker\n▸ persists executions to DB"]:::goservice TR["📊 Tracker :9103\n▸ consumes trade.executions\n▸ weighted avg cost basis\n▸ realized + unrealized P&L\n▸ publishes positions.updates\n▸ persists positions to Redis"]:::goservice end subgraph KAFKA["Kafka (KRaft) :9092"] direction LR K1(["trade.signals\n3 partitions · 7d retention\nkey: symbol"]):::kafka K2(["trade.executions\n10 partitions · 30d retention\nkey: account_id"]):::kafka K3(["positions.updates\n10 partitions · 1d retention\nkey: account_id"]):::kafka K4(["account.status\n20 partitions"]):::kafka K5(["order.errors\n5 partitions"]):::kafka K6(["system.heartbeat\n1 partition"]):::kafka end subgraph STORES["Datastores"] direction TB PG[("🐘 PostgreSQL :5432\nusers · accounts · credentials\norders · positions")]:::db RD[("⚡ Redis :6379\njwt sessions (TTL 24h)\naccount→user mapping\nposition state cache")]:::db end subgraph BROKERS["Broker APIs"] direction TB PXB["ProjectX\nREST + SignalR"]:::external TRB["Tradovate\nREST + WS"]:::external RIB["Rithmic\nProtobuf WS"]:::external OTH["DxFeed / NinjaTrader\nQuantower / SierraChart"]:::external end subgraph MON["Observability"] direction TB PROM["Prometheus :9090\nfills_detected · signals_published\nfanout_duration · active_followers\norders_placed"]:::monitor GRAF["Grafana :3001\nCopy Trade dashboard\nlatency · fill rate · P&L"]:::monitor RP["Redpanda Console :8090\ntopic inspector"]:::monitor end BROWSER <-->|"HTTP REST\nGET/POST/PATCH/DELETE"| NEXTLAYER BROWSER <-->|"WS :8081\nJSON envelopes"| GWWS NA -->|"POST /auth/login\nPOST /auth/register"| GWREST NACCTS -->|"Bearer token\n→ Go /accounts"| GWREST NPXAUTH -->|"POST /accounts ×N\nPromise.all()"| GWREST GWREST --- PG & RD GWWS --- RD GWBRIDGE -->|"consumes 5 topics"| KAFKA GWBRIDGE --> GWWS IN -->|"Connect + Subscribe"| BROKERS IN -->|"TradeSignal {id, symbol, side, qty, price}"| K1 K1 -->|"consume\ngroup: router-group"| RO RO -->|"PlaceOrder × N followers"| BROKERS RO -->|"Execution {status, fill_price, latency_ms}"| K2 RO --- PG & RD K2 -->|"consume\ngroup: tracker-group"| TR TR -->|"Position {qty, avg_price, pnl}"| K3 TR --- RD IN & RO & TR & GWREST -.->|"/metrics scrape"| PROM PROM --> GRAF KAFKA -.-> RP
2

Request Lifecycle — REST API Call

sequenceDiagram box rgb(230,241,251) Client participant BR as Browser end box rgb(230,241,251) Next.js participant NX as Next.js Route Handler participant NA as NextAuth Session end box rgb(234,243,222) Go Gateway participant MW as JWTProtected Middleware participant H as Handler end box rgb(252,235,235) Datastores participant RD as Redis participant PG as PostgreSQL end BR->>NX: fetch("/api/accounts") NX->>NA: getServerSession() NA-->>NX: { goToken: "eyJ..." } NX->>MW: GET /accounts\nAuthorization: Bearer eyJ... MW->>MW: jwt/v5 ParseWithClaims(token, secret) MW->>RD: GET tradenova:jwt:session:{userId} RD-->>MW: session exists ✓ MW->>H: Locals["userID"] = userId H->>PG: SELECT * FROM accounts\nWHERE owner_id = $1 PG-->>H: []Account rows H-->>BR: 200 JSON []Account
3

Copy Trade Pipeline — End to End

flowchart TD classDef step fill:#E6F1FB,stroke:#185FA5,color:#0C447C classDef kafka fill:#FAEEDA,stroke:#854F0B,color:#633806 classDef broker fill:#EEEDFE,stroke:#534AB7,color:#3C3489 classDef db fill:#FCEBEB,stroke:#A32D2D,color:#791F1F classDef gogreen fill:#EAF3DE,stroke:#3B6D11,color:#27500A classDef warn fill:#FFF3CD,stroke:#856404,color:#533F03 classDef discard fill:#F8D7DA,stroke:#721C24,color:#491217 MASTER(["👤 Master Account\nplaces order at broker"]):::broker subgraph INGEST["INGESTION SERVICE :9100"] I1["Broker Adapter\nSubscribe(ctx, events chan)"]:::gogreen I2["Fill Event Received\n{OrderID, Symbol, Side, Qty, Price}"]:::step I3["normalize(event)\n→ TradeSignal{id, correlationID,\n symbol, side, qty, price,\n orderType, sourceAccount}"]:::gogreen I4["producer.Publish(symbol, signal)\n→ trade.signals Kafka topic"]:::kafka end subgraph ROUTE["ROUTER SERVICE :9101"] R1["Consume trade.signals\ngroup: router-group"]:::kafka R2["DB: SELECT accounts\nWHERE is_master=false\nAND is_enabled=true\nAND owner_id=ownerOfMaster"]:::db R3["risk.Check(signal, account)\nwhitelist · max_contracts\n→ AdjQty or REJECTED"]:::gogreen R4["applyScaling(qty, account)\nfixed | proportional | percent"]:::gogreen end subgraph FANOUT["FAN-OUT (parallel goroutines — sync.WaitGroup)"] FA["Goroutine: Account A\nctx timeout: 2.5s"]:::gogreen FB["Goroutine: Account B\nctx timeout: 2.5s"]:::gogreen FC["Goroutine: Account C\nctx timeout: 2.5s"]:::gogreen CBA{"Circuit Breaker A\n5 failures → OPEN\nhalf-open after 30s"} CBB{"Circuit Breaker B"} CBC{"Circuit Breaker C"} end subgraph EXEC["EXECUTION"] E1["adapter.PlaceOrder(ctx, order)\n{ID: uuid.NewString(),\n AccountRef, Symbol,\n Side, Qty, Type}"]:::gogreen E2["Broker ACK\n{brokerOrderID, status}"]:::broker end subgraph PERSIST["PERSIST & PUBLISH"] P1["DB INSERT orders\n{signal_id, account_id,\n status, fill_price,\n latency_ms}"]:::db P2["Kafka: trade.executions\n{correlationID, status,\n fill_price, latency_ms}"]:::kafka end subgraph TRACK["TRACKER SERVICE :9103"] T1["Consume trade.executions\ngroup: tracker-group"]:::kafka T2["Update position state\nweighted avg cost basis"]:::gogreen T3["Calc P&L\nrealized + unrealized"]:::gogreen T4["Redis HSET position state\nKafka: positions.updates"]:::kafka end subgraph DISPLAY["GATEWAY :8080/:8081"] G1["Bridge goroutine\nconsumes trade.executions\n+ positions.updates"]:::step G2["resolveOwner(payload)\nRedis: tradenova:account:owner:id"]:::step G3["hub.BroadcastToUser(userID)\nJSON envelope {type, data}"]:::step G4["🖥️ Browser Updates\nOrders table · P&L · Dashboard"]:::broker end MASTER --> I1 --> I2 --> I3 --> I4 I4 --> R1 --> R2 --> R3 --> R4 R4 --> FA & FB & FC FA --> CBA FB --> CBB FC --> CBC CBA -->|closed| E1 CBB -->|closed| E1 CBC -->|closed| E1 CBA -->|open| DISCARD1(["⊘ REJECTED\ngobreaker.ErrOpenState"]):::discard CBB -->|open| DISCARD2(["⊘ REJECTED"]):::discard CBC -->|open| DISCARD3(["⊘ REJECTED"]):::discard E1 --> E2 --> P1 --> P2 P2 --> T1 --> T2 --> T3 --> T4 P2 --> G1 --> G2 --> G3 --> G4
4

User Login + Session Flow

sequenceDiagram box rgb(230,241,251) Browser participant B as Browser end box rgb(230,241,251) Next.js participant N as NextAuth end box rgb(234,243,222) Go Gateway participant G as /auth/login end box rgb(252,235,235) Datastores participant P as PostgreSQL participant R as Redis end B->>N: POST /api/auth/[...nextauth]\n{ username, password } N->>G: POST /auth/login\n{ username, password } G->>P: SELECT id, password_hash\nFROM users WHERE username=$1 P-->>G: { id, hash } G->>G: bcrypt.CompareHashAndPassword(hash, password) ✓ G->>G: jwt.NewWithClaims(HS256, {sub:userId, exp:24h}) G->>R: SET tradenova:jwt:session:{userId}\nvalue=1, EX=86400 G-->>N: { token: "eyJ...", expires_in: 86400 } N-->>B: NextAuth session\n{ goToken: "eyJ..." } Note over B,R: Subsequent requests attach session goToken as Bearer token B->>N: GET /api/orders → goAuthHeader() N->>G: GET /orders\nAuthorization: Bearer eyJ... G->>R: GET tradenova:jwt:session:{userId} R-->>G: "1" (session valid) G-->>B: 200 orders JSON Note over B,R: Logout B->>N: POST /api/auth/signout N->>G: POST /auth/logout G->>R: DEL tradenova:jwt:session:{userId} G-->>N: 200 OK N-->>B: session cleared
5

Database Schema

erDiagram USERS { text id PK text username UK text name text password_hash timestamptz created_at } ACCOUNTS { uuid id PK varchar name varchar broker varchar account_ref "broker-native ID" boolean is_master boolean is_enabled varchar scaling_mode "fixed|proportional|percent" decimal multiplier int max_contracts text[] symbol_whitelist uuid owner_id FK timestamptz created_at timestamptz updated_at } CREDENTIALS { uuid id PK uuid account_id FK varchar key_name "username|api_key|password" text encrypted_value "pgcrypto AES" timestamptz created_at } ORDERS { uuid id PK uuid account_id FK uuid signal_id varchar broker_order_id varchar symbol varchar side "BUY|SELL" int qty decimal fill_price varchar status "WORKING|FILLED|REJECTED" int latency_ms timestamptz created_at timestamptz filled_at } POSITIONS { uuid id PK uuid account_id FK varchar symbol varchar side int qty decimal avg_price decimal unrealized_pnl decimal realized_pnl timestamptz updated_at } USERS ||--o{ ACCOUNTS : owns ACCOUNTS ||--o{ CREDENTIALS : has ACCOUNTS ||--o{ ORDERS : "executed on" ACCOUNTS ||--o{ POSITIONS : holds
6

Kafka Topic Map

flowchart LR classDef prod fill:#EAF3DE,stroke:#3B6D11,color:#27500A classDef topic fill:#FAEEDA,stroke:#854F0B,color:#633806 classDef cons fill:#E6F1FB,stroke:#185FA5,color:#0C447C IN["📡 Ingestion"]:::prod RO["⚡ Router"]:::prod TR["📊 Tracker"]:::prod T1(["trade.signals\n3p · 7d · key=symbol"]):::topic T2(["trade.executions\n10p · 30d · key=account_id"]):::topic T3(["positions.updates\n10p · 1d · key=account_id"]):::topic T4(["account.status\n20p"]):::topic T5(["order.errors\n5p"]):::topic T6(["system.heartbeat\n1p"]):::topic GW["🔀 Gateway Bridge\n5 consumer goroutines"]:::cons RO2["⚡ Router\ngroup: router-group"]:::cons TR2["📊 Tracker\ngroup: tracker-group"]:::cons IN -->|TradeSignal| T1 RO -->|Execution| T2 RO -->|broker errors| T5 TR -->|Position| T3 T1 --> RO2 T2 --> TR2 T2 --> GW T3 --> GW T4 --> GW T5 --> GW T6 --> GW
7

Service & Port Reference

Layer Service Protocol Port Notes
FrontendNext.jsHTTP:3000App Router, NextAuth
GoGateway RESTHTTP:8080Fiber framework
Gateway WebSocketWS:8081gorilla/websocket Hub
Ingestionhealthz:9100master broker listener
Routerhealthz:9101fan-out engine
Executorhealthz:9102(reserved)
Trackerhealthz:9103P&L calculator
InfraPostgreSQLTCP:5432persistent volume
RedisTCP:6379sessions + position cache
Kafka (KRaft)TCP:9092no ZooKeeper
ObservabilityPrometheusHTTP:9090scrapes /metrics
GrafanaHTTP:3001Copy Trade dashboard
Redpanda ConsoleHTTP:8090topic inspector

Color Legend

Blue
Browser / Next.js / Frontend
Green
Go backend services
Amber
Kafka topics & messaging
Red
Datastores (PostgreSQL, Redis)
Purple
External broker APIs
Gray
Observability / Monitoring
Yellow
Fallback / warning paths