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 |
|---|---|---|---|---|
| Frontend | Next.js | HTTP | :3000 | App Router, NextAuth |
| Go | Gateway REST | HTTP | :8080 | Fiber framework |
| Gateway WebSocket | WS | :8081 | gorilla/websocket Hub | |
| Ingestion | healthz | :9100 | master broker listener | |
| Router | healthz | :9101 | fan-out engine | |
| Executor | healthz | :9102 | (reserved) | |
| Tracker | healthz | :9103 | P&L calculator | |
| Infra | PostgreSQL | TCP | :5432 | persistent volume |
| Redis | TCP | :6379 | sessions + position cache | |
| Kafka (KRaft) | TCP | :9092 | no ZooKeeper | |
| Observability | Prometheus | HTTP | :9090 | scrapes /metrics |
| Grafana | HTTP | :3001 | Copy Trade dashboard | |
| Redpanda Console | HTTP | :8090 | topic 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