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
18 KiB
18 KiB
InvestPlay — Project Context
Overview
InvestPlay is a cross-platform financial literacy platform targeting users aged 16-25. It combines interactive educational content, realistic market simulations, gamified progression, and AI-powered coaching to teach investing fundamentals. The platform serves both individual learners (B2C) and educational institutions (B2B) through a multi-tenant architecture with schema-level data isolation.
Vision
Make financial literacy universally accessible and engaging for the next generation of investors, starting with EU markets (Greece initial launch) and expanding globally.
Target Users
| Persona | Age Range | Platform | Key Need |
|---|---|---|---|
| Self-directed learner | 16-25 | Web, Mobile | Learn investing at own pace, earn XP, compete on leaderboards |
| Student (classroom) | 14-20 | Web | Complete assigned lessons, participate in live simulations |
| Teacher | 25-55 | Web | Create classrooms, assign curriculum, monitor progress |
| Institution admin | 30-60 | Web, Desktop | White-label platform, manage billing, view analytics |
| Parent (future) | 35-55 | Web, Mobile | Monitor child's progress, manage subscription |
Tech Stack
| Layer | Technology | Purpose |
|---|---|---|
| Monorepo | Turborepo 2.5 + pnpm 9.15 | Orchestrate builds, share code, manage dependencies |
| Backend | NestJS 11 + Node.js 20 | Modular API server with DI, guards, interceptors |
| API Protocol | GraphQL (Apollo Server 4) | Primary CRUD — type-safe, no over-fetching |
| Real-time | Socket.io (NestJS WebSocket gateway) | Live simulations, leaderboards, AI streaming |
| REST | Express (NestJS platform) | Webhooks (Stripe, Clerk, Resend), health checks |
| Frontend | React 18 + TypeScript + Vite 6 | SPA with fast HMR, tree-shaking, code splitting |
| CSS | TailwindCSS 4 + shadcn/ui (Radix primitives) | Design tokens, accessible components, dark mode |
| CMS | Payload CMS 3 (self-hosted, headless) | Dynamic block-based content, i18n, media library |
| Database | PostgreSQL 16 + Prisma ORM 6 | ACID compliance, JSON support, multi-schema tenancy |
| Cache/Queue | Redis 7 + BullMQ (NestJS BullMQ module) | Rate limiting, job queues, session store, leaderboards |
| Storage | S3-compatible (MinIO dev / Cloudflare R2 prod) | User avatars, PDF reports, CSV market data |
| Auth | Clerk (B2C) / JWT with SAML SSO (B2B) | Social login, passwordless, institutional SSO |
| Resend + React Email templates | Welcome emails, progress reports, teacher invites | |
| Billing | Stripe Connect | B2B seat-based subscriptions, usage metering |
| AI | OpenAI / Gemini (via backend proxy) | Socratic AI coach with token budget enforcement |
| Container | Docker Compose (dev) + Portainer/Traefik (prod) | Consistent environments, auto-SSL, monitoring |
| CI/CD | GitHub Actions -> GHCR -> Portainer webhook | Automated testing, building, and deployment |
| Testing | Vitest 3 + Testing Library + Playwright | Unit, integration, and E2E coverage |
| Desktop | Tauri v2 (Rust-based, future) | Native Windows/macOS/Linux wrapper |
| Mobile | Capacitor (future) | Native iOS/Android wrapper |
Directory Structure
investplay/
├── apps/
│ ├── api/ # NestJS backend
│ │ ├── src/
│ │ │ ├── modules/
│ │ │ │ ├── auth/ # JWT, SSO, role guards
│ │ │ │ ├── tenant/ # Multi-tenancy, feature flags
│ │ │ │ ├── curriculum/ # CMS-backed lesson delivery
│ │ │ │ ├── simulation/ # Historical tick streaming
│ │ │ │ ├── portfolio/ # Virtual trades, P&L calc
│ │ │ │ ├── ai-coach/ # LLM proxy with cost control
│ │ │ │ ├── analytics/ # Behavioral telemetry
│ │ │ │ ├── classroom/ # Teacher dashboard, assignments
│ │ │ │ ├── gamification/ # XP, badges, streaks
│ │ │ │ └── billing/ # Stripe Connect integration
│ │ │ ├── common/
│ │ │ │ ├── guards/ # JWT guard, tenant guard, roles guard
│ │ │ │ ├── decorators/ # @CurrentUser, @TenantId
│ │ │ │ ├── filters/ # Global exception filter
│ │ │ │ └── interceptors/ # Logging, timing, tenant injection
│ │ │ ├── config/ # Env-based config modules
│ │ │ └── main.ts # NestJS bootstrap
│ │ ├── prisma/
│ │ │ ├── schema.prisma # Full data model
│ │ │ └── migrations/ # Prisma migration history
│ │ ├── test/ # E2E test suites
│ │ ├── Dockerfile
│ │ └── package.json
│ ├── web/ # React + Vite frontend
│ │ ├── src/
│ │ │ ├── components/ # UI components (per feature)
│ │ │ ├── pages/ # Route pages
│ │ │ ├── hooks/ # Custom React hooks
│ │ │ ├── stores/ # Zustand stores
│ │ │ ├── lib/ # Utilities, API client
│ │ │ ├── styles/ # TailwindCSS setup
│ │ │ └── i18n/ # i18next configuration
│ │ ├── public/ # Static assets
│ │ ├── index.html
│ │ ├── vite.config.ts
│ │ ├── Dockerfile
│ │ └── package.json
│ ├── cms/ # Payload CMS
│ │ ├── src/
│ │ │ ├── collections/ # Content models
│ │ │ ├── globals/ # Site-wide settings
│ │ │ └── payload.config.ts
│ │ ├── Dockerfile
│ │ └── package.json
│ ├── mobile/ # Capacitor (future)
│ └── desktop/ # Tauri (future)
├── packages/
│ ├── types/ # @investplay/types
│ │ └── src/
│ │ ├── user.ts
│ │ ├── tenant.ts
│ │ ├── curriculum.ts
│ │ ├── simulation.ts
│ │ ├── portfolio.ts
│ │ └── analytics.ts
│ ├── ui/ # @investplay/ui
│ │ └── src/
│ │ ├── components/ # shadcn/ui primitives
│ │ └── lib/ # cn(), formatCurrency(), etc.
│ ├── i18n/ # @investplay/i18n
│ │ └── src/
│ │ ├── locales/
│ │ │ ├── en/ # English translations
│ │ │ │ ├── common.json
│ │ │ │ ├── finance.json
│ │ │ │ ├── gamification.json
│ │ │ │ ├── classroom.json
│ │ │ │ └── ai-coach.json
│ │ │ └── el/ # Greek translations
│ │ │ ├── common.json
│ │ │ └── ...
│ │ └── index.ts # i18next config
│ └── utils/ # @investplay/utils
│ └── src/
│ ├── validation.ts # Zod schemas
│ ├── formatting.ts # Currency, date, number
│ ├── constants.ts # App constants
│ └── env.ts # Env validation
├── docker/
│ ├── traefik/
│ │ ├── traefik.yml # Static config
│ │ └── dynamic.yml # Dynamic config (middleware, TLS)
│ └── nginx/
│ └── nginx.conf # Web SPA nginx config
├── scripts/
│ ├── setup.sh # Initial dev setup
│ ├── healthcheck.sh # Service health verification
│ └── backup.sh # Database backup script
├── docs/ # Project documentation
├── specs/ # Detailed specifications
├── .github/
│ └── workflows/
│ ├── ci.yml # Continuous integration
│ ├── deploy.yml # Deployment pipeline
│ └── pr-checks.yml # PR quality gates
├── .env.example # Environment template
├── turbo.json # Turborepo pipeline config
├── pnpm-workspace.yaml # Workspace definition
├── tsconfig.base.json # Shared TS config
├── .eslintrc.js # ESLint root config
├── .prettierrc # Prettier formatting
└── package.json # Root package.json
Architecture Patterns
Multi-tenancy
Approach: Schema-level isolation via PostgreSQL schemas.
- Each tenant (institution) gets its own PostgreSQL schema
TenantContextstored inAsyncLocalStorage— resolved from JWTtenantIdclaim- Prisma multi-schema extension for dynamic schema switching
- Global/shared tables (lookups, platform config) in
publicschema - Connection pooling per schema via Prisma's
connection_limit - Tenant resolution pipeline: HTTP request -> JWT middleware -> extract tenantId -> set AsyncLocalStorage -> Prisma middleware applies schema filter
API Design
- GraphQL is the primary API for all CRUD operations — single endpoint
/graphql - REST used only for webhooks (Stripe, Clerk, Resend) and
/health - WebSocket namespaces for real-time features:
/simulations(tick streaming),/ai-coach(chat streaming),/classroom(live teacher broadcasts) - Input validation via Zod schemas at every API boundary
- All GraphQL queries have depth limiting (max 7) and cost analysis
Content Management
- Payload CMS serves as the single source of truth for all educational content
- Content model: Track -> Module -> Lesson -> Block (dynamic array)
- Block types: Hero, Text, Image, Video, Quiz, Calculator, BudgetGame, TradeSimulation
- Frontend renders content via a
BlockRenderercomponent that maps CMS block types to React components - CMS localization: each content collection supports locale variants
AI Proxy Architecture
- All LLM requests route through the NestJS
AICoachModule— never direct from client - Context injection: backend appends portfolio state, lesson context, and user history to the system prompt
- Rate limiting via Redis token bucket (per-user daily, per-tenant monthly)
- BYOK (Bring Your Own Key): institutions can provide their own API key for higher quotas
- PII sanitization: names, emails, phone numbers stripped before sending to LLM
- Audit logging: every AI interaction logged with token count, latency, and model used
Security Architecture
- Authentication: JWT RS256 (asymmetric) — access token (15m) + refresh token (7d)
- Authorization: Role-based guards (
@Roles(ROLE.TEACHER)) + tenant scope enforcement - GraphQL safety: Depth limiting (max 7), query cost analysis, persisted operations (future)
- HTTP hardening: Helmet.js headers, CORS whitelist per tenant, rate limiting (100 req/min default)
- WebSocket: JWT handshake authentication, room-based isolation per tenant
- Secrets: Zero secrets in code — all via environment variables, Docker secrets, or secret manager
- GDPR: Soft-deletes, PII anonymization pipeline, data export API, retention policies
Key Design Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Monorepo tool | Turborepo | Better DX than Nx for this scale; native ESM support; simpler cache config |
| Package manager | pnpm | Disk-efficient (hard links), strict dependency resolution, workspace protocol |
| Backend framework | NestJS | Modular architecture with DI, decorator-based guards, native GraphQL + WebSocket support |
| Frontend build | Vite (not Next.js) | SPA is sufficient (no SSR SEO needed); faster builds; simpler deployment (nginx static) |
| CMS | Payload CMS (self-hosted) | TypeScript-native, code-first config, block-based content, built-in i18n, no vendor lock-in |
| Database ORM | Prisma | Type-safe queries, migration-first workflow, multi-schema preview feature for tenancy |
| Auth (initial) | Clerk | Fast to integrate for B2C; supports social login, passwordless, webhooks |
| Auth (B2B future) | Auth0 / SAML SSO | Required for institutional customers with Active Directory / Google Workspace |
| API protocol | GraphQL primary | Type-safe contracts, no over-fetching, single endpoint, introspection for DX tools |
| Real-time | Socket.io | Mature, fallback transport, room support, NestJS integration |
| Background jobs | BullMQ + Redis | Persistence, delayed jobs, rate limiting, job events for monitoring |
| Styling | TailwindCSS + shadcn/ui | Utility-first, design tokens, accessible Radix primitives, small bundle |
| i18n | i18next + ICU MessageFormat | Industry standard, pluralization, context, rich text in translations |
| AI cost control | Server-side token bucket | Hard enforcement not reliant on OpenAI budget alerts; Redis is fast and atomic |
| Container registry | GHCR | Free with GitHub, tight integration with Actions, no extra credentials |
| Deployment | Docker Compose + Traefik | Simpler than k8s for current scale; Traefik auto-SSL with Let's Encrypt |
| Desktop (future) | Tauri v2 | Rust-based, 10x smaller binary than Electron, memory-efficient |
| Mobile (future) | Capacitor | Minimal wrapper, access to native APIs, shares web codebase |
State Management
| State Type | Technology | Usage |
|---|---|---|
| Server state | @tanstack/react-query | Data fetching, caching, optimistic updates, pagination |
| GraphQL state | Apollo Client (future) | When GraphQL adoption increases; for now REST-like via react-query |
| Client/auth state | Zustand | Auth session, UI preferences (theme, sidebar), gamification (XP, level) |
| Form state | React Hook Form + Zod | All forms with schema validation |
| URL state | React Router v7 | Search params, path params for routing |
| Real-time state | Socket.io client | Live simulation ticks, leaderboard updates, AI chat streaming |
Naming Conventions
| Category | Convention | Example |
|---|---|---|
| Files (components) | kebab-case | user-profile.tsx, simulation-chart.tsx |
| Files (modules/classes) | PascalCase | AuthModule.ts, UserService.ts |
| Variables | camelCase | isLoading, userData |
| Functions | camelCase | getUserById(), formatCurrency() |
| React components | PascalCase | UserProfile, SimulationChart |
| Types | PascalCase | User, TenantConfig |
| Interfaces | PascalCase (no I prefix) | UserRepository, AuthPayload |
| Enums | PascalCase (singular) | Role, LessonStatus, TenantTier |
| Database tables | snake_case | users, user_progress, classroom_students |
| Database columns | snake_case | tenant_id, first_name, created_at |
| GraphQL types | PascalCase | User, LessonProgress, TradeInput |
| GraphQL fields | camelCase | userId, portfolioValue, totalXp |
| Environment variables | UPPER_SNAKE_CASE | DATABASE_URL, JWT_SECRET |
Environment Variables
See .env.example for the complete template with all variables and descriptions.
Key groups:
- APP —
NODE_ENV,PORT,APP_URL,API_URL,CORS_ORIGINS - DATABASE —
DATABASE_URL,DATABASE_POOL_MIN,DATABASE_POOL_MAX - REDIS —
REDIS_URL,REDIS_PASSWORD - JWT —
JWT_SECRET,JWT_EXPIRES_IN,JWT_REFRESH_EXPIRES_IN - AUTH —
CLERK_SECRET_KEY,CLERK_WEBHOOK_SECRET,AUTH_SSO_ENABLED - AI —
OPENAI_API_KEY,GEMINI_API_KEY,AI_DEFAULT_DAILY_LIMIT,AI_DEFAULT_MONTHLY_TOKEN_BUDGET - STORAGE —
S3_ENDPOINT,S3_ACCESS_KEY,S3_SECRET_KEY,S3_BUCKET - EMAIL —
RESEND_API_KEY,EMAIL_FROM - BILLING —
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET - TENANT —
TENANT_DEFAULT_AI_MODE,TENANT_MAX_TOKENS_MONTHLY - i18n —
DEFAULT_LOCALE,SUPPORTED_LOCALES - CMS —
CMS_API_URL,CMS_API_KEY - SECURITY —
RATE_LIMIT_TTL,RATE_LIMIT_MAX,GRAPHQL_DEPTH_LIMIT - LOGGING —
LOG_LEVEL,LOG_FORMAT
Testing Strategy
| Layer | Tool | Scope | Frequency |
|---|---|---|---|
| Unit | Vitest | Backend services, utils, hooks, formatting | Every commit |
| Integration | Supertest + Vitest | API endpoints, GraphQL resolvers | Every commit |
| Component | Vitest + Testing Library | React component rendering, user interactions | Every commit |
| E2E | Playwright | Critical user flows (login, lesson, simulation) | CI / pre-release |
| Contract | GraphQL schema tests | Schema validation, resolver shape | On schema change |
| Coverage target | 80%+ | Core business logic (services, resolvers, hooks) | CI gate |
Development Workflow
- Create a feature branch from
main:git checkout -b feat/my-feature(new feature)git checkout -b fix/my-bug(bug fix)
- Start the development environment:
docker compose -f docker-compose.dev.yml up - Develop iteratively with hot-reload enabled
- Write tests following TDD cycle: Red -> Green -> Refactor
- Run quality gates locally:
pnpm lint pnpm typecheck pnpm test - Commit using conventional commits:
feat(auth): add SAML SSO login flow fix(simulation): correct tick timestamp offset docs(api): update websocket event reference - Push branch and open a Pull Request
- CI runs automatically: lint -> typecheck -> test -> build
- PR must pass all checks and receive code review approval
- Squash merge to
main(keeps history clean) - CI runs again on main, then auto-deploys to production
Related Documents
- ARCHITECTURE.md — System architecture, request flow, multi-tenancy deep dive
- API.md — API reference, authentication, endpoints, error codes
- LOCALIZATION.md — i18n setup, adding locales, ICU formatting
- DEVELOPMENT-ROADMAP.md — Phase plan and progress tracking