Files
InvestPlay/docs/API.md
Lefteris Notas e75f913f1c
Some checks failed
CI / Lint (push) Has been cancelled
CI / TypeCheck (push) Has been cancelled
CI / Test (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/api/Dockerfile name:api]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/cms/Dockerfile name:cms]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/web/Dockerfile name:web]) (push) Has been cancelled
CI / Build (push) Has been cancelled
Deploy / Deploy (push) Has been cancelled
feat: complete Phase 1 foundation scaffold
- Monorepo: Turborepo + pnpm workspaces with 7 apps/packages
- Backend: NestJS scaffold with 9 modules (auth, tenant, curriculum, simulation, portfolio, ai-coach, analytics, classroom, gamification)
- Frontend: React 18 + Vite + TailwindCSS + shadcn/ui with 10 pages, 14 UI primitives, 3 Zustand stores
- Database: Prisma schema with 19 models, 13 enums, seed script, multi-tenant ready
- Docker: Dev, production, and Portainer Compose files with Traefik reverse proxy
- Configuration: .env.example (47 vars), Zod validation, frontend-safe env exposure
- i18n: English + Greek locale files (10 files), ICU MessageFormat, react-i18next
- Shared packages: @investplay/types, @investplay/ui, @investplay/i18n, @investplay/utils
- CI/CD: GitHub Actions (lint, typecheck, test, build, deploy, PR checks)
- Documentation: CONTEXT.md, ARCHITECTURE.md (10 Mermaid diagrams), API.md, LOCALIZATION.md, DEVELOPMENT-ROADMAP.md
- Infrastructure: Dockerfiles (dev + prod), nginx configs, backup/healthcheck scripts
- Security: JWT auth guards, role-based access, rate limiting, Helmet, CORS, PII sanitization
2026-06-12 19:32:57 +03:00

10 KiB

InvestPlay — API Documentation

Base URL Structure

Environment Base URL
Local development http://localhost:3001
Production https://api.investplay.app
Staging https://api.staging.investplay.app

Authentication

JWT Bearer Token

All authenticated requests require an Authorization header:

Authorization: Bearer <accessToken>

Access tokens are RS256-signed JWTs issued by the auth service. They are short-lived (15 minutes in production, 7 days in development).

Token Refresh

When the access token expires, use the refresh token to obtain a new pair:

POST /auth/refresh
Content-Type: application/json
Cookie: refreshToken=<httpOnly-cookie>

Response 200:
{
  "accessToken": "eyJhbG...",
  "expiresIn": 900
}

The refresh token is stored in an httpOnly, Secure, SameSite=Strict cookie set at login. If the refresh token is also expired, the user must re-authenticate.

Token Errors

Status Error Code Description
401 TOKEN_EXPIRED Access token has expired; refresh required
401 TOKEN_INVALID Token signature verification failed
401 TOKEN_MISSING No Authorization header provided
403 INSUFFICIENT_ROLE User lacks required role for this operation

GraphQL Endpoint

URL: POST /graphql

Content-Type: application/json

Example Query

query GetLessonProgress {
  me {
    id
    displayName
    xp
    level
    progress {
      completedLessons
      totalLessons
      currentStreak
    }
  }
  lesson(id: "lesson_abc123") {
    title
    estimatedMinutes
    blocks {
      ... on HeroBlock {
        headline
        subtitle
        imageUrl
      }
      ... on TextBlock {
        body
      }
      ... on QuizBlock {
        question
        options
        correctAnswer
      }
    }
  }
}

Example Mutation

mutation SubmitQuiz {
  submitQuiz(
    lessonId: "lesson_abc123",
    answers: [
      { questionId: "q1", selectedOption: 2 },
      { questionId: "q2", selectedOption: 0 }
    ]
  ) {
    ... on QuizResultSuccess {
      score
      totalQuestions
      correctAnswers
      xpEarned
      passed
    }
    ... on QuizResultError {
      code
      message
    }
  }
}

Response Format

{
  "data": { ... },
  "errors": [
    {
      "message": "Validation failed",
      "extensions": {
        "code": "VALIDATION_ERROR",
        "field": "answers",
        "details": "Each answer must include a valid questionId"
      }
    }
  ]
}

Error Codes

Extension Code HTTP Status Description
UNAUTHENTICATED 401 Not authenticated
FORBIDDEN 403 Insufficient permissions
VALIDATION_ERROR 400 Input validation failed
NOT_FOUND 404 Requested resource not found
RATE_LIMITED 429 Too many requests
AI_QUOTA_EXCEEDED 429 AI token budget exhausted for user/tenant
INTERNAL_ERROR 500 Unexpected server error

REST Endpoints

Health Check

GET /health

Response 200:
{
  "status": "ok",
  "uptime": 3600,
  "timestamp": "2026-06-12T16:00:00Z",
  "services": {
    "database": "healthy",
    "redis": "healthy",
    "storage": "healthy",
    "cms": "healthy"
  }
}

Used by Docker health checks and monitoring.

Auth Callback (Clerk)

POST /auth/callback
Content-Type: application/json

Request:
{
  "code": "oauth_code_from_clerk",
  "redirectUri": "https://app.investplay.app/auth/callback"
}

Response 200:
{
  "accessToken": "eyJhbG...",
  "refreshToken": "eyJhbG...",
  "user": {
    "id": "user_abc",
    "email": "student@example.com",
    "displayName": "Alex",
    "locale": "en",
    "onboardingComplete": false
  }
}

Auth Logout

POST /auth/logout

Response 200:
{
  "message": "Logged out successfully"
}

Clears refresh token cookie and invalidates the session.

Stripe Webhook

POST /webhooks/stripe
Content-Type: application/json
Stripe-Signature: <webhook signature>

Handles: checkout.session.completed, invoice.paid, customer.subscription.updated
Response 200: { "received": true }

Clerk Webhook

POST /webhooks/clerk
Content-Type: application/json
Svix-Signature: <webhook signature>

Handles: user.created, user.updated, user.deleted, session.created
Response 200: { "received": true }

Resend Webhook

POST /webhooks/resend
Content-Type: application/json

Handles: email.delivered, email.bounced, email.complained
Response 200: { "received": true }

WebSocket Endpoints

Simulation Tick Stream

WebSocket: wss://api.investplay.app/simulations
Auth: { token: "<accessToken>" }

Events:

Event Direction Payload Description
join Client -> Server { simulationId: string } Join a simulation room
leave Client -> Server { simulationId: string } Leave a simulation room
tick Server -> Client { timestamp, price, volume, indicator } Market price tick
status Server -> Client { status, speed, elapsed } Simulation status change
complete Server -> Client { finalPnl, totalTrades, roi } Simulation ended

AI Coach Stream

WebSocket: wss://api.investplay.app/ai-coach
Auth: { token: "<accessToken>" }

Events:

Event Direction Payload Description
message Client -> Server { content, contextId?, lessonId? } Send chat message
response Server -> Client { content, done, tokensUsed } Streaming AI response chunk
error Server -> Client { code, message } Error (quota exceeded, etc.)

Classroom Broadcast

WebSocket: wss://api.investplay.app/classroom
Auth: { token: "<accessToken>" }

Events:

Event Direction Payload Description
join Client -> Server { classroomId: string } Join classroom room
broadcast Teacher -> Server { type, payload } Teacher broadcasts event
event Server -> Student { type, payload, timestamp } Event delivered to students

Rate Limiting

Limits

Endpoint Window Limit Scope
/graphql 60 seconds 100 requests Per IP address
/auth/* 60 seconds 20 requests Per IP address
/webhooks/* 60 seconds 50 requests Per IP address
AI Chat 1 day 20 messages (default) Per user
AI Tokens 1 month 500,000 tokens (default) Per tenant

Rate Limit Headers

All responses include rate limit headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1718123456

When exceeded:

Status: 429 Too Many Requests
Retry-After: 45
X-RateLimit-Reset: 1718123456

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Please wait 45 seconds.",
    "retryAfter": 45
  }
}

AI-specific limits return:

Status: 429 Too Many Requests
X-AI-Quota-Type: daily  | monthly
X-AI-Quota-Reset: 1718123456

{
  "error": {
    "code": "AI_QUOTA_EXCEEDED",
    "message": "Daily AI message limit reached. Resets in 4 hours.",
    "quotaType": "daily",
    "resetsAt": "2026-06-13T00:00:00Z"
  }
}

Pagination

All list queries follow the Relay Connection specification:

type LessonConnection {
  edges: [LessonEdge!]!
  pageInfo: PageInfo!
}

type LessonEdge {
  node: Lesson!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

Pagination Parameters

Parameter Type Default Description
first Int 20 Number of items to fetch (max: 100)
after String null Cursor for forward pagination
last Int null Number of items backward (use with before)
before String null Cursor for backward pagination

Example with Pagination

query GetLessons {
  lessons(
    moduleId: "module_xyz",
    first: 10,
    after: "cursor_abc"
  ) {
    edges {
      cursor
      node {
        id
        title
        order
        estimatedMinutes
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Error Codes

Standard Error Format

{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description",
    "details": {},
    "requestId": "req_abc123"
  }
}

Global Error Codes

Code HTTP Status Description
VALIDATION_ERROR 400 Input validation failed
UNAUTHENTICATED 401 Authentication required
TOKEN_EXPIRED 401 Access token expired
TOKEN_INVALID 401 Invalid or malformed token
FORBIDDEN 403 Insufficient permissions
NOT_FOUND 404 Resource not found
CONFLICT 409 Resource conflict (duplicate)
RATE_LIMITED 429 Rate limit exceeded
AI_QUOTA_EXCEEDED 429 AI budget exhausted
TENANT_QUOTA_EXCEEDED 429 Tenant-level quota exceeded
INTERNAL_ERROR 500 Unexpected server error
SERVICE_UNAVAILABLE 503 Dependent service unavailable

Validation Error Details

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Input validation failed",
    "details": {
      "fields": {
        "email": "Invalid email format",
        "password": "Password must be at least 8 characters"
      }
    }
  }
}

Versioning Strategy

API Versioning

GraphQL does not use URL versioning. Instead:

  1. Schema evolution — Fields and types are added without removal. Deprecated fields are marked with @deprecated directive and maintained for a minimum of 90 days.
  2. Breaking changes — Announced via Changelog and GitHub Releases. A 30-day migration window is provided.
  3. No v1/v2 URLs — A single /graphql endpoint evolves over time. If a breaking change is unavoidable, a new endpoint /graphql/v2 may be introduced and maintained alongside the original for 6 months.

REST Versioning

REST endpoints (webhooks, health) are not versioned at the URL level. Breaking changes to webhook payloads would result in a new webhook event type (e.g., invoice.paid.v2) with the old event maintained for 90 days.