feat: complete Phase 1 foundation scaffold
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

- 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
This commit is contained in:
Lefteris Notas
2026-06-12 19:32:57 +03:00
parent fa71c60cfc
commit e75f913f1c
226 changed files with 17547 additions and 0 deletions

638
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,638 @@
# InvestPlay — Architecture
## System Overview
```mermaid
graph TB
subgraph Clients
WEB["Web SPA (React + Vite)"]
MOBILE["Mobile (Capacitor)"]
DESKTOP["Desktop (Tauri)"]
end
subgraph Edge
CF["Cloudflare (DNS + CDN)"]
TRAEFIK["Traefik (Reverse Proxy + SSL)"]
end
subgraph Backend_Services
API["NestJS API Server"]
WS["Socket.io Gateway"]
WORKER["BullMQ Workers"]
GRAPHQL["Apollo GraphQL"]
end
subgraph Data_Layer
PG[("PostgreSQL 16<br/>Multi-schema")]
REDIS[("Redis 7<br/>Cache + Queue")]
S3[("S3 Storage<br/>MinIO / R2")]
end
subgraph External
CLERK["Clerk Auth"]
STRIPE["Stripe Connect"]
RESEND["Resend Email"]
LLM["OpenAI / Gemini"]
CMS_EXTERNAL["Payload CMS"]
end
WEB --> CF
MOBILE --> CF
DESKTOP --> CF
CF --> TRAEFIK
TRAEFIK --> API
TRAEFIK --> WS
API --> GRAPHQL
API --> WS
API --> WORKER
GRAPHQL --> PG
WS --> REDIS
WORKER --> REDIS
WORKER --> PG
API --> CLERK
API --> STRIPE
API --> RESEND
API --> LLM
API --> CMS_EXTERNAL
MOBILE -.-> CLERK
```
## Request Flow
### Typical API Request
```mermaid
sequenceDiagram
participant Client as Browser / Mobile
participant CF as Cloudflare
participant Traefik as Traefik Proxy
participant Nest as NestJS API
participant Guard as Auth Guard
participant Tenant as Tenant Interceptor
participant Resolver as GraphQL Resolver
participant Prisma as Prisma ORM
participant DB as PostgreSQL
Client->>CF: HTTPS Request
CF->>Traefik: Proxy Request
Traefik->>Nest: Reverse Proxy
Note over Nest: Middleware Pipeline
Nest->>Guard: JWT Validation
Guard->>Guard: Verify RS256 Signature
Guard->>Guard: Extract tenantId from JWT
Guard-->>Nest: User + Tenant Context
Nest->>Tenant: Set TenantContext
Tenant->>Tenant: AsyncLocalStorage.set(tenantId)
Tenant-->>Nest: Context Ready
Nest->>Resolver: Execute Query/Mutation
Resolver->>Prisma: Query with tenant scope
Prisma->>DB: SELECT * FROM "tenant_schema"."table"
DB-->>Prisma: Results
Prisma-->>Resolver: Data
Resolver-->>Nest: Response
Nest-->>Traefik: JSON Response
Traefik-->>CF: Proxy Response
CF-->>Client: Response
```
## Multi-tenancy Deep Dive
### Approach: Schema-level Isolation
Each tenant (institution) receives an isolated PostgreSQL schema. This provides stronger data separation than row-level security (RLS) while sharing the same database server for operational simplicity.
### Tenant Resolution Pipeline
1. **JWT issuance:** Auth service embeds `tenantId` claim in the JWT access token at login
2. **HTTP request arrives:** Traefik proxies to NestJS
3. **JwtAuthGuard:** Extracts `tenantId` from validated token payload
4. **TenantInterceptor:** Sets `TenantContext` in Node.js `AsyncLocalStorage`
5. **Prisma middleware:** Intercepts all queries and rewrites them to the correct schema:
```typescript
// Conceptual — Prisma middleware applies schema filter
prisma.$extends({
query: {
$allModels: {
async $allOperations({ model, operation, args, query }) {
const tenantId = TenantContext.get();
if (tenantId && !args.where) args.where = {};
args.where.tenantId = tenantId;
return query(args);
},
},
},
});
```
6. **Post-Schema routing:** Prisma connects to the tenant-specific schema via `schema` parameter in `DATABASE_URL`
### Tenant Provisioning
```mermaid
flowchart LR
A[New Institution Signs Up] --> B[Create Tenant Record]
B --> C[Provision PostgreSQL Schema]
C --> D[Run Migrations on Schema]
D --> E[Create Admin User]
E --> F[Configure Feature Flags]
F --> G[Tenant Ready]
```
### Shared vs Isolated Data
| Data | Location | Isolation |
|------|----------|-----------|
| User profiles, roles | Per-tenant schema | Full |
| Lesson progress, grades | Per-tenant schema | Full |
| Virtual portfolios, trades | Per-tenant schema | Full |
| AI interaction logs | Per-tenant schema | Full |
| Curriculum content | Shared (public schema) | Read-only |
| Platform configuration | Shared (public schema) | Read-only |
| Email templates | Shared (public schema) | Read-only |
| Badges, achievements | Shared (public schema) | Read-only |
## Authentication Flow
```mermaid
sequenceDiagram
participant User
participant Client
participant API
participant Clerk
participant DB
User->>Client: Click "Sign In"
Client->>Clerk: Redirect to Clerk Hosted UI
User->>Clerk: Enter credentials / Social Login
Clerk-->>Client: Authorization code
Client->>API: POST /auth/callback (code)
API->>Clerk: Verify code
Clerk-->>API: User info + external ID
API->>DB: Find or create user
API->>API: Generate JWT (RS256 signed)
Note over API: Payload: userId, tenantId, roles, schoolId
API-->>Client: { accessToken, refreshToken, user }
Client->>Client: Store tokens (memory + httpOnly cookie)
Note over Client,API: Subsequent requests
Client->>API: Authorization: Bearer <accessToken>
API->>API: Verify JWT signature
API->>API: Check expiry
API-->>Client: 200 OK | 401 Unauthorized
Note over Client,API: Token Refresh
Client->>API: POST /auth/refresh (refreshToken)
API->>API: Verify refresh token
API-->>Client: { accessToken, refreshToken }
```
### JWT Token Structure
```json
{
"sub": "user_2abc123",
"tenantId": "tenant_45def",
"roles": ["student", "premium"],
"schoolId": "school_78ghi",
"iat": 1718000000,
"exp": 1718000900
}
```
- **Algorithm:** RS256 (asymmetric) — private key signs, public key verifies
- **Access token TTL:** 15 minutes (production) / 7 days (development)
- **Refresh token TTL:** 7 days (production) / 30 days (development)
- **Storage:** Access token in memory (Zustand store), refresh token in httpOnly cookie
## GraphQL Schema Design Principles
### Structure
- **Single endpoint:** `POST /graphql` for all queries and mutations
- **Namespace by module:** `auth { ... }`, `curriculum { ... }`, `simulation { ... }`
- **Pagination:** Relay-style `Connection` pattern with `first`/`after` cursors
### Design Rules
1. Every query must be nullable-safe — return `null` or `[]` never throw
2. Mutations return a union type: `SuccessPayload | ErrorPayload`
3. Depth limited to 7 levels (enforced via `graphql-depth-limit`)
4. Query complexity analysis for cost-based rate limiting
### Example Schema Excerpt
```graphql
type Query {
me: User
lesson(id: ID!): Lesson
lessons(moduleId: ID!, first: Int, after: String): LessonConnection
portfolio(simulationId: ID!): Portfolio
leaderboard(classroomId: ID!, timeRange: TimeRange): [LeaderboardEntry!]
}
type Mutation {
startLesson(lessonId: ID!): LessonSessionPayload
submitQuiz(lessonId: ID!, answers: [AnswerInput!]!): QuizResultPayload
executeTrade(simulationId: ID!, input: TradeInput!): TradePayload
askAICoach(message: String!, contextId: ID): AICoachPayload
}
type User {
id: ID!
email: String!
displayName: String!
locale: String!
xp: Int!
level: Int!
streak: Int!
roles: [Role!]!
progress: UserProgress
}
```
## WebSocket Architecture
### Namespaces
| Namespace | Authentication | Purpose |
|-----------|---------------|---------|
| `/simulations` | JWT handshake | Stream historical price ticks |
| `/ai-coach` | JWT handshake | Stream AI chat responses |
| `/classroom` | JWT handshake + teacher role | Live classroom broadcasts |
### Connection Lifecycle
```mermaid
sequenceDiagram
participant Client
participant WS as Socket.io Gateway
participant Auth as AuthGuard
participant Redis as Redis Adapter
participant Room as Room Manager
Client->>WS: Connect (auth: { token })
WS->>Auth: Verify JWT
Auth-->>WS: { userId, tenantId, roles }
WS->>Redis: Register connection
WS-->>Client: Connected (socket.id)
Client->>WS: join(simulation:sim_123)
WS->>Room: Add to room
WS-->>Client: Joined room
Note over Client,Room: Tick streaming begins
Client->>WS: leave(simulation:sim_123)
WS->>Room: Remove from room
WS-->>Client: Left room
Client->>WS: Disconnect
WS->>Redis: Unregister connection
```
### Simulation Tick Streaming
1. Teacher or timer starts a simulation scenario
2. BullMQ worker loads historical CSV data for the scenario
3. Worker emits ticks to Redis pub/sub channel at configured speed (1x, 2x, 5x)
4. Socket.io gateway subscribes to Redis channel and broadcasts to room members
5. Each tick: `{ timestamp, price, volume, indicator }`
6. Frontend updates charts in real-time via Recharts
## Background Job Architecture
### BullMQ Queues
| Queue | Concurrency | Description |
|-------|-------------|-------------|
| `simulation-tick` | 5 | Generate and distribute simulation price ticks |
| `portfolio-valuation` | 3 | End-of-day portfolio P&L calculation |
| `email` | 10 | Send transactional emails via Resend |
| `analytics-rollup` | 2 | Nightly analytics materialized view refresh |
| `leaderboard` | 1 | Recalculate leaderboard rankings |
| `ai-audit-log` | 20 | Persist AI interaction logs asynchronously |
| `cleanup` | 1 | Soft-delete expired sessions, rotate logs |
### Queue Architecture
```mermaid
flowchart LR
subgraph Producers
API[NestJS API]
WORKER_SCHED["Scheduled Tasks<br/>@nestjs/schedule"]
end
subgraph Redis
QUEUES[(BullMQ Queues)]
end
subgraph Consumers
QW1[Worker: simulation-tick]
QW2[Worker: portfolio-valuation]
QW3[Worker: email]
QW4[Worker: analytics-rollup]
end
API --> QUEUES
WORKER_SCHED --> QUEUES
QUEUES --> QW1
QUEUES --> QW2
QUEUES --> QW3
QUEUES --> QW4
```
Each worker runs in the same NestJS process via `@nestjs/bullmq` or can be spun as a separate process for scale.
## Content Rendering Pipeline
```mermaid
flowchart LR
CMS[Payload CMS<br/>Author writes lesson]
API[NestJS API<br/>Serves content]
FRONT[React Frontend<br/>Renders blocks]
USER[User sees content]
CMS -->|REST API| API
API -->|GraphQL query| FRONT
FRONT -->|BlockRenderer maps types| USER
subgraph Block Types
HERO[Hero]
TEXT[Text]
QUIZ[Quiz]
CALC[Calculator]
BUDGET[BudgetGame]
TRADE[TradeSimulation]
end
FRONT --> HERO
FRONT --> TEXT
FRONT --> QUIZ
FRONT --> CALC
FRONT --> BUDGET
FRONT --> TRADE
```
### Block Renderer Component
```typescript
// Conceptual component structure
const BLOCK_COMPONENTS = {
hero: HeroBlock,
text: TextBlock,
quiz: QuizBlock,
calculator: CalculatorBlock,
budgetGame: BudgetGameBlock,
tradeSimulation: TradeSimulationBlock,
} as const;
function BlockRenderer({ blocks }: { blocks: ContentBlock[] }) {
return blocks.map((block) => {
const Component = BLOCK_COMPONENTS[block.type];
return <Component key={block.id} {...block.props} />;
});
}
```
## Database Schema Relationships
### Core Entities
```mermaid
erDiagram
Tenant ||--o{ User : "belongs to"
Tenant ||--o{ Classroom : "contains"
Tenant ||--o{ Module : "owns"
User ||--o{ UserProgress : "tracks"
User ||--o{ QuizAttempt : "takes"
User ||--o{ Trade : "executes"
User ||--o{ UserBadge : "earns"
User ||--o{ AILog : "generates"
User ||--o{ ClassroomStudent : "enrolled in"
Classroom ||--o{ ClassroomStudent : "has"
Classroom ||--o{ Assignment : "assigns"
Module ||--o{ Lesson : "contains"
Lesson ||--o{ UserProgress : "tracked by"
Lesson ||--o{ QuizAttempt : "tested by"
Simulation ||--o{ Trade : "contains"
Simulation ||--o{ Portfolio : "tracks"
Portfolio ||--o{ Trade : "records"
Badge ||--o{ UserBadge : "awarded"
```
### Entity Descriptions
| Entity | Key Fields | Relationships |
|--------|-----------|---------------|
| `tenants` | id, name, domain, branding, ai_mode, feature_flags | Parent to users, classrooms, modules |
| `users` | id, email, role, locale, tenant_id, xp, level, streak | Child of tenant; parent to progress, trades, badges |
| `classrooms` | id, name, tenant_id, teacher_id, invite_code | Child of tenant; parent to classroom_students |
| `classroom_students` | id, classroom_id, user_id, joined_at | Join table classroom <-> user |
| `modules` | id, cms_id, track, order, tenant_id | Child of tenant; parent to lessons |
| `lessons` | id, cms_id, module_id, order, estimated_minutes | Child of module; parent to progress |
| `user_progress` | id, user_id, lesson_id, status, score, completed_at | Tracks lesson completion |
| `quiz_attempts` | id, user_id, quiz_id, answers, score, timestamp | Stores quiz submissions |
| `simulations` | id, scenario, config, status, started_at | Manages simulation sessions |
| `portfolios` | id, user_id, simulation_id, cash_balance, holdings | Virtual portfolio state |
| `trades` | id, user_id, simulation_id, asset, amount, price, timestamp | Trade execution records |
| `badges` | id, name, description, icon, criteria | Badge definitions |
| `user_badges` | id, user_id, badge_id, earned_at | Awarded badges |
| `ai_logs` | id, tenant_id, user_id, tokens_used, model, timestamp | AI usage audit trail |
| `analytics_events` | id, user_id, event_type, metadata, timestamp | Behavioral telemetry |
## Security Architecture
### Defense in Depth
```mermaid
flowchart LR
subgraph Layer1[Network Edge]
DDoS[DDoS Protection<br/>Cloudflare]
WAF[WAF Rules<br/>Cloudflare]
DNS[DNS Security<br/>Cloudflare]
end
subgraph Layer2[Reverse Proxy]
TLS[TLS 1.3<br/>Traefik]
RATE[Rate Limiting<br/>Traefik Middleware]
HEADERS[Security Headers<br/>Helmet.js]
CORS[CORS Policy]
end
subgraph Layer3[Application]
JWT[JWT RS256<br/>Auth Guard]
RBAC[Role-Based Access<br/>Roles Guard]
TENANT[Tenant Isolation<br/>Tenant Guard]
DEPTH[Query Depth<br/>7 Level Max]
VALID[Input Validation<br/>Zod Schemas]
end
subgraph Layer4[Data]
ENCRYPT[Encryption at Rest<br/>PostgreSQL TDE]
BACKUP[Backup + Point-in-Time<br/>Recovery]
AUDIT[Audit Logging<br/>All Data Access]
end
DDoS --> WAF
WAF --> DNS
DNS --> TLS
TLS --> RATE
RATE --> HEADERS
HEADERS --> CORS
CORS --> JWT
JWT --> RBAC
RBAC --> TENANT
TENANT --> DEPTH
DEPTH --> VALID
VALID --> ENCRYPT
ENCRYPT --> BACKUP
BACKUP --> AUDIT
```
### Security Measures by Layer
| Layer | Measure | Implementation |
|-------|---------|---------------|
| Edge | DDoS protection | Cloudflare Always-on |
| Edge | WAF | Cloudflare WAF with OWASP ruleset |
| Edge | DNS | Cloudflare DNS with DNSSEC |
| Proxy | TLS 1.3 | Traefik auto-cert via Let's Encrypt |
| Proxy | HSTS | `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload` |
| Proxy | Rate limiting | 100 req/s average, 50 burst, per-IP via Traefik middleware |
| Proxy | Security headers | Helmet.js: X-Frame-Options, X-Content-Type-Options, CSP, Permissions-Policy |
| Proxy | CORS | Whitelist per tenant domain, credentials allowed |
| App | Auth | JWT RS256, short-lived access tokens (15m), httpOnly refresh cookies |
| App | RBAC | `@Roles()` decorator guards on every protected resolver |
| App | Tenant isolation | `@TenantScoped()` guard enforces cross-tenant access prevention |
| App | GraphQL safety | Depth limit (7), query complexity analysis, alias limiting |
| App | Input validation | Zod schemas validated on every API boundary |
| App | Prompt injection | Regex/ML-based sanitization before LLM calls |
| Data | Encryption at rest | PostgreSQL TDE or EBS encryption |
| Data | Backup | Daily automated backups, 30-day retention, Point-in-Time Recovery |
| Data | Audit | All data mutations logged with actor, timestamp, diff |
## Deployment Architecture
### Production Topology
```mermaid
graph TB
subgraph Internet
USER[User Browser]
end
subgraph Contabo_VPS["Contabo VPS (Single Node)"]
subgraph Traefik
T1["Traefik v3<br/>Port 443 (TLS)"]
T2["Port 80 Redirect<br/>-> 443"]
end
subgraph Docker_Services
API["api:3001<br/>NestJS"]
WEB["web:80<br/>nginx SPA"]
CMS["cms:3000<br/>Payload CMS"]
PG["postgres:5432<br/>PostgreSQL 16"]
RD["redis:6379<br/>Redis 7"]
MINIO["minio:9000<br/>S3 Storage"]
end
subgraph External_Access
PA["Portainer Agent<br/>:9001"]
end
subgraph Volumes
V_PG["pgdata"]
V_RD["redis-data"]
V_MI["minio-data"]
V_LE["letsencrypt"]
end
end
USER -->|HTTPS| T1
USER -->|HTTP| T2
T1 -->|api.investplay.app| API
T1 -->|investplay.app| WEB
T1 -->|cms.investplay.app| CMS
API --> PG
API --> RD
API --> MINIO
CMS --> PG
CMS --> MINIO
PA -.->|Docker socket| T1
PG --> V_PG
RD --> V_RD
MINIO --> V_MI
T1 --> V_LE
```
### CI/CD Pipeline
```mermaid
flowchart LR
PUSH["Push to main"] --> CI
PR["Pull Request"] --> CI
subgraph CI[GitHub Actions CI]
LINT[lint]
TYPECHECK[typecheck]
TEST[test]
BUILD[build]
LINT --> BUILD
TYPECHECK --> BUILD
TEST --> BUILD
end
CI -->|on main| DEPLOY_TRIGGER
subgraph CD[GitHub Actions Deploy]
DOCKER["Docker Build & Push<br/>api, web, cms"]
TAG["Tag: git-sha, latest"]
PUSH_GHCR["Push to GHCR"]
end
CD -->|Portainer webhook| PORTAINER
subgraph PROD[Contabo Server]
PORTAINER["Portainer Agent"]
PULL["docker compose pull"]
UP["docker compose up -d"]
HEALTH["Health Check"]
end
DEPLOY_TRIGGER --> DOCKER
DOCKER --> TAG
TAG --> PUSH_GHCR
```
### Docker Image Tags
| Service | Image | Tags |
|---------|-------|------|
| API | `ghcr.io/investplay/api` | `latest`, `<git-sha>` |
| Web | `ghcr.io/investplay/web` | `latest`, `<git-sha>` |
| CMS | `ghcr.io/investplay/cms` | `latest`, `<git-sha>` |
Layer caching via GitHub Actions `cache-from/cache-to` with `type=gha` for each service separately.
## Scaling Considerations
- **Current:** Single VPS (Contabo) with Docker Compose — sufficient for MVP to thousands of users
- **Short-term (Phase 3-4):** Separate API workers per CPU core, Redis cluster for pub/sub
- **Medium-term (Phase 6-7):** Horizontal API scaling behind Traefik load balancer, read replicas for PostgreSQL, separate BullMQ worker processes
- **Long-term:** Kubernetes migration, multi-region deployment for EU data residency, database sharding
See [CONTEXT.md](./CONTEXT.md) for the full project context, tech stack, and development workflow.