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
639 lines
19 KiB
Markdown
639 lines
19 KiB
Markdown
# 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.
|