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
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:
638
docs/ARCHITECTURE.md
Normal file
638
docs/ARCHITECTURE.md
Normal 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.
|
||||
Reference in New Issue
Block a user