v1.0.0 Quick Start API Deploy

NexTrade Documentation

Full-stack cryptocurrency exchange platform with real-time trading, WebSocket streaming, multi-factor authentication, and admin analytics.

40+
API Endpoints
426
Tests Passing
7
Order Types
AES-256
Encryption

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

Tech Stack

LayerTechnologyVersion
FrontendNext.js + React + TypeScript16.1 / 19.2 / 5.9
StylingTailwind CSS4.x
StateZustand5.x
ChartsLightweight Charts4.x
BackendFastify + TypeScript5.x / 5.6
ORMPrisma6.x
DatabasePostgreSQL17
CacheRedis7
RuntimeNode.js22 LTS

Architecture

┌──────────────────────────────────┐ Binance API REST + WebSocket Streams └──────────┬──────────┬──────────────┘ │ │ REST calls WS streams │ │ ┌────────────────┐ ┌──────────────┴──────────┴───────────────┐ NexTrade API Server Next.js │◄──►│ Fastify 5 — Port 3007 Frontend Port 3008 Routes ─ Services ─ Middleware WebSocket ─ Cron ─ Plugins └────────────────┘ └────────┬──────────────────┬─────────────┘ │ │ ┌────────┴────────┐ ┌──────┴──────┐ PostgreSQL 17 │ │ Redis 7 Users, Orders │ │ Sessions Trades, Keys │ │ Cache Analytics │ │ Rate Lim └─────────────────┘ └─────────────┘

Request Flow

Client Request
Browser sends HTTPS request to nginx reverse proxy
Nginx Routing
TLS termination, rate limiting, forwards to API (/api/*) or frontend (/*)
Fastify Middleware
JWT authentication, API key verification, Zod input validation
Service Layer
Business logic: fee calculation, Binance API calls, database operations
Response
JSON response with X-Request-Id header for traceability

Backend 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

Quick Start

Clone & install dependencies
git clone <repo-url> nextrade
cd nextrade

# Frontend
pnpm install

# Backend
cd server
pnpm install
Configure environment
cp .env.example .env
# Edit .env — set DATABASE_URL, secrets, etc.
Set up database
# Generate Prisma client + push schema
npx prisma generate
npx prisma db push

# Seed admin account
pnpm db:seed
Start development servers
# Terminal 1 — Backend (port 3007)
cd server && pnpm dev

# Terminal 2 — Frontend (port 3008)
cd .. && pnpm dev
Verify
curl http://localhost:3007/api/health
# {"success":true,"data":{"status":"ok","checks":{"database":"ok","redis":"ok"}}}
The frontend at 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
The 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

Default Admin Account

Created on first seed via pnpm db:seed:

FieldValue
Emailadmin@cryptoexchange.com
PasswordAdmin@123456
RoleADMIN
Change the admin password immediately after deployment. Set 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

ColumnTypeDescription
idString (PK)CUID identifier
emailString (unique)User email address
passwordHashStringArgon2id hashed password
roleEnumUSER | ADMIN
emailVerifiedBooleanEmail verification status
totpSecretString?AES-256-GCM encrypted TOTP secret
totpEnabledBooleanWhether 2FA is enabled
createdAtDateTimeAccount creation time

ApiKey

Binance API credentials encrypted with AES-256-GCM. Each key and secret has its own IV and auth tag.

ColumnTypeDescription
idString (PK)CUID identifier
userIdString (FK)Owner user
labelStringDisplay label (e.g., "Main Key")
keyHintStringMasked: first 6 + last 4 chars
encryptedKeyStringAES-encrypted API key
iv / authTagStringEncryption parameters for key
encryptedSecretStringAES-encrypted API secret
ivSecret / authTagSecretStringEncryption parameters for secret
isActiveBooleanActive flag (admin can deactivate)

Order

ColumnTypeDescription
idString (PK)CUID identifier
userIdString (FK)Owner user
binanceOrderIdString?Binance internal order ID
symbolStringTrading pair (e.g., BTCUSDC)
sideEnumBUY | SELL
typeEnumLIMIT, MARKET, STOP_LIMIT, STOP_LOSS, TAKE_PROFIT, TAKE_PROFIT_LIMIT, OCO
statusEnumNEW, PARTIALLY_FILLED, FILLED, CANCELED, REJECTED, EXPIRED
priceDecimal(20,8)?Limit price
stopPriceDecimal(20,8)?Stop trigger price
quantityDecimal(20,8)Order quantity
filledQtyDecimal(20,8)Cumulative filled quantity
timeInForceStringGTC, IOC, or FOK

Trade

Each fill on an order creates a Trade record with fee details.

ColumnTypeDescription
priceDecimal(20,8)Execution price
quantityDecimal(20,8)Executed quantity
feeDecimal(20,8)Trading fee amount
feeRateDecimal(10,6)Fee rate (0.001 = 0.1%)
feeAssetStringAsset fee was paid in
isMakerBooleanWhether 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

TokenExpiryStoragePurpose
Access Token15 minlocalStorage + Authorization headerAPI authentication
Refresh Token7 daysRedis (SHA-256 hash)Token rotation
TOTP Temp Token5 minRedis2FA intermediate state
Email Verify Token24 hoursRedisEmail verification link
Password Reset Token1 hourRedisPassword reset link

Login Flow

Client API Server Redis POST /login ──────────────► Check lockout ──────────────► GET lockout:email {email, password} Verify password (argon2) Check emailVerified If TOTP disabled: ◄── {accessToken, Issue JWT + refresh token ──► SET refresh:hash refreshToken} If TOTP enabled: ◄── {requiresTOTP, Store temp token ───────────► SET totp:token tempToken} (5 min TTL) POST /totp/verify ──────────► Validate code against ─────► GET totp:token {tempToken, code} encrypted secret DEL totp:token ◄── {accessToken, Issue full tokens ──────────► SET refresh:hash refreshToken}

Account Lockout

2FA (TOTP)

Setup
POST /api/auth/totp/setup — generates secret, returns QR code data URL. Secret held in Redis for 10 min.
Enable
POST /api/auth/totp/enable with verification code — encrypts secret with AES-256-GCM, stores in DB.
Verify on Login
Login returns tempToken. Client calls POST /api/auth/totp/verify with code to get full JWT.
Disable
POST /api/auth/totp/disable requires current password + valid TOTP code.

Trading Engine

Supported Order Types

TypeRequired FieldsFee TypeDescription
LIMITprice, quantity, timeInForceMakerExecute at specific price or better
MARKETquantityTakerExecute immediately at best price
STOP_LIMITprice, stopPrice, quantityTakerLimit order triggered at stop price
STOP_LOSSstopPrice, quantityTakerMarket sell triggered at stop price
TAKE_PROFITstopPrice, quantityTakerMarket sell triggered at profit target
TAKE_PROFIT_LIMITprice, stopPrice, quantityMakerLimit order triggered at profit target
OCOprice, stopPrice, stopLimitPrice, quantityMixedOne-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:

Market Data

All market data is proxied through the API server with Redis caching. The frontend never talks directly to Binance.

Cache Strategy

EndpointCache TTLWarm Strategy
Trading pairs1 hourOn first request
24hr ticker5 secondsEvery 30s via cron
Order book depth2 secondsOn demand
Recent trades2 secondsOn demand
Klines / candles10 secondsOn demand
User balances10 secondsOn 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

ActionClient → ServerServer → 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

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.

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

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

MethodEndpointAuthRate LimitDescription
POST/api/auth/registerPublic5/hourCreate account, send verification email
POST/api/auth/loginPublic10/15minReturns tokens or TOTP challenge
POST/api/auth/refreshPublic30/15minRotate access + refresh tokens
POST/api/auth/logoutAuthRevoke refresh token
GET/api/auth/verify-emailPublicVerify email via token
POST/api/auth/forgot-passwordPublic3/15minSend password reset email
POST/api/auth/reset-passwordPublic5/15minReset password with token
POST/api/auth/totp/setupAuthGenerate TOTP secret + QR
POST/api/auth/totp/enableAuthEnable 2FA after code verification
POST/api/auth/totp/disableAuthDisable 2FA (password + code required)
POST/api/auth/totp/verifyPublic10/15minComplete 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

MethodEndpointAuthCacheDescription
GET/api/market/pairsPublic1 hourAll trading pair configs
GET/api/market/ticker/:symbolPublic5 sec24hr ticker data
GET/api/market/depth/:symbolPublic2 secOrder book (limit: 5-1000)
GET/api/market/trades/:symbolPublic2 secRecent trades (limit: 1-1000)
GET/api/market/klines/:symbolPublic10 secCandlestick 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

MethodEndpointAuthRate LimitDescription
POST/api/trading/ordersAuth API Key30/minPlace order on Binance
GET/api/trading/ordersAuthList user's orders (paginated)
DELETE/api/trading/orders/:idAuth API Key30/minCancel order
GET/api/trading/historyAuthTrade fill history
GET/api/trading/balancesAuth API KeyAccount balances from Binance
GET/api/trading/history/exportAuthCSV 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

MethodEndpointAuthDescription
GET/api/user/profileAuthGet user profile
PATCH/api/user/profileAuthUpdate email or password
GET/api/user/api-keysAuthList API keys (masked)
POST/api/user/api-keysAuthAdd Binance API key (max 5)
DELETE/api/user/api-keys/:idAuthDelete API key
POST/api/user/api-keys/:id/testAuthTest Binance connectivity

Analytics

MethodEndpointAuthDescription
GET/api/analytics/dashboardAuthSummary: volume, trades, fees, P&L
GET/api/analytics/volumeAuthDaily volume time series
GET/api/analytics/pnlAuthRealized P&L by symbol

Admin

MethodEndpointAuthDescription
GET/api/admin/feesAdminGlobal fee configuration
PATCH/api/admin/feesAdminUpdate global fees
GET/api/admin/usersAdminPaginated user list
GET/api/admin/users/:idAdminUser details
PATCH/api/admin/users/:id/feesAdminSet per-user fee override
PATCH/api/admin/api-keys/:id/deactivateAdminDeactivate user's API key
DELETE/api/admin/api-keys/:idAdminDelete user's API key
GET/api/admin/ordersAdminAll platform orders
GET/api/admin/statsAdminPlatform-wide statistics

Health Checks

MethodEndpointPurposeReturns 503 If
GET/api/health/liveLiveness probe (is the process running?)Never — always 200
GET/api/health/readyReadiness probe (are dependencies up?)Database or Redis unavailable
GET/api/healthLegacy health checkDatabase 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

RouteAccessDescription
/PublicLanding page
/trade/[pair]PublicTrading interface (e.g., /trade/BTCUSDC)
/loginPublicSign in
/registerPublicCreate account
/verify-emailPublicEmail verification callback
/forgot-passwordPublicPassword recovery
/reset-passwordPublicPassword reset form
/accountAuthUser profile settings
/account/api-keysAuthBinance API key management
/account/securityAuth2FA TOTP settings
/analyticsAuthTrading analytics dashboard
/adminAdminAdmin dashboard
/admin/usersAdminUser management
/admin/feesAdminFee configuration
/admin/ordersAdminPlatform-wide orders

State Management (Zustand)

useAuthStore

Manages authentication state. Stores tokens in localStorage, auto-refreshes on 401 responses.

useOrderStore

Dual-mode store: demo (localStorage simulation) when unauthenticated, API-backed when authenticated.

useMarketStore

Current trading pair, ticker data, and pair list loaded from the API.

Design System

Color Palette

Page BG
#0D0D0D
Panel BG
#141414
Input BG
#1A1A1A
Border
#2A2A2A
Blue
#218CFF
Green
#18E589
Red
#E5484D

Typography

UsageFontWeights
UI text, labels, buttonsDM Sans400, 500, 600, 700
Prices, data, codeJetBrains Mono400, 500

Component Patterns

Security

Security Headers

HeaderValue
X-Content-Type-Optionsnosniff
X-Frame-OptionsSAMEORIGIN
Referrer-Policystrict-origin-when-cross-origin
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preload (production only)
Permissions-Policycamera=(), microphone=(), geolocation=(), payment=()
Content-Security-Policydefault-src 'self'; connect-src 'self' ws://... wss://...
X-Request-IdUUID per request for traceability
X-Powered-ByStripped

Encryption Details

WhatAlgorithmKey SizePurpose
PasswordsArgon2id64KB mem, 3 iterationsIrreversible hash
API KeysAES-256-GCM256-bitReversible encryption
TOTP SecretsAES-256-GCM256-bitReversible encryption
Refresh TokensSHA-256Stored 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

Rate Limiting

RouteLimitWindowKey
/auth/register51 hourIP
/auth/login1015 minIP
/auth/forgot-password315 minIP
/market/*1201 minIP
/trading/*301 minuserId
Default1001 minuserId or IP

Rate limiting is backed by Redis for distributed consistency across cluster nodes.

Deployment

Production Checklist

  • ☐ Copy .env.production.example to .env and fill ALL values
  • ☐ Generate unique JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, ENCRYPTION_KEY
  • ☐ Set strong DB_PASSWORD and REDIS_PASSWORD
  • ☐ Set FRONTEND_URL to 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_PASSWORD to 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

┌──────────────────────────────────────────────────────┐ docker compose nginx:443 ──┬──► frontend:3008 (Next.js) ├──► api:3007 (Fastify) └──► api:3007/ws (WebSocket) api ────────┬──► postgres:5432 (persistent vol) └──► redis:6379 (password auth) └──────────────────────────────────────────────────────┘

Image Details

ImageBaseFeatures
API Servernode:22-alpineMulti-stage build, non-root user (appuser:1001), dumb-init for PID 1, healthcheck
Frontendnode:22-alpineStandalone output mode, non-root user, dumb-init, healthcheck
Databasepostgres:17-alpinePersistent volume, health probe via pg_isready
Cacheredis:7-alpinePassword auth, 256MB max memory, LRU eviction
Proxynginx:1.27-alpineTLS termination, rate limiting, WebSocket upgrade

Health Check Dependencies

Services start in order via health checks: postgresredisapifrontendnginx

Nginx Configuration

Routing Rules

LocationBackendRate LimitNotes
/api/auth/*api:300710 req/minStrict auth rate limiting
/api/*api:3007100 req/minGeneral API rate limiting
/wsapi:3007NoneWebSocket upgrade, 24h timeout
/_next/static/*frontend:3008None1-year cache, immutable
/*frontend:3008NoneNext.js pages

TLS Configuration

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

Backend Tests
Spins up PostgreSQL 17 + Redis 7 services. Runs: prisma generatetsc (type check) → vitest run (354 tests).
Frontend Tests
Runs: next build (type check + compile) → vitest run (72 tests).
Docker Build
Only on 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
Restore drops and recreates the database. All current data will be lost. The script requires interactive confirmation.

Monitoring

Health Endpoints

Use for uptime monitoring and container orchestration:

ProbeEndpointUse For
Liveness/api/health/liveKubernetes liveness probe, uptime checks
Readiness/api/health/readyKubernetes 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

Cron Jobs

JobIntervalPurpose
market-refreshEvery 30sWarm Redis cache with ticker data
order-syncEvery 10sSync order status from Binance, record fills
analytics-aggregationEvery 1hAggregate daily volume/PnL snapshots
session-cleanupEvery 6hRemove expired tokens from Redis

Environment Variables

Backend (server/.env)

VariableDefaultDescription
PORT3007API server port
NODE_ENVdevelopmentdevelopment | production | test
DATABASE_URLPostgreSQL connection string
REDIS_URLredis://localhost:6379Redis connection string
JWT_ACCESS_SECRETJWT signing secret (min 32 chars). Generate: openssl rand -base64 48
JWT_REFRESH_SECRETRefresh token secret (min 32 chars). Generate: openssl rand -base64 48
ENCRYPTION_KEYAES-256-GCM key (64 hex chars). Generate: openssl rand -hex 32
SMTP_HOSTsmtp.gmail.comSMTP server hostname
SMTP_PORT587SMTP server port
SMTP_USERSMTP username
SMTP_PASSSMTP password / app-specific password
SMTP_FROMnoreply@cryptoexchange.comSender email address
FRONTEND_URLhttp://localhost:3008Frontend URL for email links and CORS
ADMIN_EMAILadmin@cryptoexchange.comInitial admin account email
ADMIN_PASSWORDInitial admin password (min 8 chars, complexity required)

Frontend (.env.local)

VariableDefaultDescription
NEXT_PUBLIC_API_URLhttp://localhost:3007Backend API base URL

Docker Compose (.env at root)

VariableDescription
DB_PASSWORDPostgreSQL password (must match DATABASE_URL)
REDIS_PASSWORDRedis password (must match REDIS_URL)
NEXT_PUBLIC_API_URLPublic 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 connectSrc includes 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