Closed Beta — Demo trading only. No real funds. Help us find bugs!
    BETA
    FeaturesPricingAbout
    Sign InJoin Beta

    Developer Docs

    Getting StartedBot EnginesExchange IntegrationsArchitectureSecurityDatabaseDeploymentTesting

    User Guide

    Getting StartedTrading BotsAI BotDCA BotSignal BotArbitrage BotPump BotScalping BotGrid BotCombo BotTradingDeFiPortfolioMarketplaceSubscriptionsSettingsAdmin

    BlockbotX Security Architecture

    Overview

    BlockbotX implements a security-first architecture covering authentication, encryption, rate limiting, input validation, and monitoring. Every layer is designed with defense-in-depth: no single control is relied upon in isolation. Tokens are stored in httpOnly cookies, API keys are encrypted at rest with AES-256-GCM, rate limiting is backed by Redis, and a real-time security logger detects and auto-blocks anomalous activity.


    Authentication

    JWT Token System

    BlockbotX uses a dual-token JWT architecture:

    • Access tokens: 15-minute expiry (configurable via JWT_EXPIRES_IN env var). Carries userId, email, role, status, emailVerified, and type: "access".
    • Refresh tokens: 7-day expiry (configurable via REFRESH_TOKEN_EXPIRES_IN env var). Carries the same payload with type: "refresh". Stored as a Session record in the database.
    • Issuer/audience: Both tokens are signed with issuer: "blockbotx" and audience: "blockbotx-api", verified on every decode.
    • Storage: Tokens are stored in httpOnly cookies (not localStorage). The accessToken cookie uses sameSite: "lax" with a 15-minute maxAge. The refreshToken cookie uses sameSite: "strict" with a 7-day maxAge. Both are marked secure: true in production.
    • API clients: Also supports Authorization: Bearer <token> header as a fallback. The auth middleware checks the cookie first, then falls back to the header.

    Token payload interface:

    interface JwtPayload {
      userId: string;
      email: string;
      role?: string;
      status?: string;
      emailVerified?: boolean;
      type: "access" | "refresh";
    }
    

    Verification: verifyToken() validates signature, issuer, and audience. Returns null for expired or invalid tokens. Distinguishes between TokenExpiredError and JsonWebTokenError in logging.

    Source: lib/auth/jwt.ts


    Login Flow

    The login endpoint (POST /api/auth/login) executes the following steps in order:

    1. Zod schema validation -- Request body is validated against loginSchema. Invalid input returns 400 with the first error message.

    2. User lookup -- Email is normalized to lowercase. User record is fetched with all fields needed for authentication checks (password hash, 2FA settings, status, lock state, failed attempts).

    3. Account status check -- Checks for banned (403, permanent), disabled (403, permanent), or suspended (403, temporary). Expired suspensions are auto-cleared by resetting status to active.

    4. Lock check -- If lockedUntil is set and in the future, returns 429 with remaining minutes. Lockout is triggered after 5 failed login attempts and lasts 30 minutes.

    5. Password verification -- bcrypt comparison of submitted password against stored hash.

    6. Failed attempt increment -- On password failure, failedLoginAttempts is incremented. At 5 failures, lockedUntil is set to 30 minutes from now. A LoginHistory record is created with success: false.

    7. 2FA check -- If twoFactorEnabled is true and no totpToken is provided, returns { require2FA: true } with status 200. The client must re-submit with the TOTP code.

    8. TOTP validation -- If 2FA is enabled and a totpToken is provided, verifies against the stored secret. Invalid codes increment failed attempts and may trigger lockout.

    9. Token pair generation -- On successful authentication, generates both access and refresh tokens via generateTokenPair(). Failed attempt counter is reset to 0.

    10. Session creation -- A Session record is created in the database with the refresh token, 7-day expiry, IP address, and user agent.

    11. Login history and device fingerprint -- A successful LoginHistory record is created. A SHA-256 hash of the User-Agent string serves as a device fingerprint. If the fingerprint is not found in the KnownDevice table, a new device record is created and a new-device alert email is sent (fire-and-forget). Known devices have their lastUsedAt timestamp updated.

    12. Set cookies -- The response sets accessToken (httpOnly, sameSite lax, 15-minute maxAge) and refreshToken (httpOnly, sameSite strict, 7-day maxAge) cookies. The access token is also returned in the response body for API client convenience.

    Source: app/api/auth/login/route.ts


    Password Security

    • Hashing: bcrypt with 12 salt rounds (via bcrypt.hash()).
    • Validation requirements: Minimum 8 characters, at least one uppercase letter, at least one lowercase letter, at least one number, at least one special character.
    • Secure token generation: 32-byte crypto.randomBytes() for password reset tokens and email verification tokens, output as hex (64 characters).

    Source: lib/auth/password.ts


    Auth Middleware

    Two layers of auth middleware exist:

    lib/auth/middleware.ts -- Core token extraction and verification:

    • getAuthUser(req) extracts the token from the accessToken cookie or Authorization: Bearer header, verifies it, and ensures type === "access". Throws UnauthorizedError on failure.
    • getOptionalAuthUser(req) wraps getAuthUser() and returns null instead of throwing.
    • requireRole(userRole, requiredRole) enforces a role hierarchy: user (1) < admin (2) < superadmin (3).

    lib/security/auth-middleware.ts -- Route-level wrappers:

    • requireAuth(handler) wraps a route handler, calling getAuthUser() and attaching the user to the request via a WeakMap (avoids type-unsafe req.user assignment).
    • optionalAuth(handler) attempts authentication but does not reject unauthenticated requests.
    • requireRole(roles) returns a middleware that verifies the authenticated user has one of the specified roles.
    • composeMiddleware(...middlewares) composes multiple wrappers in left-to-right order using reduceRight.
    • getUserFromRequest(req) retrieves the user attached by requireAuth.

    Source: lib/auth/middleware.ts, lib/security/auth-middleware.ts


    Two-Factor Authentication (2FA)

    • Library: otpauth (TOTP implementation)
    • Algorithm: SHA1, 6 digits, 30-second period
    • Secret: 20-byte random secret, encoded as base32
    • Clock drift tolerance: Window of 1 (accepts tokens from 1 period before or after current, i.e., +/-30 seconds)
    • Setup flow: generateTOTPSecret(userEmail) creates a TOTP instance with issuer "BlockbotX" and returns the base32 secret and otpauth:// URI. generateTOTPQRCode(otpauthUrl) renders the URI as a PNG data URL via the qrcode library.
    • Verification: verifyTOTPToken(token, secret) validates a 6-digit code against the stored secret. Returns true if totp.validate() returns a non-null delta.
    • Backup codes: generateBackupCodes(count = 10) generates codes in XXXX-XXXX format using uppercase alphanumeric characters. Codes are bcrypt-hashed before storage and are one-time use.

    Source: lib/auth/totp.ts


    Encryption

    • Algorithm: AES-256-GCM (authenticated encryption with associated data)
    • Key: 32-character ENCRYPTION_KEY environment variable, validated on first use (lazy initialization allows next build to succeed without the key). Throws a fatal error if the key is missing or not exactly 32 characters.
    • IV: 12-byte (96-bit) random IV generated via crypto.randomBytes(12) for each encryption call, ensuring unique ciphertext even for identical plaintext.
    • Output format: iv:authTag:encryptedData (all hex-encoded, colon-separated)
    • Decryption: Parses the three-part format, reconstructs the decipher with the auth tag, and returns the plaintext. Invalid formats or tampered ciphertext cause decryption failure.
    • Used for: Exchange API keys (Binance, OKX), exchange secret keys, OKX passphrase. Keys are decrypted only at the point of use and never cached in memory.
    • Hash utility: hash(text) produces a SHA-256 hex digest. verifyHash(text, hashedText) uses crypto.timingSafeEqual() to prevent timing attacks during comparison.
    • Key generation: generateEncryptionKey(length = 32) produces a cryptographically secure random key using crypto.randomBytes().

    Source: lib/encryption/api-keys.ts, lib/encryption/index.ts


    Rate Limiting

    Rate limiting is Redis-backed using express-rate-limit with rate-limit-redis (RedisStore). Four tiers are defined:

    TierLimit (prod)WindowKeyApplied To
    API100 requests15 minIPAll API routes (default)
    Auth5 requests15 minIPLogin, register, forgot-password, reset-password, 2FA routes. skipSuccessfulRequests: true so only failures count.
    Strict3 requests1 hourIPPassword reset and other sensitive operations
    User1000 requests1 hourJWT userId (fallback to IP)Per authenticated user across all routes

    Key details:

    • In development/test, limits are relaxed (e.g., API: 10,000 instead of 100) to avoid blocking during development.
    • JWT user extraction reads from the Authorization header or accessToken cookie via extractUserIdFromJWT(), verifies the token, and extracts userId. Falls back to IP for anonymous users.
    • IP extraction normalizes IPv6 addresses via ipKeyGenerator() from express-rate-limit. Reads x-forwarded-for, x-real-ip, or req.ip.
    • Server-level rate limiting is applied in server.ts before every request via applyRateLimit().
    • Route-specific stricter limits can be applied via the withRateLimit(handler, limiter) wrapper for individual API routes.
    • getLimiterForPath(url) selects the appropriate limiter based on URL pattern (auth routes get authLimiter, all others get apiLimiter).
    • 429 responses return { error: "Too many requests. Please try again later." }.

    Source: lib/security/rate-limiter.ts


    CSRF Protection

    • Library: csrf package
    • Secret storage: Generated via tokens.secretSync() and stored in a csrf-secret httpOnly cookie with a 24-hour maxAge, sameSite: "strict", and secure: true in production.
    • Token generation: generateCsrfToken() reads the secret from the cookie (or generates a new one), then calls tokens.create(secret) to produce a per-request token.
    • Verification: The client sends the token in the x-csrf-token header. verifyCsrfToken(req) reads both the secret from the cookie and the token from the header, then calls tokens.verify(secret, token).
    • Safe methods skipped: GET, HEAD, OPTIONS are not subject to CSRF verification.
    • Route-level application: withCsrfProtection(handler) wraps individual route handlers. Returns 403 with { error: "Invalid or missing CSRF token" } on failure.

    Note: CSRF protection is applied at the route level (not in the proxy) to avoid Edge runtime compatibility issues with the csrf package.

    Source: lib/security/csrf.ts


    Content Security Policy (CSP)

    The CSP header is generated per-request in proxy.ts with a unique nonce derived from crypto.randomUUID():

    default-src 'self';
    script-src 'self' 'nonce-{nonce}' 'strict-dynamic' https://js.stripe.com;
    style-src 'self' 'nonce-{nonce}';
    img-src 'self' data: https: blob:;
    font-src 'self' data:;
    connect-src 'self' https://api.binance.com wss://stream.binance.com https://api.stripe.com https://*.ingest.sentry.io;
    frame-src 'self' https://js.stripe.com;
    media-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'self';
    upgrade-insecure-requests;
    
    • 'strict-dynamic' allows scripts loaded by trusted (nonced) scripts to execute without explicit whitelisting.
    • 'unsafe-eval' is added to script-src only in development mode (required by Next.js HMR and React DevTools).
    • 'unsafe-inline' is added to style-src only in development mode (production uses nonce).
    • Development mode also adds wss://localhost:3000 and ws://localhost:3000 to connect-src for WebSocket HMR.
    • The nonce is passed to pages via the x-nonce request header.

    Source: proxy.ts


    Security Headers

    Applied by proxy.ts on every response via addSecurityHeaders():

    HeaderValue
    Content-Security-PolicyPer-request nonce-based policy (see above)
    X-Frame-OptionsDENY
    X-Content-Type-Optionsnosniff
    Referrer-Policystrict-origin-when-cross-origin
    Permissions-Policycamera=(), microphone=(), geolocation=()
    Strict-Transport-Securitymax-age=31536000; includeSubDomains (production only)

    Source: proxy.ts


    Input Sanitization

    Server-side sanitization is powered by DOMPurify running on jsdom:

    FunctionPurpose
    sanitizeText(text)Strips all HTML tags, returns plain text only
    sanitizeHtml(dirty)Allows safe tags: b, i, em, strong, a, p, br, ul, ol, li, code, pre. Allowed attributes: href, title, target. Data attributes disabled.
    sanitizeSearchQuery(query)Removes non-word characters (except hyphens and spaces), limits to 100 characters. Adds defense-in-depth on top of Prisma's built-in SQL injection protection.
    sanitizeObject(obj)Recursively walks all properties and applies sanitizeText() to every string value. Handles nested objects and arrays.
    sanitizeEmail(email)Trims, lowercases, validates against email regex, then applies sanitizeText(). Returns null if invalid.
    sanitizeUrl(url)Parses with new URL(), allows only http:, https:, and mailto: protocols. Rejects javascript:, data:, and other dangerous schemes. Returns null if invalid.
    sanitizeFilename(filename)Removes path traversal characters (../), strips leading dots, allows only a-zA-Z0-9._-, limits to 255 characters.
    sanitizeNumber(value, min?, max?)Validates numeric input, checks isNaN/isFinite, enforces optional bounds. Returns null if invalid.
    sanitizeBoolean(value)Converts string/number/boolean inputs to strict boolean. Accepts "true", "1", "yes" as true and "false", "0", "no" as false. Returns null for unrecognized inputs.
    sanitizeJson(jsonString)Parses JSON, recursively sanitizes all string values via sanitizeObject(), re-serializes. Returns null if invalid JSON.

    Source: lib/security/sanitize.ts


    CORS Configuration

    • Allowed origins: Populated from NEXT_PUBLIC_APP_URL and CORS_ALLOWED_ORIGINS (comma-separated) environment variables.
    • Development: http://localhost:3000 and http://localhost:3001 are automatically added.
    • Credentials: Access-Control-Allow-Credentials: true is only set when the request origin matches the whitelist.
    • Allowed methods: GET, POST, PUT, DELETE, OPTIONS
    • Allowed headers: Content-Type, Authorization, x-csrf-token
    • Preflight cache: Access-Control-Max-Age: 86400 (24 hours)
    • OPTIONS preflight: Returns 204 with CORS headers.
    • Route-level application: withCors(handler) wraps route handlers, handles preflight automatically, and adds CORS headers to all responses.

    Source: lib/security/cors.ts


    Security Monitoring and Logging

    Security Logger

    Security events are logged to both Winston (for immediate visibility) and the SecurityLog database table (for long-term tracking and analysis).

    Event types:

    • auth_failure -- Failed login attempts, invalid tokens
    • rate_limit -- Rate limit violations
    • csrf_failure -- Invalid or missing CSRF tokens
    • suspicious_activity -- Anomalous patterns detected
    • unauthorized_access -- Attempts to access protected resources without authorization

    Event structure:

    interface SecurityEvent {
      type: 'auth_failure' | 'rate_limit' | 'csrf_failure' | 'suspicious_activity' | 'unauthorized_access';
      userId?: string;
      ip: string;
      userAgent: string;
      details: Record<string, unknown>;
    }
    

    Anomaly Detection

    checkForAnomalies() runs automatically after every logged security event. It counts recent events of the same type from the same IP within the last 15 minutes and compares against defined thresholds:

    Event TypeThreshold
    auth_failure5
    rate_limit3
    csrf_failure3
    suspicious_activity2
    unauthorized_access3

    When a threshold is breached:

    1. A high-severity security alert is created in the database.
    2. Admin users are notified via the notification service (sendSecurityAlertToAdmins()).

    Auto-Blocking

    When event count reaches 10x the threshold (e.g., 50 auth failures from one IP in 15 minutes):

    • The IP address is automatically blocked for 24 hours via the BlockedIP database table.
    • A critical-severity alert is created and admins are notified.
    • Duplicate blocks are prevented by checking for existing entries.

    Security Statistics

    getSecurityStats() provides dashboard metrics:

    • Last 24 hours: auth failures, rate limits, CSRF failures, total
    • Last 7 days: auth failures
    • All time: total events

    Log Retention

    cleanupOldSecurityLogs(days = 90) deletes logs older than the specified retention period. Security alerts (type: "security_alert") are preserved permanently.

    Source: lib/security/security-logger.ts


    Account Security

    • Account lockout: 5 failed login attempts triggers a 30-minute lock. The lockedUntil timestamp is checked on every login attempt. The counter resets on successful login.
    • Device fingerprinting: SHA-256 hash of the full User-Agent string. Stored in the KnownDevice table with @@unique([userId, deviceFingerprint]).
    • New device alerts: On login from an unrecognized device, an alert email is sent (fire-and-forget) with the device name (parsed from User-Agent), IP address, and login time.
    • Session management: Each login creates a Session record with the refresh token, expiry, IP, and user agent. Users can view and revoke active sessions.
    • Login history: Every login attempt (successful or failed) is recorded in the LoginHistory table with IP address, user agent, success status, and failure reason.
    • Session expiry: 7 days, consistent across all components (JWT refresh token, database session, refresh token cookie).
    • Account status enforcement: Banned/disabled accounts are rejected at login. Suspended accounts with expired suspendedUntil are automatically reactivated. The proxy also checks token status and clears cookies for banned/disabled users on page navigation.
    • Force password reset: If forcePasswordReset is true on the user record, the login response includes a flag so the client can redirect to the password reset flow.

    Proxy (Next.js 16 Convention)

    The proxy.ts file (Next.js 16 convention, replaces middleware.ts) runs on every request and provides:

    1. Maintenance mode -- Checks system status via an internal API call (cached for 30 seconds). Non-admin users are redirected to /maintenance. Essential API routes (/api/system/status, /api/auth/login, /api/health) are exempt.
    2. Authentication redirects -- Unauthenticated users accessing protected routes are redirected to /signin with a from query parameter for post-login redirect. Authenticated users accessing auth pages are redirected to /dashboard (or /verify-email if unverified).
    3. Email verification enforcement -- Unverified users are redirected to /verify-email for all protected routes.
    4. Admin route protection -- Defense-in-depth check: /admin routes verify the token has admin or superadmin role. Real enforcement is in API route handlers.
    5. Security headers -- Applied to every response (see Security Headers section above).
    6. CSP nonce generation -- A unique nonce is generated per request and passed to pages via the x-nonce header.

    Token verification in the proxy uses jose (jwtVerify) for Edge runtime compatibility, separate from the jsonwebtoken-based verifyToken() used in API routes.

    Source: proxy.ts


    Logger Redaction

    The Winston logger includes a custom redactFormat that automatically filters sensitive fields from all log metadata:

    Sensitive field patterns (case-insensitive, matches both camelCase and snake_case):

    • api_key, api_secret, secret_key
    • password, passwd
    • token, access_token, refresh_token
    • authorization, cookie
    • private_key, encryption_key
    • webhook_secret, jwt_secret
    • credentials

    Matching field values are replaced with [REDACTED]. The redaction is applied recursively to nested objects and arrays. Fields named level, message, timestamp, and stack are preserved.

    Production logging: Structured JSON output to logs/error.log (error level) and logs/combined.log (all levels), both with 5MB rotation and 5-file retention.

    Source: lib/logger.ts


    Environment Variables (Security-Related)

    VariablePurposeRequirements
    JWT_SECRETSigns all JWT tokensMust not be the default placeholder value in production
    JWT_EXPIRES_INAccess token lifetimeDefault: 15m
    REFRESH_TOKEN_EXPIRES_INRefresh token lifetimeDefault: 7d
    ENCRYPTION_KEYAES-256-GCM key for API key encryptionMust be exactly 32 characters
    NEXT_PUBLIC_APP_URLPrimary allowed CORS originRequired
    CORS_ALLOWED_ORIGINSAdditional allowed CORS originsComma-separated, optional
    STRIPE_WEBHOOK_SECRETStripe webhook HMAC-SHA256 verificationRequired for payment processing

    AI-powered crypto trading platform in closed beta. 8 bot engines, DeFi integration, and demo trading.

    Product

    • Features
    • Pricing
    • About
    • Join Beta

    Resources

    • Documentation
    • API Reference
    • Support
    • Blog
    • Status

    Legal

    • Terms of Service
    • Privacy Policy
    • Disclaimer

    © 2026 BlockbotX. All rights reserved.

    Built with AI · Powered by Next.js