# 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 | | **Email** | 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 - `TenantContext` stored in `AsyncLocalStorage` — resolved from JWT `tenantId` claim - Prisma multi-schema extension for dynamic schema switching - Global/shared tables (lookups, platform config) in `public` schema - 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 `BlockRenderer` component 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](../.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 1. Create a feature branch from `main`: - `git checkout -b feat/my-feature` (new feature) - `git checkout -b fix/my-bug` (bug fix) 2. Start the development environment: `docker compose -f docker-compose.dev.yml up` 3. Develop iteratively with hot-reload enabled 4. Write tests following TDD cycle: Red -> Green -> Refactor 5. Run quality gates locally: ```bash pnpm lint pnpm typecheck pnpm test ``` 6. Commit using conventional commits: ``` feat(auth): add SAML SSO login flow fix(simulation): correct tick timestamp offset docs(api): update websocket event reference ``` 7. Push branch and open a Pull Request 8. CI runs automatically: lint -> typecheck -> test -> build 9. PR must pass all checks and receive code review approval 10. Squash merge to `main` (keeps history clean) 11. CI runs again on main, then auto-deploys to production ## Related Documents - [ARCHITECTURE.md](./ARCHITECTURE.md) — System architecture, request flow, multi-tenancy deep dive - [API.md](./API.md) — API reference, authentication, endpoints, error codes - [LOCALIZATION.md](./LOCALIZATION.md) — i18n setup, adding locales, ICU formatting - [DEVELOPMENT-ROADMAP.md](./DEVELOPMENT-ROADMAP.md) — Phase plan and progress tracking