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

473 lines
10 KiB
Markdown

# 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
```graphql
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
```graphql
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
```json
{
"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:
```graphql
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
```graphql
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
```json
{
"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
```json
{
"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.
## Related Documentation
- [ARCHITECTURE.md](./ARCHITECTURE.md) — System architecture and multi-tenancy deep dive
- [CONTEXT.md](./CONTEXT.md) — Project context, tech stack, and development workflow
- [LOCALIZATION.md](./LOCALIZATION.md) — Locale support and i18n API details