Skip to content

overview architecture

Sergey Bogorad edited this page Aug 1, 2026 · 4 revisions

System architecture

Kognitika uses an event-driven architecture (EDA) with a clear separation between UI components, engine hooks, and analytics processing. This document covers the major architectural patterns.

Event-driven architecture with EventBus

The EventBus class at src/core/events/event-bus.ts is the central communication channel. It decouples trainer engines from UI components, analytics subscribers, and persistence layers. Engines emit events through the bus, and subscribers react to them without direct dependencies.

Key events defined in EventBus.EVENTS:

Event Emitter Subscribers
CELL_CLICK Engine Analytics, Recorder
TRAINING_COMPLETE Engine DB-writer, Leaderboard
MISTAKE_MADE Engine Analytics
STABILITY_UPDATE Analytics UI (HUD widgets)
DIFFICULTY_SUGGESTION Analytics worker Engine (adaptive mode)

The EventBus supports middleware chains and Zod schema validation for every event type. Validation errors are caught by a configurable handler instead of crashing the bus.

use{Module}Engine pattern

Every trainer module follows the same hook pattern:

UI Component <--> use{Module}Engine <--> EventBus <--> Analytics / Subscribers

Each use{Module}Engine hook (for example, useSchulteEngine, useNBackEngine, useTypingEngine) owns the game state, exposes actions (click, restart, submit), and emits events to the EventBus. UI components render from the hook's state and call its actions. They never contain game logic directly.

This pattern gives:

  • Testability: engines can be tested without DOM rendering.
  • Consistency: every trainer shares the same interface for state, actions, and event emission.
  • Analytics decoupling: analytics subscribers receive events through the bus without engine imports.

Seeded determinism

All generators (Schulte grids, N-Back sequences, spatial layouts, mental-math problems) accept a seed parameter. The same seed always produces the same output, which makes tests reproducible:

const grid1 = generateGrid(5, 'classic', 42);
const grid2 = generateGrid(5, 'classic', 42);
expect(grid1).toEqual(grid2); // always true

This is verified by src/tests/reproducibility.test.ts.

Analytics boundaries and Rust direction

The current product has two related but distinct analytics contracts:

  • src/workers/analytics.worker.ts and src/lib/cognitive-metrics.ts use a lightweight click-oriented JavaScript/TypeScript contract for immediate UI feedback.
  • src/core/analyze-session/session-analysis.ts and crates/kognitika-core implement the strict full-session AnalyzeSession contract. The Rust crate already supports native builds and exports analyzeSessionJson for WASM.

Rust is therefore not merely a future placeholder, but it is not yet the production authority either. The staged target is:

  1. Every cognitive module emits a canonical versioned event contract.
  2. Express validates ownership and creates durable analytics jobs in PostgreSQL.
  3. TypeScript remains authoritative while a native Rust/Axum analyzer runs in shadow mode.
  4. Field-level parity, latency, failure, and fallback metrics gate a deterministic canary rollout.
  5. Rust becomes the primary session analyzer with a temporary TypeScript circuit-breaker fallback.
  6. Longitudinal, baseline-aware skill dynamics move to Rust after session analysis is stable.

Browser WASM remains a separate frame-budget decision. Existing benchmarks show that worker isolation helps responsiveness, but do not justify replacing the browser TypeScript path solely for throughput. See Rust analytics roadmap.

Express + Socket.io server

The server at server.ts starts an Express app with a Socket.io server attached to the same HTTP server. It serves:

  • API routes under /api/* (auth, game, analytics, admin, feedback, ideas, leaderboard, neurotrainer, daily-trajectory, practice-flow, observability).
  • Static files from the Vite build in production.
  • Vite dev middleware in development mode.

Rate limiting applies to all API routes (100 requests per 15 minutes) with a stricter limit for auth endpoints (10 per hour).

The Socket.io server handles real-time duel sessions through src/server/realtime/duels.ts. CORS configuration is shared between Express and Socket.io via src/server/config/cors.ts, which enforces an explicit allowlist in production.

EDA flow diagram

  ┌──────────────┐    action     ┌──────────────────┐
  │   UI Widget  │◄──────────────│  use{Module}Engine│
  │  (React)     │    state      │                  │
  └──────┬───────┘               └────────┬─────────┘
         │                                │ emit events
         │                                ▼
         │                       ┌──────────────────┐
         │                       │    EventBus       │
         │                       │  (type-safe,      │
         │                       │   validated)      │
         │                       └────┬──────┬───────┘
         │                            │      │
         │              ┌─────────────┘      └─────────────┐
         │              ▼                                 ▼
         │   ┌──────────────────┐              ┌──────────────────┐
         │   │  Analytics       │              │  DB / Leaderboard │
         │   │  Subscriber      │              │  Subscriber       │
         │   └────────┬─────────┘              └──────────────────┘
         │            │
         │            ▼
         │   ┌──────────────────┐
         │   │ Analytics Worker │
         │   │ (JS/TS current)  │
         │   └────────┬─────────┘
         │            │ metrics
         │            ▼
         │   ┌──────────────────┐
         └──►│ STABILITY_UPDATE │
             │ DIFFICULTY_SUG.  │
             └──────────────────┘

The diagram shows the current UI flow: a user interacts with a widget, the engine updates state and emits events, and subscribers forward lightweight analytics to the JavaScript/TypeScript worker or persist completed sessions. The planned server-side Rust path is deliberately asynchronous and is not shown as part of the synchronous UI loop: durable job → Axum analyzer → versioned summary → longitudinal projection.

Clone this wiki locally