NexTrade Documentation
Full-stack cryptocurrency exchange platform with real-time trading, WebSocket streaming, multi-factor authentication, and admin analytics.
What is NexTrade?
NexTrade is a production-ready cryptocurrency trading platform that proxies all data through its own API server. Users connect their Binance API keys to execute real trades, while the platform adds authentication, fee management, analytics, and real-time data streaming.
Key Features
- Real-time market data — Proxied from Binance with Redis caching and WebSocket fan-out
- 7 order types — Limit, Market, Stop-Limit, Stop-Loss, Take-Profit, Take-Profit Limit, OCO
- Secure auth — Email/password with optional TOTP 2FA, account lockout, email verification
- Encrypted key storage — AES-256-GCM encryption for Binance API keys and TOTP secrets
- Trading fees — Configurable global and per-user maker/taker fees
- Admin panel — User management, fee configuration, platform statistics
- Analytics dashboard — Volume tracking, P&L, trade history with CSV export
- Demo mode — Simulated trading for users without API keys
- Docker-ready — Multi-stage builds, docker-compose, nginx with TLS
Tech Stack
| Layer | Technology | Version |
|---|---|---|
| Frontend | Next.js + React + TypeScript | 16.1 / 19.2 / 5.9 |
| Styling | Tailwind CSS | 4.x |
| State | Zustand | 5.x |
| Charts | Lightweight Charts | 4.x |
| Backend | Fastify + TypeScript | 5.x / 5.6 |
| ORM | Prisma | 6.x |
| Database | PostgreSQL | 17 |
| Cache | Redis | 7 |
| Runtime | Node.js | 22 LTS |
Architecture
Request Flow
/api/*) or frontend (/*)X-Request-Id header for traceabilityBackend Directory Structure
server/
├── prisma/
│ ├── schema.prisma # Database models
│ └── seed.ts # Admin account seeder
└── src/
├── index.ts # Server bootstrap + graceful shutdown
├── config/env.ts # Zod-validated environment config
├── plugins/ # Prisma, Redis, JWT, CORS, Helmet, Rate-Limit, WS
├── middleware/ # auth, admin, require-api-key
├── routes/ # auth, user, market, trading, analytics, admin
├── services/ # Business logic layer
├── lib/
│ ├── crypto.ts # AES-256-GCM encrypt/decrypt
│ ├── password.ts # Argon2id hash/verify
│ ├── totp.ts # TOTP generate/verify
│ ├── email.ts # Nodemailer templates
│ └── binance/ # REST client + WS manager
├── cron/ # Scheduled jobs
├── ws/ # WebSocket handlers + fan-out
└── __tests__/ # 354 backend tests
Getting Started
Prerequisites
- Node.js 22+ — Runtime
- pnpm — Package manager (
corepack enable) - PostgreSQL 17 — Database
- Redis 7 — Cache and session store
- Binance API Key — For live trading (optional for demo mode)
Quick Start
git clone <repo-url> nextrade
cd nextrade
# Frontend
pnpm install
# Backend
cd server
pnpm install
cp .env.example .env
# Edit .env — set DATABASE_URL, secrets, etc.
# Generate Prisma client + push schema
npx prisma generate
npx prisma db push
# Seed admin account
pnpm db:seed
# Terminal 1 — Backend (port 3007)
cd server && pnpm dev
# Terminal 2 — Frontend (port 3008)
cd .. && pnpm dev
curl http://localhost:3007/api/health
# {"success":true,"data":{"status":"ok","checks":{"database":"ok","redis":"ok"}}}
http://localhost:3008 works in demo mode without a Binance API key. Add your key in Account → API Keys for live trading.Configuration
Generate Secure Secrets
Never use default/example values in production. Generate cryptographically secure secrets:
# JWT secrets (min 32 characters)
openssl rand -base64 48
# Encryption key (64 hex chars = 256-bit AES key)
openssl rand -hex 32
ENCRYPTION_KEY is irrecoverable. If lost, all stored Binance API keys and TOTP secrets become permanently unreadable. Back it up in a secure secrets manager.Password Requirements
- Minimum 8 characters
- At least one lowercase letter
- At least one uppercase letter
- At least one number
- At least one special character
Default Admin Account
Created on first seed via pnpm db:seed:
| Field | Value |
|---|---|
| admin@cryptoexchange.com | |
| Password | Admin@123456 |
| Role | ADMIN |
ADMIN_EMAIL and ADMIN_PASSWORD in your .env.Database Schema
7 Prisma models backed by PostgreSQL. All IDs are CUIDs. Financial values use Decimal(20,8) precision.
User
| Column | Type | Description |
|---|---|---|
| id | String (PK) | CUID identifier |
| String (unique) | User email address | |
| passwordHash | String | Argon2id hashed password |
| role | Enum | USER | ADMIN |
| emailVerified | Boolean | Email verification status |
| totpSecret | String? | AES-256-GCM encrypted TOTP secret |
| totpEnabled | Boolean | Whether 2FA is enabled |
| createdAt | DateTime | Account creation time |
ApiKey
Binance API credentials encrypted with AES-256-GCM. Each key and secret has its own IV and auth tag.
| Column | Type | Description |
|---|---|---|
| id | String (PK) | CUID identifier |
| userId | String (FK) | Owner user |
| label | String | Display label (e.g., "Main Key") |
| keyHint | String | Masked: first 6 + last 4 chars |
| encryptedKey | String | AES-encrypted API key |
| iv / authTag | String | Encryption parameters for key |
| encryptedSecret | String | AES-encrypted API secret |
| ivSecret / authTagSecret | String | Encryption parameters for secret |
| isActive | Boolean | Active flag (admin can deactivate) |
Order
| Column | Type | Description |
|---|---|---|
| id | String (PK) | CUID identifier |
| userId | String (FK) | Owner user |
| binanceOrderId | String? | Binance internal order ID |
| symbol | String | Trading pair (e.g., BTCUSDC) |
| side | Enum | BUY | SELL |
| type | Enum | LIMIT, MARKET, STOP_LIMIT, STOP_LOSS, TAKE_PROFIT, TAKE_PROFIT_LIMIT, OCO |
| status | Enum | NEW, PARTIALLY_FILLED, FILLED, CANCELED, REJECTED, EXPIRED |
| price | Decimal(20,8)? | Limit price |
| stopPrice | Decimal(20,8)? | Stop trigger price |
| quantity | Decimal(20,8) | Order quantity |
| filledQty | Decimal(20,8) | Cumulative filled quantity |
| timeInForce | String | GTC, IOC, or FOK |
Trade
Each fill on an order creates a Trade record with fee details.
| Column | Type | Description |
|---|---|---|
| price | Decimal(20,8) | Execution price |
| quantity | Decimal(20,8) | Executed quantity |
| fee | Decimal(20,8) | Trading fee amount |
| feeRate | Decimal(10,6) | Fee rate (0.001 = 0.1%) |
| feeAsset | String | Asset fee was paid in |
| isMaker | Boolean | Whether user was the maker |
FeeConfig & UserFeeOverride
FeeConfig stores global maker/taker fees (default 0.1% each). UserFeeOverride allows per-user custom rates. The trading service resolves: UserFeeOverride ?? FeeConfig.
AnalyticsSnapshot
Daily aggregate of each user's trading activity. Created by the hourly analytics cron job. Contains: tradeCount, buyVolume, sellVolume, feesGenerated, realizedPnl.
Authentication
JWT Token Strategy
| Token | Expiry | Storage | Purpose |
|---|---|---|---|
| Access Token | 15 min | localStorage + Authorization header | API authentication |
| Refresh Token | 7 days | Redis (SHA-256 hash) | Token rotation |
| TOTP Temp Token | 5 min | Redis | 2FA intermediate state |
| Email Verify Token | 24 hours | Redis | Email verification link |
| Password Reset Token | 1 hour | Redis | Password reset link |
Login Flow
Account Lockout
- Threshold: 5 failed login attempts
- Duration: 15-minute lockout
- Tracking: Redis key
lockout:<email>with TTL - Reset: Automatically after TTL, or on successful login
2FA (TOTP)
POST /api/auth/totp/setup — generates secret, returns QR code data URL. Secret held in Redis for 10 min.POST /api/auth/totp/enable with verification code — encrypts secret with AES-256-GCM, stores in DB.tempToken. Client calls POST /api/auth/totp/verify with code to get full JWT.POST /api/auth/totp/disable requires current password + valid TOTP code.Trading Engine
Supported Order Types
| Type | Required Fields | Fee Type | Description |
|---|---|---|---|
| LIMIT | price, quantity, timeInForce | Maker | Execute at specific price or better |
| MARKET | quantity | Taker | Execute immediately at best price |
| STOP_LIMIT | price, stopPrice, quantity | Taker | Limit order triggered at stop price |
| STOP_LOSS | stopPrice, quantity | Taker | Market sell triggered at stop price |
| TAKE_PROFIT | stopPrice, quantity | Taker | Market sell triggered at profit target |
| TAKE_PROFIT_LIMIT | price, stopPrice, quantity | Maker | Limit order triggered at profit target |
| OCO | price, stopPrice, stopLimitPrice, quantity | Mixed | One-Cancels-Other: limit + stop-limit pair |
Order Lifecycle
NEW ──► PARTIALLY_FILLED ──► FILLED
│
├──► CANCELED (user cancel)
├──► REJECTED (validation failure)
└──► EXPIRED (time-in-force expired)
Fee Calculation
Fees are calculated per fill:
fee = fillQuantity × fillPrice × feeRate
// Fee rate resolution:
effectiveRate = UserFeeOverride[userId] ?? FeeConfig.global
makerRate = effectiveRate.makerFee // default 0.001 (0.1%)
takerRate = effectiveRate.takerFee // default 0.001 (0.1%)
All financial calculations use Prisma Decimal (not JavaScript floats) to avoid precision loss.
Order Sync (Cron)
Every 10 seconds, the order-sync cron job:
- Queries all orders with status
NEWorPARTIALLY_FILLED - Polls Binance for each order's current status
- Records new fills as Trade records with fee calculations
- Updates order status and
filledQty - Broadcasts updates via WebSocket to the order owner
Market Data
All market data is proxied through the API server with Redis caching. The frontend never talks directly to Binance.
Cache Strategy
| Endpoint | Cache TTL | Warm Strategy |
|---|---|---|
| Trading pairs | 1 hour | On first request |
| 24hr ticker | 5 seconds | Every 30s via cron |
| Order book depth | 2 seconds | On demand |
| Recent trades | 2 seconds | On demand |
| Klines / candles | 10 seconds | On demand |
| User balances | 10 seconds | On demand |
Supported Quote Assets
Trading pairs are filtered to: USDC, USDT, BTC, ETH, BNB
WebSocket System
Connection
// Connect with JWT authentication
ws://localhost:3007/ws?token=<accessToken>
// Max 3 concurrent connections per user
Message Protocol
| Action | Client → Server | Server → Client |
|---|---|---|
| Subscribe | {"action":"subscribe","streams":["BTCUSDC@ticker"]} | {"action":"subscribed","streams":[...]} |
| Unsubscribe | {"action":"unsubscribe","streams":[...]} | {"action":"unsubscribed","streams":[...]} |
| Ping | {"action":"ping"} | {"action":"pong"} |
| Data | — | {"stream":"BTCUSDC@ticker","data":{...}} |
| Order Update | — | {"stream":"user:orders","data":{orderId,status}} |
Available Streams
<symbol>@ticker— 24hr mini ticker updates<symbol>@depth— Order book updates<symbol>@kline_<interval>— Candlestick updates<symbol>@trade— Real-time trades<symbol>@aggTrade— Aggregate tradesuser:orders— Private order status updates (automatic)
Fan-Out Architecture
The server maintains one upstream Binance WebSocket connection per stream and fans out data to all subscribed clients. Reference counting ensures streams are unsubscribed after a 30-second grace period when no clients need them.
Analytics
Daily snapshots aggregated by the hourly cron job. Available via dashboard API and the frontend analytics page.
- Volume tracking — Buy/sell volume per day
- Trade count — Number of executed trades
- Fee totals — Platform fees generated
- Realized P&L — Profit/loss by symbol
- CSV export — Download trade history with formula injection protection
Admin Panel
Accessible to users with the ADMIN role. Admin role is verified against the database on every request (not just the JWT claim).
Capabilities
- Fee management — View/update global maker/taker fees, set per-user overrides
- User management — Paginated user list with order/trade/key counts, user detail view
- API key control — Deactivate or delete any user's API keys
- Order oversight — View all platform orders with user/symbol/status filters
- Platform stats — Total users, orders, trades, fees, active API keys
API Reference
All endpoints return JSON in the format: {"success": true, "data": {...}}. Errors return: {"success": false, "error": "message"}.
Responses include the X-Request-Id header (UUID) for traceability.
Authentication
| Method | Endpoint | Auth | Rate Limit | Description |
|---|---|---|---|---|
| POST | /api/auth/register | Public | 5/hour | Create account, send verification email |
| POST | /api/auth/login | Public | 10/15min | Returns tokens or TOTP challenge |
| POST | /api/auth/refresh | Public | 30/15min | Rotate access + refresh tokens |
| POST | /api/auth/logout | Auth | — | Revoke refresh token |
| GET | /api/auth/verify-email | Public | — | Verify email via token |
| POST | /api/auth/forgot-password | Public | 3/15min | Send password reset email |
| POST | /api/auth/reset-password | Public | 5/15min | Reset password with token |
| POST | /api/auth/totp/setup | Auth | — | Generate TOTP secret + QR |
| POST | /api/auth/totp/enable | Auth | — | Enable 2FA after code verification |
| POST | /api/auth/totp/disable | Auth | — | Disable 2FA (password + code required) |
| POST | /api/auth/totp/verify | Public | 10/15min | Complete 2FA login |
Register Request
POST /api/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "MySecure@Pass1"
}
Login Response (TOTP disabled)
{
"success": true,
"data": {
"tokens": {
"accessToken": "eyJhbGci...",
"refreshToken": "a1b2c3d4..."
}
}
}
Login Response (TOTP enabled)
{
"success": true,
"data": {
"requiresTOTP": true,
"tempToken": "tmp_a1b2c3..."
}
}
Market Data
| Method | Endpoint | Auth | Cache | Description |
|---|---|---|---|---|
| GET | /api/market/pairs | Public | 1 hour | All trading pair configs |
| GET | /api/market/ticker/:symbol | Public | 5 sec | 24hr ticker data |
| GET | /api/market/depth/:symbol | Public | 2 sec | Order book (limit: 5-1000) |
| GET | /api/market/trades/:symbol | Public | 2 sec | Recent trades (limit: 1-1000) |
| GET | /api/market/klines/:symbol | Public | 10 sec | Candlestick data |
Klines Query Parameters
GET /api/market/klines/BTCUSDC?interval=1h&limit=500
// Intervals: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
Trading
| Method | Endpoint | Auth | Rate Limit | Description |
|---|---|---|---|---|
| POST | /api/trading/orders | Auth API Key | 30/min | Place order on Binance |
| GET | /api/trading/orders | Auth | — | List user's orders (paginated) |
| DELETE | /api/trading/orders/:id | Auth API Key | 30/min | Cancel order |
| GET | /api/trading/history | Auth | — | Trade fill history |
| GET | /api/trading/balances | Auth API Key | — | Account balances from Binance |
| GET | /api/trading/history/export | Auth | — | CSV download of trade history |
Place Order Request
POST /api/trading/orders
Authorization: Bearer <accessToken>
{
"symbol": "BTCUSDC",
"side": "BUY",
"type": "LIMIT",
"price": "45000.00",
"quantity": "0.001",
"timeInForce": "GTC"
}
User Management
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/user/profile | Auth | Get user profile |
| PATCH | /api/user/profile | Auth | Update email or password |
| GET | /api/user/api-keys | Auth | List API keys (masked) |
| POST | /api/user/api-keys | Auth | Add Binance API key (max 5) |
| DELETE | /api/user/api-keys/:id | Auth | Delete API key |
| POST | /api/user/api-keys/:id/test | Auth | Test Binance connectivity |
Analytics
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/analytics/dashboard | Auth | Summary: volume, trades, fees, P&L |
| GET | /api/analytics/volume | Auth | Daily volume time series |
| GET | /api/analytics/pnl | Auth | Realized P&L by symbol |
Admin
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/admin/fees | Admin | Global fee configuration |
| PATCH | /api/admin/fees | Admin | Update global fees |
| GET | /api/admin/users | Admin | Paginated user list |
| GET | /api/admin/users/:id | Admin | User details |
| PATCH | /api/admin/users/:id/fees | Admin | Set per-user fee override |
| PATCH | /api/admin/api-keys/:id/deactivate | Admin | Deactivate user's API key |
| DELETE | /api/admin/api-keys/:id | Admin | Delete user's API key |
| GET | /api/admin/orders | Admin | All platform orders |
| GET | /api/admin/stats | Admin | Platform-wide statistics |
Health Checks
| Method | Endpoint | Purpose | Returns 503 If |
|---|---|---|---|
| GET | /api/health/live | Liveness probe (is the process running?) | Never — always 200 |
| GET | /api/health/ready | Readiness probe (are dependencies up?) | Database or Redis unavailable |
| GET | /api/health | Legacy health check | Database or Redis unavailable |
Readiness Response
{
"success": true,
"data": {
"status": "ready",
"checks": {
"database": "ok",
"redis": "ok"
},
"timestamp": "2026-02-07T12:00:00.000Z"
}
}
Frontend Application
Pages & Routes
| Route | Access | Description |
|---|---|---|
| / | Public | Landing page |
| /trade/[pair] | Public | Trading interface (e.g., /trade/BTCUSDC) |
| /login | Public | Sign in |
| /register | Public | Create account |
| /verify-email | Public | Email verification callback |
| /forgot-password | Public | Password recovery |
| /reset-password | Public | Password reset form |
| /account | Auth | User profile settings |
| /account/api-keys | Auth | Binance API key management |
| /account/security | Auth | 2FA TOTP settings |
| /analytics | Auth | Trading analytics dashboard |
| /admin | Admin | Admin dashboard |
| /admin/users | Admin | User management |
| /admin/fees | Admin | Fee configuration |
| /admin/orders | Admin | Platform-wide orders |
State Management (Zustand)
useAuthStore
Manages authentication state. Stores tokens in localStorage, auto-refreshes on 401 responses.
user— Current user profile (null if logged out)login(email, password)— Returns tokens or TOTP challengeregister(email, password)— Creates accountlogout()— Clears tokens and state
useOrderStore
Dual-mode store: demo (localStorage simulation) when unauthenticated, API-backed when authenticated.
isDemoMode— Toggleable; persisted in localStorageplaceOrder()— Demo: simulates locally; Live:POST /api/trading/orderscancelOrder()— Demo: removes locally; Live:DELETE /api/trading/orders/:id- Form state:
formPrice,formQuantity,formSide,formType, etc.
useMarketStore
Current trading pair, ticker data, and pair list loaded from the API.
Design System
Color Palette
#0D0D0D#141414#1A1A1A#2A2A2A#218CFF#18E589#E5484DTypography
| Usage | Font | Weights |
|---|---|---|
| UI text, labels, buttons | DM Sans | 400, 500, 600, 700 |
| Prices, data, code | JetBrains Mono | 400, 500 |
Component Patterns
- Border radius: 8px (cards, inputs, buttons)
- Borders: 1px solid
#2A2A2A(no shadows) - Transitions: 150ms color/background transitions
- Buy buttons: Green background (
#18E589), black text - Sell buttons: Red background (
#E5484D), white text - Primary action: Blue background (
#218CFF), white text - Scrollbars: 4px thin,
#2A2A2Athumb
Security
Security Headers
| Header | Value |
|---|---|
| X-Content-Type-Options | nosniff |
| X-Frame-Options | SAMEORIGIN |
| Referrer-Policy | strict-origin-when-cross-origin |
| Strict-Transport-Security | max-age=31536000; includeSubDomains; preload (production only) |
| Permissions-Policy | camera=(), microphone=(), geolocation=(), payment=() |
| Content-Security-Policy | default-src 'self'; connect-src 'self' ws://... wss://... |
| X-Request-Id | UUID per request for traceability |
| X-Powered-By | Stripped |
Encryption Details
| What | Algorithm | Key Size | Purpose |
|---|---|---|---|
| Passwords | Argon2id | 64KB mem, 3 iterations | Irreversible hash |
| API Keys | AES-256-GCM | 256-bit | Reversible encryption |
| TOTP Secrets | AES-256-GCM | 256-bit | Reversible encryption |
| Refresh Tokens | SHA-256 | — | Stored as hash in Redis |
Input Validation
All endpoints validate input with Zod schemas. Invalid input returns 400 with detailed field-level errors:
{
"success": false,
"error": "Validation failed",
"details": [
"password: Password must contain a special character",
"email: Invalid email"
]
}
Additional Protections
- Request body limit: 1MB max
- CORS: Locked to
FRONTEND_URLin production - Trust proxy: Enabled in production for correct client IP behind nginx
- CSV injection: Values starting with
=,+,-,@are prefixed with' - Error masking: 5xx errors return "Internal server error" in production (no stack traces)
- Email enumeration:
/forgot-passwordalways returns success (doesn't reveal if email exists)
Rate Limiting
| Route | Limit | Window | Key |
|---|---|---|---|
| /auth/register | 5 | 1 hour | IP |
| /auth/login | 10 | 15 min | IP |
| /auth/forgot-password | 3 | 15 min | IP |
| /market/* | 120 | 1 min | IP |
| /trading/* | 30 | 1 min | userId |
| Default | 100 | 1 min | userId or IP |
Rate limiting is backed by Redis for distributed consistency across cluster nodes.
Deployment
Production Checklist
- ☐ Copy
.env.production.exampleto.envand fill ALL values - ☐ Generate unique
JWT_ACCESS_SECRET,JWT_REFRESH_SECRET,ENCRYPTION_KEY - ☐ Set strong
DB_PASSWORDandREDIS_PASSWORD - ☐ Set
FRONTEND_URLto production domain (e.g.,https://trade.example.com) - ☐ Configure SMTP credentials for email delivery
- ☐ Place TLS certificates in
nginx/ssl/(fullchain.pem + privkey.pem) - ☐ Set
ADMIN_PASSWORDto a strong password - ☐ Run
docker compose up -d - ☐ Verify:
curl https://yourdomain.com/api/health/ready - ☐ Set up database backup cron job
- ☐ Configure monitoring and alerting
Deploy with Docker Compose
# Create .env from template
cp server/.env.production.example .env
# Generate secrets
echo "JWT_ACCESS_SECRET=$(openssl rand -base64 48)" >> .env
echo "JWT_REFRESH_SECRET=$(openssl rand -base64 48)" >> .env
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env
# Start all services
docker compose up -d
# Run database migration + seed
docker compose exec api npx prisma migrate deploy
docker compose exec api npx prisma db seed
# Check logs
docker compose logs -f api
Deploy with PM2 (without Docker)
# Build backend
cd server && pnpm build
# Start with PM2 (cluster mode)
pm2 start ecosystem.config.cjs --env production
# Monitor
pm2 monit
# View logs
pm2 logs crypto-exchange-api
Docker
Architecture
Image Details
| Image | Base | Features |
|---|---|---|
| API Server | node:22-alpine | Multi-stage build, non-root user (appuser:1001), dumb-init for PID 1, healthcheck |
| Frontend | node:22-alpine | Standalone output mode, non-root user, dumb-init, healthcheck |
| Database | postgres:17-alpine | Persistent volume, health probe via pg_isready |
| Cache | redis:7-alpine | Password auth, 256MB max memory, LRU eviction |
| Proxy | nginx:1.27-alpine | TLS termination, rate limiting, WebSocket upgrade |
Health Check Dependencies
Services start in order via health checks: postgres → redis → api → frontend → nginx
Nginx Configuration
Routing Rules
| Location | Backend | Rate Limit | Notes |
|---|---|---|---|
| /api/auth/* | api:3007 | 10 req/min | Strict auth rate limiting |
| /api/* | api:3007 | 100 req/min | General API rate limiting |
| /ws | api:3007 | None | WebSocket upgrade, 24h timeout |
| /_next/static/* | frontend:3008 | None | 1-year cache, immutable |
| /* | frontend:3008 | None | Next.js pages |
TLS Configuration
- Protocols: TLS 1.2 + TLS 1.3
- Ciphers: ECDHE-ECDSA and ECDHE-RSA with AES-128/256-GCM-SHA256/384
- OCSP Stapling: Enabled
- Session Tickets: Disabled (forward secrecy)
- HSTS: 1 year, includeSubDomains, preload
SSL Certificate Setup
# Using Let's Encrypt with certbot
certbot certonly --standalone -d yourdomain.com
# Copy to nginx/ssl/
cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem nginx/ssl/
cp /etc/letsencrypt/live/yourdomain.com/privkey.pem nginx/ssl/
# For local development (self-signed)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout nginx/ssl/privkey.pem \
-out nginx/ssl/fullchain.pem \
-subj "/CN=localhost"
CI / CD
GitHub Actions pipeline runs on every push to main and on pull requests.
Pipeline Stages
prisma generate → tsc (type check) → vitest run (354 tests).next build (type check + compile) → vitest run (72 tests).main push, after tests pass. Builds both API and frontend Docker images tagged with commit SHA.Concurrency
Uses GitHub Actions concurrency groups — new pushes cancel in-progress runs for the same branch.
Backups
Database Backup
# Manual backup
./server/scripts/backup.sh
# Automated via cron (daily at 2 AM)
0 2 * * * /path/to/server/scripts/backup.sh >> /var/log/backup.log 2>&1
Creates compressed pg_dump files in /app/backups/. Old backups are automatically rotated after 30 days.
Database Restore
# List available backups
./server/scripts/restore.sh
# Restore specific backup
./server/scripts/restore.sh /app/backups/crypto_exchange_20260207_020000.sql.gz
Monitoring
Health Endpoints
Use for uptime monitoring and container orchestration:
| Probe | Endpoint | Use For |
|---|---|---|
| Liveness | /api/health/live | Kubernetes liveness probe, uptime checks |
| Readiness | /api/health/ready | Kubernetes readiness probe, load balancer health |
Logging
Structured JSON logging in production via Pino. Pretty-printed in development.
# View API logs (Docker)
docker compose logs -f api
# View nginx access logs
tail -f nginx/logs/access.log
# PM2 logs
pm2 logs crypto-exchange-api --lines 100
Key Metrics to Monitor
- Health endpoint response time — Alert if >5s
- Database connections — Prisma connection pool
- Redis memory usage — Should stay below 256MB limit
- Error rate — 5xx responses in nginx access logs
- WebSocket connections — Active connection count
- Cron job execution — Order-sync and market-refresh logs
Cron Jobs
| Job | Interval | Purpose |
|---|---|---|
| market-refresh | Every 30s | Warm Redis cache with ticker data |
| order-sync | Every 10s | Sync order status from Binance, record fills |
| analytics-aggregation | Every 1h | Aggregate daily volume/PnL snapshots |
| session-cleanup | Every 6h | Remove expired tokens from Redis |
Environment Variables
Backend (server/.env)
| Variable | Default | Description |
|---|---|---|
| PORT | 3007 | API server port |
| NODE_ENV | development | development | production | test |
| DATABASE_URL | — | PostgreSQL connection string |
| REDIS_URL | redis://localhost:6379 | Redis connection string |
| JWT_ACCESS_SECRET | — | JWT signing secret (min 32 chars). Generate: openssl rand -base64 48 |
| JWT_REFRESH_SECRET | — | Refresh token secret (min 32 chars). Generate: openssl rand -base64 48 |
| ENCRYPTION_KEY | — | AES-256-GCM key (64 hex chars). Generate: openssl rand -hex 32 |
| SMTP_HOST | smtp.gmail.com | SMTP server hostname |
| SMTP_PORT | 587 | SMTP server port |
| SMTP_USER | — | SMTP username |
| SMTP_PASS | — | SMTP password / app-specific password |
| SMTP_FROM | noreply@cryptoexchange.com | Sender email address |
| FRONTEND_URL | http://localhost:3008 | Frontend URL for email links and CORS |
| ADMIN_EMAIL | admin@cryptoexchange.com | Initial admin account email |
| ADMIN_PASSWORD | — | Initial admin password (min 8 chars, complexity required) |
Frontend (.env.local)
| Variable | Default | Description |
|---|---|---|
| NEXT_PUBLIC_API_URL | http://localhost:3007 | Backend API base URL |
Docker Compose (.env at root)
| Variable | Description |
|---|---|
| DB_PASSWORD | PostgreSQL password (must match DATABASE_URL) |
| REDIS_PASSWORD | Redis password (must match REDIS_URL) |
| NEXT_PUBLIC_API_URL | Public API URL for frontend |
Troubleshooting
Common Issues
Health check returns 503
Database or Redis is unreachable. Check connection strings in .env and verify services are running:
docker compose ps
docker compose logs postgres
docker compose logs redis
"Invalid email or password" for admin
The database may have an old password hash. Re-run the seed to update:
cd server && pnpm db:seed
WebSocket connection refused
Check that:
- JWT token is valid and not expired
- CSP
connectSrcincludes the WebSocket origin - Nginx is configured for WebSocket upgrade (
Connection: upgrade)
Binance API key test fails
Ensure your Binance API key has "Enable Reading" permission enabled. For trading, enable "Enable Spot & Margin Trading". Check IP restrictions on the Binance side.
Encrypted data unreadable after deployment
The ENCRYPTION_KEY must be identical across deployments. If it changes, all AES-encrypted data (API keys, TOTP secrets) becomes permanently unrecoverable. Always back up this key.
Rate limit hit (429 Too Many Requests)
Wait for the rate limit window to reset. Rate limits are per-IP for public routes and per-user for authenticated routes. Check Retry-After header in the response.
Powered by Blockshark
Found a bug? Contact our support team at blockshark.com