spikerjandClaude Fable 5.1 9f87013e68 docs: Art Studio groups + focus dialog; photo stacking is desktop-only; stack→concept import
Reflects the 2026-09-25 Art Studio decisions: chess sets become generic
ArtAssetGroups inside My Assets (api/artstudio/groups), Open takes over the
screen in a focus dialog, the in-browser Photo Stack / Touch-Up / Darkroom
Companion lane is retired (Desktop Stacker is the only stacker), and a
published stack re-enters the web side as a Completed concept
(POST api/artstudio/concepts/from-photograph).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 12:41:29 +00:00

SpikerSoft Platform

SpikerSoft exists to give students accessible education that expands their horizons — through technology, hands-on learning, entrepreneurship, science, and real-world skills — helping every student discover and develop their own interests and abilities beyond the limits of a traditional classroom. Families learn together — at home, on the road, or alongside school — and sponsors fund the students, the tools, and the journeys that turn learning into experience.

Part classroom, part workshop, part expedition.

Platform Overview

SpikerSoft is a multi-repo platform. The full product stack today:

SpikerSoft Platform/
├── spikersoft-angular/            # Web SPA — Angular 22 Nx workspace (PWA-enabled)
├── spikersoft-backend/            # .NET 10 C# solution — REST API, GameServer, 28 event-handler/worker projects
├── spikersoft-artpipe/            # ProArt ("art_pipe") — Python GPU generation pipeline behind the Art Studio
├── spikersoft-infrastructure/     # Docker Swarm stacks, Traefik, MinIO, OpenBao, runbooks, CI plumbing
├── spikersoft-ios/                # Native SwiftUI client (embedded Godot games, activity tracker)
├── spikersoft-android/            # Native Jetpack Compose client (embedded Godot games, activity tracker)
├── spikersoft-games-godot/        # Unified Godot 4 multi-mode game client (Space MVP; Dungeon/Voxel/Hex modes)
├── spikersoft-stacker/            # Desktop focus-stacking app (Rust + wgpu + egui, fully offline)
└── spikersoft-time-and-materials/ # Field-service Android app (backed by /api/tm in spikersoft-backend)

The web pair (spikersoft-angular + spikersoft-backend) remains the center of gravity; the other repos are real shipping surfaces documented in their own sections below (Native Mobile Apps, Godot Games Client, Desktop Stacker, Time & Materials, Art Studio).

Frontend (spikersoft-angular)

An Nx monorepo Angular application providing:

  • Integrated Game Ecosystem - Three interconnected web-based games (Space, Voxel, Dungeon Crawler) forming a single MMO experience, a server-authoritative Hex Tower Defence, plus standalone learning games (Chess, Fishing, puzzle games) and a set of routed mini-games
  • Art Studio - AI-assisted 2D/3D asset generation: prompt-to-3D pipeline (concept → modeling → texturing → rigging → animation → export → enrichment), concept candidate picking/editing, material variants, chess piece sets, a class gallery with moderation, focus stacking, and loading finished assets directly into the games
  • Visual Programming Tools - Blockly and Rete.js node-graph editors for programming in-game robots and spacecraft, doubling as real STEM learning tools
  • 3D Visualization - Three.js and WebGL-powered immersive experiences
  • Developer Tools - JSON editors, encoding utilities, visual flowcharts, and coding playgrounds/curricula for eight tracks (C#, Python, JavaScript, Regex, C, C++, SQL, x86 assembly) that compile and grade student code either server-side or fully in-browser via WebAssembly runtimes (works offline)
  • Geography & Travel - Interactive world maps, a shared WebGL geo-globe, country exploration, travel planning tied to real-world adventures
  • Photography - Personal photo gallery with RAW uploads, burst/stack grouping, AI auto-tagging, and a camera-gear registry derived from EXIF
  • Activity Tracker - MapLibre + PMTiles GPS tracker with recorded routes, analysis reports, and repeatable paths with attempts
  • Sponsorship & Fundraising - Stripe-powered donation system connecting donors with individuals, families, family funds, and location funds
  • Real-time Communication - Authenticated WebRTC video calling (SignalR signaling + coturn TURN), OvenPlayer live streaming, SignalR chat, 1:1 direct messages, and notifications
  • Team & Hiring - Dynamic staff profiles and a master-detail careers page with a geo-globe (ambassadors auto-generated per location)

Tech Stack:

  • Angular 22 with standalone components (TypeScript 6, Nx 23)
  • Three.js for 3D graphics
  • RxJS + Signals for reactive state management
  • SignalR for real-time updates
  • Keycloak authentication integration
  • PWA service worker with offline-first lesson grading

Backend (spikersoft-backend)

A multi-project .NET 10 solution providing:

  • REST API - Main platform API with CQRS pattern (MediatR)
  • Game Server - Purpose-built real-time multiplayer server
  • Event Handlers - 28 distributed worker projects for async processing (code grading, book pipeline, art pipeline, GPU coordination, photography, notifications, host-fleet monitoring, and more — see Event Handlers)
  • AI Services - LLamaSharp-powered ML capabilities, GPU-scheduled vision/quiz/embedding workers, and the artpipe model fleet

Tech Stack:

  • .NET 10 / C# 14
  • MongoDB (sharded cluster) - Primary database
  • Redis (cluster) - Caching and pub/sub
  • RabbitMQ - Message queue
  • SignalR - Real-time communication
  • Keycloak - Identity management
  • Docker Swarm - Container orchestration

Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│                              CLIENTS                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐          │
│  │  Angular Web App │  │  Native iOS +    │  │  Godot Games     │          │
│  │  (Browser, PWA)  │  │  Android apps    │  │  Client (embeds  │          │
│  │                  │  │  (SwiftUI /      │  │  in mobile apps) │          │
│  │                  │  │   Compose)       │  │                  │          │
│  └────────┬─────────┘  └────────┬─────────┘  └────────┬─────────┘          │
│  ┌──────────────────┐  ┌──────────────────┐                                 │
│  │  Time & Materials│  │  Desktop Stacker │   (Stacker: offline pipeline,   │
│  │  Android app     │  │  (Rust, offline) │    publish needs sign-in)       │
│  └────────┬─────────┘  └──────────────────┘                                 │
└───────────┼─────────────────────┼─────────────────────┼─────────────────────┘
            │ HTTPS/WSS           │ HTTPS + WSS          │ WSS (GameServer)
            ▼                     ▼                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           TRAEFIK (Load Balancer)                           │
└─────────────────────────────────────────────────────────────────────────────┘
            │                     │                     │
            ▼                     ▼                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                              SERVICES                                        │
├─────────────────────────────────────────────────────────────────────────────┤
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐          │
│  │  SpikerSoft.API  │  │ SpikerSoft.Game  │  │  Event Handlers  │          │
│  │  (REST + SignalR)│  │ Server (30Hz)    │  │  (RabbitMQ, 28   │          │
│  │                  │  │                  │  │   worker projects)│         │
│  │  • Auth          │  │  • Zone Manager  │  │  • Code Execution│          │
│  │  • CRUD APIs     │  │  • Space Zones   │  │  • Book pipeline │          │
│  │  • Chat + DM Hubs│  │  • Voxel Zones   │  │  • ArtPipe (GPU) │          │
│  │  • Sponsor API   │  │  • Camp Zones    │  │  • GPU Coordinator│         │
│  │  • Lessons API   │  │  • Robot Engine  │  │  • Photography   │          │
│  │  • Geography API │  │  • Combat System │  │  • Notifications │          │
│  │  • Tracker API   │  │                  │  │  • Fleet agents  │          │
│  │  • Art Studio API│  │                  │  │  • …see inventory│          │
│  └────────┬─────────┘  └────────┬─────────┘  └────────┬─────────┘          │
└───────────┼─────────────────────┼─────────────────────┼─────────────────────┘
            │                     │                     │
            ▼                     ▼                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           DATA LAYER                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐          │
│  │  MongoDB Cluster │  │  Redis Cluster   │  │  RabbitMQ        │          │
│  │  (Sharded)       │  │  (6 nodes)       │  │  (Message Queue) │          │
│  │                  │  │                  │  │                  │          │
│  │  • Users         │  │  • Session Cache │  │  • Task Queues   │          │
│  │  • Game State    │  │  • Rate Limiting │  │  • Event Bus     │          │
│  │  • Content       │  │  • SignalR       │  │  • DLQ Support   │          │
│  └──────────────────┘  └──────────────────┘  └──────────────────┘          │
└─────────────────────────────────────────────────────────────────────────────┘
            │
            ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           IDENTITY                                           │
├─────────────────────────────────────────────────────────────────────────────┤
│  ┌──────────────────────────────────────────────────────────────┐          │
│  │  Keycloak v26 (OAuth2 / OIDC)                                │          │
│  │  • JWT Token Issuance                                         │          │
│  │  • Multi-tenant Organization Support                          │          │
│  │  • SSO Integration                                            │          │
│  └──────────────────────────────────────────────────────────────┘          │
└─────────────────────────────────────────────────────────────────────────────┘

Game Server Architecture

The SpikerSoft.GameServer is a purpose-built, high-performance real-time game server powering all three integrated game experiences through a unified backend.

Key Features

  • 30 Hz Fixed Timestep - Deterministic game loop for consistent simulation
  • Hybrid Transport - WebSocket for browsers, LiteNetLib (UDP) for native clients
  • Server-Authoritative - Anti-cheat by design, server owns game state
  • Command-Based Sync - Client prediction with server reconciliation
  • Persistent Zones - Shared world state like RuneScape/EverQuest
  • Full Persistence - Character progress saved permanently
  • Robot Execution Engine - Server-side programmable robot instruction processing

Capacity Targets

  • 200-500 concurrent players per zone
  • ~5,000 robot entities per zone (250 clients x ~20 robots each)
  • Multiple zones per server instance
  • Spatial hashing for O(1) proximity queries

Integration

The game server integrates with the existing SpikerSoft ecosystem:

SpikerSoft.GameServer/          # Game server executable
    ├── Zones/
    │   ├── SpaceZone           # Orbital mechanics, spacecraft, docking
    │   ├── VoxelZone           # Block world, robot management, terraforming
    │   └── CampZone            # Dungeon crawler RPG, stronghold PvE
    ├── Services/
    │   ├── RobotExecutionEngine    # Server-side robot program interpreter
    │   └── MongoRobotProgramRepository  # Robot program persistence
    ├── References:
    │   ├── SpikerSoft.Common   # Shared models (Commands, Events, Entities)
    │   ├── SpikerSoft.Data     # MongoDB persistence
    │   └── SpikerSoft.Business # Game services, maze generation

Integrated Game Ecosystem

Three Angular game components share the same SpikerSoft.GameServer backend and form a single interconnected web-based MMO. Players transition seamlessly between experiences -- orbiting in space, terraforming planets with voxel blocks, and defending strongholds in dungeon combat -- all tied together through shared characters, resources, and progression.

┌────────────────┐     ┌──────────────────┐     ┌────────────────────┐
│   Space Game   │────▶│   Voxel World    │────▶│  Dungeon Crawler   │
│  (ship/orbit)  │     │ (terraform/build)│     │  (stronghold PvE)  │
└────────────────┘     └──────────────────┘     └────────────────────┘
        │                      │                         │
        └──────────────┬───────┘─────────────────────────┘
                       ▼
              SpikerSoft GameServer
         (WebSocket / MessagePack / MongoDB)

Space Game

A real-time orbital mechanics simulation where players pilot spacecraft, manage fleets of drones, mine asteroids, and navigate between celestial bodies. The space game serves as the entry point to planetary exploration -- when you land on a terraformed planet or asteroid, you transition into the voxel world.

  • Server-authoritative physics with thruster simulation, docking, and autopilot
  • Spacecraft management with ownership enforcement
  • Beam weapons, mines, projectile combat, and radar/stealth systems
  • All UI rendered inside the WebGL scene via the shared @spikersoft/gl-hud framework (no DOM overlays; see gl-hud)
  • In-scene options menu with rebindable keys (KeybindingManager, server-synced via api/game/{gameId}/keybindings) and persisted graphics settings (bloom, FXAA)
  • Procedural planet and asteroid generation

Voxel World (Minecraft Port)

A multiplayer voxel sandbox built on a TypeScript Minecraft port, connected to SpikerSoft's backend for server-authoritative multiplayer. Players terraform planets, build strongholds, and deploy programmable robots to automate resource gathering and construction.

  • Full block world with chunk-based terrain, mining, and building
  • Multiplayer via SpikerSoft GameServer (character sync, block changes)
  • Programmable robot units (gatherer, builder, scout, combat) with visual programming
  • In-world real-time debugging with 3D path visualization and step-through execution

Dungeon Crawler RPG

An EverQuest-inspired multiplayer RPG with Ultima Online-style ruleset. Players explore procedurally generated dungeons, fight bosses, and ultimately assault other players' strongholds built in the voxel world. The pilot of a spacecraft can serve as the player character.

  • Server-authoritative combat with spells, potions, and equipment
  • Procedural maze/dungeon generation with boss encounters
  • Zone-based world with camp, dungeon, and stronghold areas
  • Character persistence with level progression and inventory

Cross-Game Integration

Feature Space Voxel Dungeon
Characters Ships/spacecraft Avatars + robots RPG characters
Entry point Launch from orbit Land on planet Enter stronghold
Resources Asteroid mining Block harvesting Loot drops
Ownership Spacecraft per player Robots per player Characters per player
Entity filtering ListShipsCommand ListUnitsCommand (voxel zones) ListCharactersCommand (camp/dungeon zones)

gl-hud — in-scene WebGL UI

All game UI for the space game and dungeon crawler renders inside the Three.js scene — an orthographic HUD scene composed of troika-three-text labels and shader-driven bars, with zero DOM overlays. This started as space-game code and is now the shared @spikersoft/gl-hud library (spikersoft-angular/libraries/game/gl-hud/), exporting HUDScene, HUDPanel, HUDButton, HUDTextInput, a layout manager, tooltips, prompts, messages, context menus, and a crosshair. New game UI should compose these primitives rather than reaching for HTML.

Beyond the MMO trio

The three MMO games are not the whole games surface:

  • Hex Tower Defence — server-authoritative multiplayer tower defence. The TypeScript game reducer is ported to C# (spikersoft-backend/SpikerSoft.Games.HexTowerDefence/) and compiled into the API, with its own SignalR hub at /hubs/hex-tower-defence (it does not use the GameServer). Frontend lib: spikersoft-angular/libraries/game/hex-tower-defence/. Its buildings and enemies can be reskinned with Art Studio assets (Asset-to-Game Integration).
  • Chess — see Chess Game; pieces can be Art-Studio-generated sets.
  • Routed mini-games — /deploy-defender, /github-sweeper, /snake-scroller, /commit-snake, /duality-game, /hexatile-game, and /perfidy (an alias of the snake game), plus the fishing and puzzle games. Registered in projects/spikersoft/src/routes.ts.

Robot & Visual Programming

The robot system brings SpikerSoft's educational mission into the game world. Players visually program robots using the same Blockly and Rete.js tools available in SpikerSoft's developer toolbox, creating a bridge between gameplay and real STEM learning.

How It Works

  1. Deploy a robot from your spacecraft onto a planet surface
  2. Program it using Blockly (drag-and-drop blocks) or Rete (node graph editor) -- directly in the game world
  3. Compile the visual program into a typed instruction list (client-side)
  4. Upload the instructions to the server for secure execution
  5. Debug with real-time 3D visualization -- glowing pathfinding lines, floating instruction labels, energy bars, and ghost block previews

Instruction Set

Programs compile to a typed instruction set (not arbitrary code) for security and scalability:

Category Instructions
Movement MoveTo, MoveForward, Turn
World Mine, Place, Scan
Control Branch, Loop, Wait, CallSubroutine
Variables SetVariable, GetVariable, Compare, math ops
Communication Broadcast, OnMessage, TransferItem

RAM as a Game Mechanic

Each robot has a RAM capacity (a fun, thematic resource constraint) that limits how complex its program can be. Simple gather-and-return loops fit in a small bot; complex multi-step build routines require upgraded hardware. This teaches resource-awareness and code optimization.

Encapsulation

Once a player writes a working routine (e.g., "gather moon rocks and return to base"), they can save it and reuse it as a single block/node in future programs. This mirrors real software engineering -- building abstractions from tested components.

Multi-Robot Coordination

Robots communicate via broadcast channels, enabling fleet-level behaviors:

  • A scout broadcasts "ore found at (x,y,z)" on the resources channel
  • Gatherer robots listen on resources and navigate to the coordinates
  • Builders listen for "materials delivered" and begin construction
  • Robots can transfer items to each other when within 2 blocks

Visual Programming Tools

The same Blockly and Rete editors available at /tools/(tools:blockly) and /tools/(tools:diagram) power robot programming. When opened in "robot mode" within the voxel game, they load robot-specific blocks/nodes instead of general-purpose ones. This dual use means skills learned programming robots transfer directly to the developer tools, and vice versa.

Mobile Readiness

  • Touch-friendly UI with 44px minimum touch targets
  • Protocol version negotiation for native iOS/Android clients
  • Offline program editing with localStorage cache and dirty-tracking sync

Art Studio

The Art Studio is SpikerSoft's AI-assisted asset creation suite: students and staff generate 2D concept art and full 3D game assets from text prompts and/or reference images, watch them move through a multi-stage GPU pipeline live, curate the results in a personal library and a moderated class gallery, and load finished assets directly into the platform's games. It spans four repos: the Angular feature (spikersoft-angular/libraries/features/art-studio/), the .NET orchestration (spikersoft-backend, ArtStudio domain + SpikerSoft.EventHandlers.ArtPipeProcessor), the Python model pipeline (spikersoft-artpipe, aka ProArt / art_pipe), and the per-model Swarm stacks (spikersoft-infrastructure/spikersoft-artpipe-model-*).

What a user can do

The shell (art-pipe-shell) is a five-view app routed at /tools/(tools:art-studio) behind AuthGuard: Concepts · Pipeline · Library · Metrics · Builder. Art Studio is for fictional 2D/3D art; photo work (stacking, touch-up) lives in the Desktop Stacker and the Photography gallery.

  • Generate an asset from a text prompt, an uploaded image, or both (art-prompt-composer). The composer exposes a per-stage "git rail" editor (concept → … → enrichment) with auto/manual/skip badges, per-stage model pickers, and workflow presets. Generation modes: Generate, Batch, Preset Batch, Continuous, and Coverage Matrix (the multi-asset modes are client-side fan-out over the single-asset submit endpoint). A distinct Text → 3D path skips the concept stage entirely (Shap-E / Hunyuan3D direct).
  • Pick a concept candidate — the asset parks in AwaitingSelection, the concept stage's candidate images (typically 6) render as a grid, and POST api/artstudio/{id}/select-concept resumes the pipeline. Unpicked candidates stay reproducible (per-image seed recorded in provenance).
  • Edit a concept candidate by instruction — POST {id}/edit-concept, backed by FLUX.2 Klein (fast) or Qwen-Image-Edit (precision). The asset re-parks for another pick after the edit completes (or fails), so editing is iterative.
  • Create material variants — POST {id}/material-variant runs a "Material Variation (Paint 2.1)" retexture that preserves the source topology/UVs and produces a sibling asset (blobs copied, VariantOfAssetId provenance) — e.g. "the same knight, but polished gold".
  • Watch live progress — stage timeline with state chips, percent, queue position ("you are Nth in line", published by the GPU coordinator), and per-stage phase text. Delivered over SignalR (hubs/notifications, art.asset.lifecycle events) and degrades to 4-second polling.
  • View results in-browser — a three.js GLB viewer with skeleton/animation-clip playback and PBR channel switching (art-asset-viewer).
  • Manage a personal library — search/status/kind/tag filters (deep-linkable query params), cancel, delete, restart from any stage, download. Tiles are uniform (a fixed five-slot action bar; an error line never shifts the row) and Open takes over the screen in a focus dialog (?asset= deep link, Escape/Back closes, focus and scroll return to the tile you opened).
  • Export — per-artifact downloads, a streamed ZIP bundle (GET api/artstudio/{id}/bundle: mesh + textures + manifest), and a games-handoff manifest (GET {id}/manifest).
  • Share, remix, rate — assets can be submitted to a class gallery (art-gallery), where staff moderate submissions and a flagged-content review queue (/admin/art-review, /admin/art-gallery-review). Approved gallery assets can be remixed by other users (RemixOfAssetId provenance) and rated.
  • Group assets — an ArtAssetGroup is a folder-like card inside My Assets (api/artstudio/groups; kinds ChessSet and Custom, e.g. a monster set for a dungeon), with a slot template and members you click into. A chess-set group fans one theme out into six per-piece prompts; its slot keys equal the game loadout slot ids, so a set can be shared, moderated, and equipped into the chess game. See Asset-to-Game Integration.
  • Use a published stack as a concept — a stack published from the Desktop Stacker (a Stacked photograph in the gallery) can be imported as a Completed concept (POST api/artstudio/concepts/from-photograph; lightbox Use as concept or the concepts wall's From my photos) and then edited and promoted to a 3D model like any other concept. In-browser focus stacking and touch-up were retired on 2026-09-25 — the Desktop Stacker is the only stacker.
  • Staff/admin extras — a d3 metrics dashboard (/admin/art-metrics), a live "Now Running" activity board, a visual Pipeline Builder node graph that saves as a workflow preset (/admin/art-builder), preset management (/admin/art-presets), and a usage meter.

Two adjacent GPU lanes are not Art Studio stages but ride the same worker fleet: QR Art (api/qrart — stylized QR code generation, classic SD1.5 QR Monster v2 and sdxl variants; the anonymous client-side QR tool stays anonymous, this authenticated lane adds the AI stylization) and photo auto-tagging (Florence-2 tags gallery photographs; see Photography & Gallery).

Guardrails (students are minors)

  • Prompt moderation: blocklist screening, 500-char cap, URL/PII rejection (Business/Domain/ArtStudio/Services/Guardrails/).
  • Per-student generation quotas and a full audit log (art-studio-audit collection).
  • AllowStudentMethodChoice defaults false — model/method choice is staff-first; students get the curated defaults.
  • Fail-closed safety gate: every output image is classified (resident SafetyCheck model, ~20 ms/check via RPC). Flagged artifacts move to a quarantine bucket with staff-only download; generation parameters pass through an allow-list + clamp policy (ArtStudioGenerationParamPolicy).
  • No anonymous gallery; all sharing is authenticated and moderated.

Pipeline architecture (spikersoft-artpipe)

The Python side is three processes over HTTP (see spikersoft-artpipe/CLAUDE.md for the deep-dive):

Process Role
ArtPipe server (src/artpipe/server.py, :9100) Stdlib-only dispatcher; discovers model backends by their models/*/artpipe.json manifests; never imports model code
Batch orchestrator (src/artpipe/batch/, Flask + SQLite, :5000) Multi-asset batch/curation workflows
Blender addon (src/addons/artpipe/) In-Blender integration
  • Worker protocol: one JSON object on stdin, newline-delimited JSON events (started|progress|result|error) on stdout; model stdout is redirected to stderr so prints can't corrupt the protocol.
  • Stages are independent modules (src/artpipe/batch/stages/): concept, modeling, texturing, rigging, animation, export, enrichment. Stage plans vary by asset type: character = all 7; prop/equipment skip animation; environment skips rigging + animation.
  • Rigging fallback chain: UniRig → RigNet → Mesh2Rig → smart_rig.
  • Enrichment normalizes meshes, renders thumbnails / MP4/GIF animation previews / multi-view screenshots, and runs CV analysis (symmetry, silhouette, form, color auto-tags).
  • Blender daemon pool: persistent headless Blender HTTP servers (ports 19200+) with session leasing, auto-recycle after 50 batches, and stale-lease reclamation.
  • Each model backend lives in its own venv with an artpipe.json manifest as the contract. Current manifest inventory spans text-to-image (SDXL Lightning/Turbo, Flux Schnell), image editing (FLUX.2 Klein, Qwen-Image-Edit), image→3D (TripoSR, TripoSG, SF3D, InstantMesh, Pixal3D, Hunyuan3D, Hunyuan3D-Omni, Trellis-mac), text→3D (Shap-E, Hunyuan3D), texturing (Hunyuan3D-Paint 2.0/2.1, SD-Turbo-Tex), rigging (UniRig, RigNet), motion (MDM), pose (OpenPose), QR stylization (QR Monster), tagging (Florence-2), and safety (safety_check).

Backend orchestration (SpikerSoft.EventHandlers.ArtPipeProcessor)

The .NET GPU worker that shells out to art_pipe. Three mutually exclusive execution modes (ArtPipeConfig):

Mode Config Behavior
Per-stage ArtPipe:Stages Consumes art.asset.stage.requested.<stage>; per-task GPU lease (acquire → load → run → unload → release)
Resident ArtPipe:ResidentModel Keeps one model in VRAM for the container lifetime; consumes art.model.<modeldir>.tasks; recycles after N jobs (e.g. SDXLLightning 500)
Model-queue ArtPipe:ModelQueue Same per-model queue, but a short-lived subprocess + per-job lease — an idle container holds zero VRAM. This is the production doctrine

Routing is API-side via ArtStudio:StageModelMap (production defaults: modeling → Pixal3D, texturing → Hunyuan3DPaint21) with worker-side ArtPipe:PublishToModelQueues. Resilience pieces: a stage-run watchdog, capped failover down the per-stage method list (ArtStudioStageFailoverOptions — PinnedOnly methods like the edit models and material variation are excluded from failover chains and generation pickers), and display-derivative generation (400 px AVIF thumbnail + display AVIF via the shared cavif wrapper). A separate single-replica ArtStudioMetrics handler folds lifecycle events into the metrics read model.

GPU coordinator & VRAM leases

SpikerSoft.EventHandlers.GpuCoordinator is a cluster-wide VRAM lease broker: every GPU-bound worker (artpipe stages, image description, embeddings, quiz generation) must acquire a lease before touching a CUDA context.

  • RabbitMQ lanes: gpu.lease.requests / .releases / .task-complete / .queue-status / .keepalive, plus per-node heartbeats.
  • VRAM-aware concurrent grants with model-affinity scheduling; queue-position publishing feeds the UI's "Nth in line".
  • A durable Redis ledger reloads booked VRAM + active leases on restart so a coordinator bounce can't double-grant into GPUs workers still hold.
  • Deliberately replicas: 1 (in-memory tracker is authoritative), pinned to node SERVER, HTTP /health + /gpu/status on port 8090 (not exposed through Traefik).
  • Two permanent GPU lanes: the 4090 node (RTX 4090, 24,576 MB budget, permanent holder of the artpipe-gpu placement label — runs the artpipe model stacks and image description) and SERVER (RTX 3070 Ti, 8,192 MB — runs embeddings and quiz generation). Lease requests carry requiredNodeId so a grant is budgeted against the requester's own card.

Per-model container lanes (spikersoft-infrastructure)

Each model runs as its own Swarm stack with fully baked weights (HF_HUB_OFFLINE=1; two-tier image build artpipe-base → artpipe-model-env-<model> → artpipe-model-<model>). Placement rides the movable node.labels.artpipe-gpu == true label; restart policy is condition: any (a clean exit-0 on broker loss must not wedge a lane at zero consumers).

Stack (spikersoft-artpipe-…) Stage / role VRAM (MB)
modeling (prodstages monolith) all 7 stages serialized, per-task leases per-stage
-model-sdxl concept (SDXL Lightning, resident) — currently stood down (replicas 0) —
-model-flux2klein concept edit_image (FLUX.2 Klein) 13,000
-model-qwenedit concept edit_image (Qwen-Image-Edit; also needs ~15 GB host RAM for the offloaded text encoder) 16,000
-model-triposr modeling image_to_3d 3,000
-model-sf3d modeling image_to_3d 6,000
-model-shape modeling text_to_3d (Shap-E) 6,000
-model-instantmesh modeling image_to_3d 8,000
-model-triposg modeling image_to_3d 8,000
-model-hunyuanomni modeling image_to_3d with bbox proportion control (Hunyuan3D-Omni) 11,000
-model-textto3d modeling (Hunyuan3D text/image→3D) 14,000
-model-pixal3d modeling image_to_3d, PBR output — production default 18,000
-model-hunyuan texturing texture_mesh (Hunyuan3D-Paint 2.0, diffuse) 14,000
-model-hunyuan21 texturing texture_mesh (Hunyuan3D-Paint 2.1, full PBR maps) — production default 21,000
-model-blender resident Blender (rig/animate/export/enrich) ~2,048
-model-safety resident SafetyCheck gate ~1,024
-model-qrmonster QR Art lane (on-demand, replicas 0 when idle) 9,500
-model-florence2 photo auto-tag lane (pinned to SERVER; replicas 0 when idle) 2,048

VRAM figures are lease sizes (torch reserved, not allocated — measured on live runs, not taken from upstream quotes).

Storage & data

  • MinIO buckets: art-asset-artifacts (all pipeline artifacts — concept images, meshes, PBR texture maps, rigs, animations, exports, AVIF derivatives) and art-asset-quarantine (safety-flagged, staff-only). Artifacts are referenced by MinIO-native object keys — not GridFS.
  • Mongo collections: art-assets (owner, type, prompt, stage plan, artifacts + selections, safety flags, gallery status, remix/variant/group/photograph provenance, method selections + provenance, ratings), art-asset-stage-runs (per-run state/timings/retries), art-asset-groups, art-variant-upload-jobs (ClamAV-scanned user uploads), qr-art-jobs, art-studio-audit, art-workflow-presets, game-art-loadouts, art-studio-metrics.
  • Artifact kinds are extension-aware (ArtifactKinds.cs): images png|jpeg|webp|tiff, meshes glb|gltf|obj|fbx|ply|stl|usdz, video mp4|webm; texturing images classify into a PBR map taxonomy. The only wired export action today is export_glb (Blender) — the wider mesh-format list is the classifier's vocabulary, not a user-facing export menu.
  • Tracing propagates end-to-end: the API's stage-request publish carries trace context through the .NET worker into the Python subprocess (JAEGER_ENDPOINT), so one Jaeger trace covers submit → GPU job → artifact upload.

Asset-to-Game Integration

Finished Art Studio assets don't stop at downloads — they skin the platform's games.

The handoff contract

GET api/artstudio/{id}/manifest (and gallery/{id}/manifest for gallery assets) returns a games manifest: root-relative artifact download routes, rig info, attribution, and provenance. The Angular side consumes it through art-asset-game-loader.service.ts, which downloads the game-ready GLB, parses it with GLTFLoader, and normalizes it — the returned container Group is scaled so its largest dimension is 1 world unit, XZ-centered, and grounded at y=0, so any game can place it with a single uniform scale + translation. A budget backstop (ART_ASSET_MODEL_BUDGET: 192 MB / 2,000,000 triangles) protects the render loop, and blobs are cached in memory + the Cache API.

Per-game loadouts

api/game/{gameId}/art-loadout (GameArtLoadoutController, Mongo game-art-loadouts) persists which asset fills which slot per game. The slot registry (GameArtSlotRegistry.cs):

Game Slots
chess 12 — piece:{pawn,rook,knight,bishop,queen,king} for player 1 and opponent:* for player 2
hex-tower-defence 9 — building:{command-center, matter-mine, solar-power-plant, laser-tower, barricade, build-slot}, enemy:{normal, gargantuan, zerg}
  • Chess (art-studio-chess-piece-model-provider.ts) diffs slot descriptors, loads sequentially against an aggregate byte budget, hot-swaps piece prototypes before disposing retired ones, and falls back to the bundled classic mesh on tooLarge/loadFailed. Custom meshes are never tinted — each side is its own set.
  • Chess piece sets are a first-class domain: CreateChessPieceSetCommandHandler fans one theme into six per-piece prompts (ChessPieceSetPrompts.cs — composition constraints lead, theme last, shared negative prompt), pieces are modeled (production default Pixal3D), and completed sets can be shared to the gallery, moderated, and equipped.
  • Hex Tower Defence uses a per-slot art-asset-picker with lazy import("@spikersoft/feature-art-studio"), explicit disposal per slot, and a tooHeavy toast.
  • The voxel game is not yet connected to the Art Studio path, and the older 3d-models collection (ThreeDModelController) is a read-only legacy surface — new modeling goes through the Art Studio.

Coding Curriculum & Playground (Eight Tracks)

SpikerSoft's flagship coding-education tools run eight parallel language tracks — C#, Python, JavaScript, Regex, C, C++, SQL, and x86 assembly (LessonCatalogService.listByLanguage() accepts exactly that set). The original four are documented in depth here; the four newer tracks are covered in C, C++, SQL & x86 tracks below.

  • C# Playground — 102-lesson curriculum graded by Roslyn server-side and .NET WebAssembly in the browser.
  • Python Playground — ~101-lesson curriculum graded by CPython subprocess server-side and Pyodide in the browser.
  • JavaScript Playground — 138-lesson curriculum (10 orientation tutorials + 128 graded challenges across 17 tiers) graded by Node server-side and QuickJS-WASM in the browser. Same harness contract as Python (runTests() returning PASS:/FAIL:/ERROR: strings); offline + Run-locally + batch re-verification all work the same way. Every challenge ships with a reference solution that's executed end-to-end through Node by JavaScriptChallenge_ReferenceSolutionPasses.
  • Regex Playground — 12-chapter curriculum graded fully in-browser against a pre-computed RegexLessonPlan and re-verified server-side through Node (not .NET) to preserve JavaScript regex flavor. See Regex Playground & Curriculum below.

All three tracks share the same lesson UI (the language-agnostic LanguageRunner Angular component), the same attempt/progress APIs, the same PASS/FAIL line protocol, and the same offline-first PWA model. A student can start a lesson at home, finish it on a campus bus with no signal, and see their unlocks update the moment connectivity returns. The JavaScript Playground is reachable at /tools/(tools:javascript-playground), Python at /tools/(tools:python-playground), and C# at /tools/(tools:csharp-playground).

Backend dispatch is strategy-based: ILessonGradingExecutorFactory picks Roslyn / CPython / Node per LessonGradingRuntime, IFreePlayCodeExecutorFactory picks the per-language free-play executor from request metadata, and IStudentCodeHintAnalyzerFactory returns the language-appropriate hint analyzer (StudentCodeHintAnalyzer for C#, PythonStudentCodeHintAnalyzer for Python, JavaScriptStudentCodeHintAnalyzer for JavaScript). A future TypeScript curriculum plugs into the same JS runtime via the new LessonMetadata.Preprocessor = LessonPreprocessor.TypeScript flag — no second runtime needed.

Python curriculum tiers

Python lessons live under SpikerSoft.Business.CodeExecution/Domain/Lessons/Curriculum/Python/ and mirror the C# tier structure plus a Python-only TierP1_Pythonic for idioms that have no C# parallel:

Tier Numbers Topic
Tier00_Welcome 20001-20007 Welcome tutorials (intro, modules, functions, print, first submit, how the test harness works)
Tier01_Foundations 20100-20107 Hello World, variables (int, float, bool, str, None)
Tier02_Operators 20200-20206 Arithmetic, comparison, logical, conditional expression, f-strings
Tier03_ControlFlow 20300-20307 if / elif / else, dict dispatch, for / while / iteration (match/case is now introduced in Tier 13 patterns)
Tier04_Functions 20400-20405 return, multi-param, keyword args, defaults, lambda
Tier05_Collections 20500-20506 list, append, iteration, dict, set, nested lists, tuple
Tier06_Strings 20600-20605 slicing, split/join, replace/case, ''.join accumulation, isdigit() validation, f-strings (canonical)
Tier07_Classes 20699-20707 decorator-syntax recipe, class, __init__, methods, @property, @staticmethod/@classmethod, __repr__, _private, self
Tier08_Inheritance 20800-20805 base/derived, override, abc.ABC, duck typing, multiple inheritance, super()
Tier09_Generics 20900-20902 TypeVar, generic class (Stack[T]), bounded generics
Tier10_Exceptions 21000-21003 try/except, finally, custom exception, context manager (with)
Tier11_FunctionalAndComprehensions 21100-21105 filter / map list comps, sorted(key=), group-by, any/all/next, generator expressions
Tier12_Callables 21200-21203 callable, lambda + filter, closures, decorators
Tier13_Patterns 21300-21302 tuple patterns, class patterns, @dataclass(frozen=True)
Tier14_OptionalAndNone 21400-21402 None as sentinel, or defaults, typing.Optional
Tier15_Async 21500-21503 coroutines, async/await, asyncio.gather, cancellation
Tier16_Advanced 21600-21604 *args/**kwargs, generators (yield), tuple unpacking, __getitem__, dunder operator overloading
TierP1_Pythonic 21700-21704 f-string format spec, dict / set comprehensions, enumerate+zip, advanced slicing

JavaScript curriculum tiers

JavaScript lessons live under SpikerSoft.Business.CodeExecution/Domain/Lessons/Curriculum/JavaScript/ and use the 30000–31999 lesson-number range with DisplayNumber = LessonNumber - 30000 so the sidebar shows clean 1..N badges. See .cursor/rules/javascript-curriculum.mdc for the locals-first / Node↔QuickJS parity rules:

Tier Numbers Topic
Tier00_Welcome 30001-30010 Welcome tutorials (history of JS, engines, browser vs Node, console basics, code style, reading errors, first submit, how the test harness works, function recipe)
Tier01_Foundations 30100-30109 console.log, numbers, strings, booleans, null/undefined, typeof, let vs const, template literals (preview), multi-line
Tier02_Operators 30200-30207 Arithmetic, ===/!==, logical, ternary, modulo, ++/--, compound assignment, concat-vs-template
Tier03_ControlFlow 30300-30308 if/else, switch, for, for-of, for-in, while, do-while, break/continue
Tier04_Functions 30400-30409 Declaration vs expression, arrow, defaults, rest, spread, early return, purity, closures, hoisting, higher-order
Tier05_Arrays 30500-30508 literal/index, push/pop, length, slice/splice, concat/spread, indexOf/includes, sort, join/split, Array.from/of
Tier06_Strings 30600-30608 length/index, case, slice, includes, replaceAll, split, trim, padStart, template literals (canonical)
Tier07_Objects 30700-30708 literal, dot/bracket, mutation, keys/values/entries, destructuring, shorthand, spread/assign, JSON, nested
Tier08_Iteration 30800-30808 forEach, map, filter, reduce, find, some/every, flat/flatMap, chaining, sort comparator
Tier09_Classes 30900-30908 class, ctor, methods, getters/setters, static, this rules, arrow this, bind/call/apply, #private
Tier10_Inheritance 31000-31005 extends/super, override, super in methods, instanceof, mixins, Object.create
Tier11_Modules 31099-31104 ESM export/import recipe, IIFE module, Symbol.iterator, Symbol keys, Object.freeze, namespace export
Tier12_Errors 31200-31204 throw/catch, finally, custom Error, name+message, re-throw (errors-in-async moved to Tier 13)
Tier13_Async 31300-31310 callbacks, Promise basics, .then chains, resolve/reject, async, await, all/race/allSettled, sequential vs parallel, errors in async functions
Tier14_Modern 31400-31407 optional chaining, nullish coalescing, Map, Set, WeakMap, generators, for-of generator, generator iterator
Tier15_Patterns 31500-31504 revealing module, observer (event bus), factory, strategy, singleton
Tier16_Performance 31600-31604 memoization, debounce, throttle, avoiding O(n²), lazy generators

Pedagogical contract: no concept used before introduction

Every challenge lesson is reachable from "Hello, World" using only language features that were formally introduced (and graded) in an earlier lesson. A student who works the curriculum top-to-bottom never has to leave the platform to look up a syntactic form they have not been taught — if the reference solution uses an f-string, decorator, optional chain, or generator expression, an earlier lesson exists that puts that exact construct in front of them as a "recipe to copy". Reference solutions for every gradable challenge are checked in alongside the lesson and executed end-to-end on every CI run (the ReferenceSolutionPasses tests, collected by the SpikerSoft.UnitTests.slnf solution filter), so a contract violation surfaces as a red test, not as a confused student.

The audit findings that drove the most recent Python and JavaScript reorderings are living documents in docs/curriculum/python-curriculum-audit.md and docs/curriculum/javascript-curriculum-audit.md. Re-run the same audit when adding a new tier; the verification gate is dotnet test SpikerSoft.UnitTests.slnf --filter "ReferenceSolutionPasses".

Regex Playground & Curriculum

The fourth language track is a 12-chapter regular-expressions curriculum under SpikerSoft.Business.CodeExecution/Domain/Lessons/Curriculum/Regex/ (Chapter01_Literals → Chapter12_Recipes, covering literals, special characters, character classes, quantifiers, anchors, alternation, capturing groups, back-references, replace, lookarounds, named groups + Unicode property escapes, and idiomatic recipes). Lessons are written as RegexLessonStrategyBase subclasses that hand the SPA a serialized RegexLessonPlan — test text plus expected matches/replacements row-by-row.

Grading mechanics differ deliberately from the C# / Python / JavaScript tracks:

  • Live grading runs entirely in the browser. RegexLessonRunnerService and RegexLessonGraderService (in libraries/features/dev-tools-reg-ex) feed the student's pattern through the browser's native RegExp against the plan's test text and emit row-level pass/fail just like the JS / Python harnesses do.
  • Server re-verification routes through Node, not .NET. When an offline-graded regex submission flushes through POST /api/Lessons/progress/batch, RegexLessonGradingExecutor builds a JS harness via RegexLessonHarnessBuilder and runs it on the worker's Node subprocess (the same JavaScriptLessonExecutor plumbing the JS Playground uses). This is not a casual choice — JavaScript regex differs from .NET's System.Text.RegularExpressions in ways that matter for the curriculum: the v-flag set operations [[a-z]--[aeiou]] / &&, JS-only replacement patterns $` / $', and the supported \p{...} Unicode property names all diverge between engines. Re-grading through Node guarantees the server records the same pass/fail the student saw locally.
  • No second runtime in the browser. Regex lessons need no Pyodide, no QuickJS, no .NET WASM — the SPA already has RegExp. Offline support is automatic.

Server-side regex re-grading rides the Offline Lesson Re-Grading via Worker RPC pipeline (below), so a code-runner crash does not take the API down. SPA-side details and the in-app "Lesson" pane / cheatsheet UI live in libraries/features/dev-tools-reg-ex/README.md.

WASM exclude. SpikerSoft.Wasm.csproj excludes Regex*.cs from the link-included executor sources — regex re-grading is server-only by design.

C, C++, SQL & x86 tracks

Four further curricula live under SpikerSoft.Business.CodeExecution/Domain/Lessons/Curriculum/{C,Cpp,Sql,X86}/:

Track Size Browser runtime Playground route
C 11 chapters browser Clang toolchain (see below) /tools/(tools:c-playground)
C++ 11 chapters browser Clang toolchain /tools/(tools:cpp-playground)
SQL 24 chapters DuckDB-WASM /tools/(tools:sql-playground)
x86 assembly 13 chapters Blink WASM emulator /tools/(tools:x86-playground)
  • Browser Clang toolchain (libraries/platform/clang-runtime/): a worker-hosted Clang + LLD + WASI-libc + libc++ build (browsercc, LLVM 20.1.2) paired with @bjorn3/browser_wasi_shim — full C89→C23 and C++98→C++23 entirely client-side, a sibling to the Roslyn-WASM / Pyodide / QuickJS runtimes. Server-side grading routes through CCodeRunnerController / CppCodeRunnerController / SqlCodeRunnerController.
  • x86 playground (libraries/features/dev-tools-x86-playground/): the Blink-based emulator with a GDB-like register/memory/disassembly debugger; supports GNU as, FASM, and NASM syntax — all client-side. (This was a TODO item; it shipped.)
  • Pattern courses also exist per language at /learn/coding/patterns/{c,cpp,sql,x86}.

Git playground & software-practice tracks

Version-control and testing are curricula too: /learn/coding/version-control/git (lessons) and /learn/coding/version-control/git/playground (a real Gitea-backed sandbox — credentials stay server-side behind a broker, GitPlaygroundController), plus unit / integration / e2e testing tracks.

Playground visual progress trail

The former standalone "My Journey" page is now the Progress tab inside each playground's curriculum pane (libraries/platform/playground-visual-progress/): a per-language winding trail of lesson stops with a daily streak, a celebratory sound on completion, and a reduced-motion variant. Opt-in, remembered per playground.

Content locale

libraries/platform/content-locale/ is the single reactive source of truth for the language that server-localized content arrives in (lesson catalog, journey trail, curriculum chapters, geography facts, attempt instructions). Any surface sending contentLocale follows its documented two-obligation contract so UI locale and content locale can't drift apart.

Dual execution

Path Where When Latency
Server (default) SpikerSoft.EventHandlers.CodeExecution Docker worker Online and the user has not enabled "Run locally" ~150-400 ms (RabbitMQ round-trip + Roslyn / CPython / Node subprocess)
Browser (Roslyn WASM) SpikerSoft.Wasm runtime in a Web Worker C# only; offline OR user toggled "In-browser compile" ~30-60 ms warm; first boot 8-15 s
Browser (Pyodide) /assets/vendor/pyodide/ loaded by PyodideRuntimeService Python only; offline OR user toggled "Run locally (Pyodide)" ~50-150 ms warm; first download ~10 MB
Browser (QuickJS) /assets/vendor/quickjs/quickjs.global.js loaded by QuickJsWorkerClient JavaScript only; offline OR user toggled "Run locally (QuickJS)" ~5-30 ms warm; first download ~2.3 MB (single-file UMD with WASM inlined)

Frontend state (runtime downloads cached by service worker, lesson catalog cached in IndexedDB, queued progress flushed on reconnect) means once a student loads a lesson online they can keep working offline indefinitely.

Worker container Python + Node layer

The worker image installs python3 (Debian default) and a Node.js 20.x LTS via the official NodeSource setup script (Debian's bundled nodejs package is too old for some lesson features). Both interpreter paths are pinned explicitly:

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl ca-certificates gnupg python3 \
    && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y --no-install-recommends nodejs \
    && rm -rf /var/lib/apt/lists/* \
    && test -x /usr/bin/python3 && /usr/bin/python3 --version \
    && test -x /usr/bin/node    && /usr/bin/node --version
ENV CodeExecution__PythonExecutable=/usr/bin/python3
ENV CodeExecution__NodeExecutable=/usr/bin/node

Two startup probes (PythonInterpreterProbeHostedService and NodeInterpreterProbeHostedService) each run --version once and log the resolved path so a misconfigured deploy fails loudly. Pin worker images away from :latest so Swarm picks up new layers reliably (the Python + Node installs live in the runtime stage of the Dockerfile).

WASM coupling note. SpikerSoft.Wasm.csproj link-includes Domain/CodeExecution/Execution/**/*.cs from SpikerSoft.Business, but excludes server-only files via a list (Python*, JavaScript*, Regex*, executor/factory abstractions, RoslynLessonGradingExecutor). When you add a new server-only executor, add its file pattern to that exclude list — otherwise the WASM build pulls in IOptions / ILogger / Microsoft.Extensions.Services that the WASM project deliberately doesn't reference, and you'll see a wall of "type or namespace not found" errors.

Offline Lesson Re-Grading via Worker RPC

Browser graders (SpikerSoft.Wasm for C#, Pyodide for Python, QuickJS for JavaScript, the SPA's native RegExp for regex) are convenient and work entirely offline, but they run on the student's machine and are tamperable. Before the platform credits a skill unlock or marks a lesson complete for-real, the server has to re-execute the same lesson against the same grader and confirm it actually passed. That's the trust boundary the offline IndexedDB queue crosses every time it flushes through POST /api/Lessons/progress/batch.

The architectural rule: code execution NEVER runs in the API process. If the code-runner container crashes mid-grade, the API stays up; a fresh client retry round-trips through Rabbit on the next batch flush. This is enforced by the API's DI graph — only ILessonRegradeClient is registered there; no ILessonGradingExecutor implementation is reachable from SpikerSoft.Api.

The flow: LessonsController.BatchProgress builds a LessonRegradeRequest for each queued submission and hands it to LessonRegradeClient, which publishes it to the lesson.regrade.requests exchange and blocks on the worker's reply via RabbitMQ Direct-Reply-To (amq.rabbitmq.reply-to). The worker's LessonRegradeWorkerHostedService consumes the request, dispatches through the same ILessonGradingExecutorFactory that backs the live "Submit" path (Roslyn / CPython / Node / Regex-via-Node — no code duplication between live-submit and regrade), and publishes a LessonRegradeResponse back to the API's reply queue.

sequenceDiagram
    autonumber
    participant SPA as Angular SPA
    participant API as SpikerSoft.Api
    participant MQ as RabbitMQ
    participant Worker as SpikerSoft.EventHandlers.CodeExecution
    Note over SPA: Student grades offline (in-browser)<br/>Submission queued in IndexedDB
    SPA->>API: POST /api/Lessons/progress/batch
    Note over API: For each queued attempt
    API->>API: Build LessonRegradeRequest<br/>(lessonNumber, attemptToken,<br/>studentCode, testCode, runtime)
    API->>MQ: publish to lesson.regrade.requests<br/>reply-to = amq.rabbitmq.reply-to
    MQ->>Worker: deliver request
    Note over Worker: ILessonGradingExecutorFactory<br/>picks Roslyn / CPython / Node / Regex
    Worker->>Worker: Execute & grade
    Worker->>MQ: publish LessonRegradeResponse<br/>to API's Direct-Reply queue
    MQ->>API: deliver response
    alt Response received within timeout
        API->>API: Persist progress if AllTestsPassed
        API->>SPA: 200 OK with per-attempt status
    else Timeout (worker down / overloaded)
        API->>SPA: 5xx server_error<br/>(IndexedDB keeps the attempt; SPA retries)
    end

Idempotency. Each request carries the SPA-generated attemptToken. If the worker grades a token, the API records it; if a network blip causes the SPA to retry, the controller short-circuits on the duplicate token rather than re-publishing — so the worker never regrades the same attempt twice.

Timeout handling. LessonRegradeClient enforces a per-call timeout (default 30 s). On expiry, the call throws TimeoutException, the controller maps it to HTTP server_error, and the SPA leaves the offline attempt in IndexedDB so the next batch flush picks it up. The same attempt token is re-used so the worker still de-dupes correctly when it eventually catches up.

Same factory, same code. Both worker consumer paths (live-submit code.execution.requests and offline-regrade lesson.regrade.requests) dispatch through ILessonGradingExecutorFactory. There is no separate "regrade" code path that could drift from the live grader — fixing a hint in RoslynLessonGradingExecutor fixes both modes simultaneously.


C# Coding Curriculum & Playground

The C# Playground is SpikerSoft's flagship coding-education tool — a 102-lesson curriculum that teaches C# from "Hello, World" through async/await, LINQ, and operator overloading. It runs both online (server-graded) and offline (browser-graded) with progress preserved across the boundary, so a student can start a lesson at home, finish it on a campus bus with no signal, and see their unlocks update the moment connectivity returns.

Curriculum Structure

102 lessons grouped into 17 progressive tiers. Each lesson is gated behind explicit prerequisites and unlocks the next as it is completed:

Tier Theme Sample Topics
Tier00_Welcome Orientation Hello World, Anatomy of a Class, Anatomy of a Method
Tier01_Foundations Variables int, double, bool, string, var
Tier02_Operators Math & comparison Arithmetic, comparison, logical, string interpolation
Tier03_ControlFlow Branching & loops if/else, switch expressions, for/while/foreach
Tier04_Methods Functions void/return, multi-param, out/ref params
Tier05_Collections Data structures Arrays, List<T>, Dictionary<K,V>, 2D arrays
Tier06_Strings Text manipulation String methods, StringBuilder, TryParse
Tier07_Classes OOP basics Class definition, constructors, properties, validation
Tier08_Inheritance Polymorphism base/derived, virtual/override, single & multiple interfaces
Tier09_Generics Type parameters Generic methods, generic classes, constraints
Tier10_Exceptions Error handling try/catch/finally, using / IDisposable
Tier11_Linq Querying Select, OrderBy, Any/All/First, query syntax
Tier12_Delegates Function references delegate, Action/Func, lambdas
Tier13_Patterns Modern C# Pattern matching, records
Tier14_Nullability Null safety Nullable reference types
Tier15_Async Concurrency async/await
Tier16_Advanced Power features params arrays, operator overloading

Each lesson is a server-side LessonStrategyBase C# class that owns:

  • StarterCode — what the student sees on first visit
  • TutorialPanels — interactive, narrated walk-throughs presented before the gradable challenge
  • TestCode — the grader (executes student code + asserts on output, exit code, exceptions)
  • Prerequisites — the lesson numbers that must be completed first (drives the sidebar lock state)
  • Hints — heuristic checks that surface common errors before Roslyn even runs

Lesson definitions are the single source of truth. A LessonCatalogHydrationService reads them on backend boot and writes the catalog to MongoDB's lessons collection. The same .cs files are link-included by the SpikerSoft.Wasm project so the browser grades with byte-identical logic.

Two Execution Paths, One UX

Path Where code runs When it's used Latency
Server (default) SpikerSoft.EventHandlers.CodeExecution Docker worker, 4 concurrent consumers reading from code.execution.requests, results published to code.execution.responses Online and the user has not enabled "Run compiles locally" ~150–400 ms (RabbitMQ round-trip + Roslyn)
Browser (WASM) SpikerSoft.Wasm runtime hosted in a Web Worker, Roslyn 5.x compiled and run in-browser Offline, or the user toggled "Run compiles locally" on, or the server is unreachable ~30–60 ms warm; first boot 8–15 s

CSharpRunnerService picks a path automatically and re-attempts on the alternate path if the chosen one fails — for example, a momentary network blip during a server submit silently re-grades against the WASM runtime so the student never sees an error.

Browser-Side Grader (SpikerSoft.Wasm)

A .NET 10 Microsoft.NET.Sdk.WebAssembly project that ships Roslyn + the lesson strategies + the same RoslynCodeExecutor the server uses, exposed to JavaScript via [JSExport]:

Export Purpose
Ping() Readiness probe
GetAttempt(lessonNumber) Issues a single-use compile token (mirrors the server-side Redis token model, but in-memory)
ExecuteLessonCode(json) Compiles + runs + grades a lesson submission
ExecuteFreePlayCode(json) Compiles + runs free-form code (no grading, no token)
AddReferenceImage(byte[]) Fed by main.js after boot so Roslyn has framework PE bytes to compile against

Important runtime constraints baked into the executor:

  • WebCIL is disabled (<WasmEnableWebcil>false</WasmEnableWebcil>) so the same downloaded assembly bytes are valid PE images Roslyn can use as MetadataReference.CreateFromImage references — no unwrapping step required.
  • ConcurrentBuild is forced off under OperatingSystem.IsBrowser() because Mono's WASM runtime has no monitor wait support; Roslyn's parallel compile path deadlocks instantly without this guard.
  • Invariant globalization drops ICU (~5 MB saving). Lesson TestCode uses ordinal comparisons.

Bundle stats: ~7 MB Brotli-compressed, one-time download, then cached by the Angular service worker (lazy strategy). Visitors who never enable the toggle pay zero download cost.

Lesson Sidebar & Progress

The LessonSidebar Angular component renders the curriculum and computes lock/checkmark state client-side from two signals: lessons (the catalog) and completedLessons (the user's progress). A lesson unlocks when all of its Prerequisites are present in the completed set.

Progress reconciliation strategy (added to fix a real-world stale-state bug):

  • Optimistic on submit — completedLessonsSignal updates the moment a lesson grades green, so the next lesson unlocks without waiting for the server round-trip.
  • Forced refresh after a passing submit — both catalog and progress are re-fetched with throttle bypassed, ensuring server-side prereq edits land immediately and the lesson catalog stays accurate.
  • Forced refresh after tutorial completion — same mechanism, so newly unlocked downstream lessons appear immediately when the student finishes the last tutorial panel.
  • Tab-focus refresh — when the browser tab regains focus (e.g., after a long offline session, or after switching back from another app), a throttled refresh runs (15 s minimum interval) so multi-device or multi-session progress catches up automatically without F5.
  • Union semantics — server progress and local optimistic updates are merged via Set union, never overwritten. This eliminates a race where a slow progressSync.enqueue round-trip could re-lock a just-completed lesson if the server refresh landed before the persist call did.
  • Service-worker freshness — /api/Lessons/progress uses the freshness strategy (network-first with 3 s timeout, cache fallback) so the very first paint after launching the app is always current rather than serving a 10-minute-old cached response.

Offline Mode

When the user disconnects or navigator.onLine === false:

  1. The runner auto-routes new submissions through the WASM grader.
  2. Successful completions are queued in IndexedDB by progress-sync.service.
  3. On reconnect, the queue flushes to POST /api/Lessons/progress/batch (batched items, each with lessonNumber + attemptToken).
  4. The next online catalog refresh confirms server-side persistence and merges any progress made on other devices.

Free-play code (anything outside a lesson) also runs against the WASM runtime when offline — students never lose access to the playground because the network blipped.

Pre-Flight WASM Cache (Site Settings → Offline)

The WASM bundles that power the playgrounds and games are downloaded lazily on first use. That's great for visitors who only ever read blogs, but it's a problem for the user about to board a flight who wants to keep grading Python lessons or playing the SQL Clue game without wifi.

The Site Settings page (under the user profile, accessible via the menu's "Site Settings" entry, replacing the older "Appearance" link) carries an Offline section that lets users explicitly preload every WASM-backed runtime ahead of time and verify, at a glance, which ones are already cached.

Module Ships with Used for
Pyodide /assets/vendor/pyodide/pyodide.asm.wasm + loader Python playground, Python lessons (offline grading)
.NET WASM main.js + _framework/dotnet.js + every hashed name in the embedded boot manifest (for example dotnet.native.*.wasm, Roslyn DLLs) C# playground, C# lessons (offline grading)
DuckDB /assets/duckdb-wasm/duckdb-mvp.wasm + worker SQL playground, Clue for SQL game
QuickJS /assets/vendor/quickjs/quickjs.global.js (single-file UMD with WASM inlined) + classic worker JavaScript playground, JavaScript lessons (offline grading); future TypeScript curriculum reuses this same runtime via LessonPreprocessor.TypeScript

Per-module UI:

  • Status badge — Ready offline, Partially cached, Not cached, Downloading…, Verifying…, or Error. Computed by hitting caches.match(url) for each primary asset; sidecar files (lockfiles, secondary worker variants) don't drag the status to "partial" if missing.

  • Preload button — Issues fetch(url, { cache: 'reload' }) for every asset, then cache.put into a dedicated named cache (spikersoft-offline-preload-v1) so status checks succeed even when the Angular service worker is off (typical ng serve). When the SW is on, entries exist in both places. Re-checks status on completion. Idempotent (the label switches to "Re-preload" for a forced refresh). For .NET, the URL list is not hard-coded: OfflineCacheService parses the JSON embedded in _framework/dotnet.js after each dotnet publish, so hashed _framework/* names stay in sync with the bundle on disk.

  • Preload everything — Sequential per-module so a slow connection isn't asked to download 50 MB at once; the user gets per-module progress feedback either way.

  • First use vs preload — Preload answers “are the bytes on disk?” The C# playground’s first in-browser run still starts the Web Worker and initializes Mono + WebAssembly (CPU work, not a second full download). That one-time cost often lands around one second even when every DLL was pre-cached; later runs stay in the tens of milliseconds. Turn Verify by initializing on for the .NET module if you want that startup to happen on the settings page instead of on first Run.

  • Verify by initializing — Optional advanced toggle (defaults off). When on, after each module's bytes finish downloading the section asks the runtime to actually boot and prove it runs in this browser:

    • Pyodide loads via PyodideRuntimeService.getPyodide() and round-trips 1+1 through runPythonAsync so any JS↔WASM bridge issue surfaces immediately. The runtime stays resident, so the next playground use is warm.
    • .NET WASM probes the bundle via WasmRuntimeService.isBundleAvailable() (gives a friendly "run dotnet publish" error when the bundle was never built) before booting the worker via ensureReady(). Same residency benefit as Pyodide.
    • DuckDB lazy-imports @duckdb/duckdb-wasm, instantiates a throwaway engine in a fresh worker, runs SELECT 1, then tears the worker down — no shared singleton to keep alive (each consumer of DuckDB owns its own AsyncDuckDB), so the verify path doesn't leak memory.

    Verifier registration happens lazily in WasmVerifierBootstrap.registerAll() when the panel renders, not at app startup — visitors who never open Site Settings don't pay any DI cost for the runtime services.

The registry (projects/spikersoft/src/app/_services/offline-cache/wasm-module-registry.ts) is the single source of truth for what shows up in this panel. Adding a new WASM-backed feature later means appending one WasmModuleDescriptor to DEFAULT_WASM_MODULES — no UI changes required. Wiring an initialize-time verifier for the new module is one extra line in WasmVerifierBootstrap.registerAll().

Tutorial Panels

Before the gradable challenge, each lesson can present any number of TutorialPanels — short, focused interactive segments authored in C# (so they live in source control alongside the lesson). The component records tutorial completion locally and forces a catalog refresh on the final panel so newly unlocked downstream lessons appear immediately.

Hint Analyzer

StudentCodeHintAnalyzer (in SpikerSoft.Business.CodeExecution/Domain/CodeExecution/Hints — with Python/JavaScript/SQL siblings picked by StudentCodeHintAnalyzerFactory) runs heuristic pattern checks on student source before it reaches Roslyn. Common mistakes (missing using, wrong return type, null-check inversions, off-by-one in for-loop bounds) surface as friendly hints instead of cryptic compiler errors. The same analyzer runs server- and browser-side because the file is link-included by SpikerSoft.Wasm.

Activity Tracking

Each meaningful interaction (lesson-attempt, lesson-complete, tutorial-complete, free-play-run, path-failover) emits an event to ActivityTrackingService, feeding the personalization layer (recommended next lessons, parent dashboards, learning streaks).

Building the WASM Bundle

The bundle is not committed to source control — spikersoft-angular/projects/spikersoft/src/assets/dotnet/ contains only its README. Build it locally with:

# from spikersoft-backend/
dotnet workload install wasm-experimental   # one-time
dotnet publish SpikerSoft.Wasm -c Release   # auto-flattens into ../spikersoft-angular/projects/spikersoft/src/assets/dotnet/

The publish target can be overridden: dotnet publish SpikerSoft.Wasm -c Release -p:AngularAssetsDotnet=/elsewhere/.

Rebuild whenever any of the following change:

  • A lesson strategy under SpikerSoft.Business.CodeExecution/Domain/Lessons/Curriculum/**
  • RoslynCodeExecutor or anything else under SpikerSoft.Business.CodeExecution/Domain/CodeExecution/Execution/
  • The hint analyzer
  • WasmCompilerEntry.cs or the [JSExport] surface
  • The Microsoft.CodeAnalysis.CSharp package version

In CI the bundle is published by spikersoft-backend/.gitea/workflows/spikersoft-wasm.yml to a Gitea Generic Package (git.spikersoft.com/spikerj/-/packages/generic/spikersoft-wasm) and downloaded by the frontend pipeline before pnpm run build, so the deployed Docker image always ships with a current bundle. The frontend can pin to a specific build by setting the WASM_VERSION repository variable; defaults to latest.

Gitea Actions — git exit 128 on arm64 only: The frontend workflow (.gitea/workflows/main.yml) runs a multi-arch Docker publish on ubuntu-amd64 and ubuntu-arm. If actions/checkout fails with exit code 128 on the ARM runner while AMD64 succeeds, the cause is usually runner configuration, not the app repo: (1) Wrong job image architecture — the act runner’s default job container must be an aarch64 image on ARM hosts; using an amd64-only image produces invalid ELF header when git-remote-https loads (see actions/checkout#417 and related runner-image reports). Fix by setting the job image in act runner config.yaml to a multi-arch or arm64v8 image, or use the host’s native arch. (2) Network — job containers that cannot reach git.spikersoft.com need container.network: host or the same Docker network as Gitea (Gitea issue discussion). (3) Missing git in the job image — checkout requires Git in PATH. The workflow also sets http.version to HTTP/1.1 and a larger http.postBuffer before clone to reduce flaky fetches. Seeing the real error: expand every collapsed log section in the UI and search for fatal: (the summary line is often generic). Re-run via Run workflow with input debug_git enabled to turn on GIT_TRACE and GIT_CURL_VERBOSE for that run. The Git binary diagnostics step prints uname/file for git and git-remote-https so a wrong CPU arch shows up as invalid ELF header before checkout.

Key Files

Layer File Purpose
Lesson definitions SpikerSoft.Business.CodeExecution/Domain/Lessons/Curriculum/Tier{NN}_*/Lesson*.cs All 102 C# lessons (one class per lesson)
Lesson base SpikerSoft.Business.CodeExecution/Domain/Lessons/LessonStrategyBase.cs Abstract base — StarterCode, TestCode, Prerequisites, TutorialPanels
Catalog hydration SpikerSoft.Api/Services/LessonCatalogHydrationService.cs Reads strategies on boot, upserts into Mongo lessons
Roslyn executor SpikerSoft.Business.CodeExecution/Domain/CodeExecution/Execution/RoslynCodeExecutor.cs Shared compile + run engine; sequential build under WASM
Hint analyzer SpikerSoft.Business.CodeExecution/Domain/CodeExecution/Hints/StudentCodeHintAnalyzer.cs Pre-Roslyn pattern checks (per-language siblings via StudentCodeHintAnalyzerFactory)
Server worker SpikerSoft.EventHandlers.CodeExecution/ RabbitMQ-driven sandboxed grader (4 consumers)
WASM project SpikerSoft.Wasm/SpikerSoft.Wasm.csproj .NET 10 WebAssembly app; publishes to assets/dotnet/
JS interop SpikerSoft.Wasm/WasmCompilerEntry.cs [JSExport] shim for the browser worker
Worker boot spikersoft-angular/projects/spikersoft/src/assets/dotnet/main.js Pre-fetches managed assemblies, feeds PE bytes to Roslyn via AddReferenceImage
Web Worker libraries/features/dev-tools-csharp-runner/src/lib/wasm-runtime.worker.ts Hosts the .NET runtime on a worker thread
Path-routing service libraries/features/dev-tools-csharp-runner/src/lib/csharp-runner.service.ts Server↔WASM routing with auto-failover
Language-agnostic shell libraries/platform/language-runner/src/lib/language-runner.ts Shared playground SHELL composed by all 3 wrapping shells (C#/Python/JS) via LANGUAGE_RUNNER_CONFIG + LANGUAGE_RUNTIME_ADAPTER
C# wrapping shell libraries/features/dev-tools-csharp-runner/src/lib/csharp-runner.ts Wraps the language-runner with the C#-specific LANGUAGE_RUNNER_CONFIG and csharp-runtime.adapter.ts
Lesson sidebar libraries/shared/lesson-panes/src/lib/lesson-sidebar/ Tier-grouped lesson list with prereq-driven lock state (shared across all 3 playgrounds)
Offline queue libraries/platform/progress-sync/src/lib/progress-sync.service.ts IndexedDB enqueue + flush-on-reconnect (consumed via PROGRESS_SYNC_PORT so the platform shell stays decoupled)
SW config projects/spikersoft/ngsw-config.json freshness for /api/Lessons/progress, lazy for /assets/dotnet/

API Endpoints

Method Endpoint Purpose
GET /api/Lessons Lesson catalog (number, title, tier, prereqs, tutorial panels)
GET /api/Lessons/progress Caller's completed lesson numbers
POST /api/Lessons/progress/batch Apply batched progress (offline WASM queue flush); idempotent per (userId, lessonNumber, attemptToken)
GET /api/Lessons/{lessonNumber}/attempt Lesson attempt payload + single-use compile token (server path)
POST /api/Lessons/{lessonNumber}/complete-tutorial Mark a tutorial lesson complete (LessonsController)
POST /api/CSharpCodeRunner/lesson Submit code for grading via the server worker
POST /api/CSharpCodeRunner/run Run arbitrary C# (no grading; free-play in the UI)

All endpoints require authentication. POST /api/CSharpCodeRunner/* and GET /api/Lessons/{n}/attempt are unused when the student runs entirely on the WASM path — Angular uses the in-browser runtime and batches progress via POST /api/Lessons/progress/batch when back online.


Native Mobile Apps (iOS & Android)

SpikerSoft ships native mobile clients — SwiftUI on iOS (spikersoft-ios), Jetpack Compose on Android (spikersoft-android) — as one App Store / Play Store listing each. The two apps are deliberate mirrors of each other, and both are API clients only: business logic stays server-side.

The per-repo READMEs still describe an early "Phase 1" scope; the source is the authority. Everything below ships today.

Launcher shell & app catalog

Instead of a tab bar, both apps present an OS-style launcher home: drag-to-reorder app icons, interest-based folders, and an "All Apps" screen. An onboarding wizard has the user rate areas of interest (≥3 required); the ratings seed the default folder layout, and later interest changes only ever append folders — user pins and deletions are never undone. Icon order and onboarding completion are persisted on the profile (Profile/launcher-layout), so both devices share one home layout.

The app catalog is a cross-platform contract: iOS Features/Home/AppCatalog.swift and Android features/home/AppCatalog.kt carry identical app ids that must also match the backend allowlist.

  • Shipping: books, quizzes, practice, lessons, knots, tracker, games
  • Coming soon (dimmed, non-navigating): geography, chemistry, reading-journey, our-stack, art-studio, gallery, photo-gallery, tools, sponsor, blog, contact

Feature surface

Feature Notes
Keycloak OIDC login Public PKCE clients spikersoft-ios / spikersoft-android via AppAuth; tokens in Keychain / EncryptedSharedPreferences. Redirect URIs differ by one slash: com.spikersoft.app://oauth2redirect (iOS) vs com.spikersoft.app:/oauth2redirect (Android)
Books + EPUB reader Shelf, server-side page API (Book/{id}/epub-page), progress sync (reader/progress)
"Listen" audiobook mode TTS narration of book pages with media-session/background playback. iOS is dual-engine: Kokoro neural TTS on-device (KokoroTTSEngine + a compact phonemizer) with Apple TTS fallback, chosen by TTSEngineRouter; Android uses a ListenService foreground service (android.speech.tts, MediaSession, audio focus)
Quizzes List / start / answer / complete against Quiz/* endpoints
C# lessons Catalog, tutorial panels, graded challenges, and a native code editor with syntax highlighting (CodeRunner/lesson for grading)
Practice Deck-based practice packs + UML diagram renderer + the Knots catalog (see Practice); packs download as versioned immutable MinIO zips, sha256+size-verified client-side
Activity tracker The biggest native feature — see Activity Tracker, Routes & Paths
Embedded Godot games See Godot Games Client
SignalR In-app notifications (hubs/notifications) and live code-execution results (codeExecutionHub)

Backend contract

Both platforms speak to the same production endpoints (there is no separate dev backend — local work means temporarily pointing at a LAN IP):

Service URL
REST API https://api.spikersoft.com/api
Keycloak https://ids.spikersoft.com, realm spikersoft
SignalR hubs https://api.spikersoft.com/hubs/notifications, /codeExecutionHub (host only, no /api)
Static assets (practice packs) https://api.spikersoft.com root
GameServer wss://gameserver.spikersoft.com (reached by the embedded Godot client, not native code)

Tech stacks & CI

  • iOS: SwiftUI, iOS 17+, Swift strict concurrency complete, XcodeGen-generated project. SPM: AppAuth, SignalR-Client-Swift, SwiftGodotKit, MapLibre 6.23+. MetalFX links device-only (it's absent from the simulator SDK). CI builds the unsigned device slice on the Mac runner on purpose — the simulator slice compiles Godot out entirely and wouldn't exercise the shipping SwiftGodotKit path.
  • Android: Kotlin, Compose Material 3, compileSdk/targetSdk 35, minSdk 26. Hilt, Navigation Compose, Retrofit + OkHttp + kotlinx.serialization, AppAuth, SignalR Java client, Coil (+ AVIF decoder), MapLibre 13.4.1, Room (schemas committed), WorkManager, org.godotengine:godot:4.7.1.stable. CI runs assembleDebug + Robolectric unit tests; both repos have Sonar scans.

Practice (flashcards, knots, UML)

The Practice product spans web and native: deck types + a session engine, per-language Concepts/Architecture decks, 22 GoF UML diagrams × 7 interest themes with precomputed layouts, and an illustrated knots catalog. The Angular lib (libraries/shared/practice-decks/) is the source of truth; iOS/Android carry generated vendored copies verified by a CI drift gate. Knot frame assets ship as versioned immutable MinIO packs (practice-packs/knots-v{N}.zip, ZIP_STORED, verified by sha256+size on device).


Activity Tracker, Routes & Paths

A GPS activity tracker for run / hike / bike / walk / other, shipping on web (/tracker), iOS, and Android, with recorded routes, derived analysis, and repeatable paths.

Privacy stance

Location data belongs to minors, so the rules are strict: recorded routes and paths are owner-scoped everywhere — requesting another user's id answers 404 (no existence leak), and both mobile recorders carry an explicit PRIVACY: never log coordinates rule. iOS records with When-In-Use authorization only (a CLBackgroundActivitySession keeps recording with the screen locked — no "Always" permission requested).

Maps

All three clients share the same map stack: six raster base maps (osm-standard, osm-humanitarian, cartodb-light, cartodb-dark, esri-satellite, esri-topo) + terrain-DEM hillshade + a red GPS path line. The web defines it in map.service.ts; the mobile apps bundle a style JSON mirroring it (MapLibre Native). Terrain elevation is served as PMTiles from MinIO (map-tiles/terrain.pmtiles) over HTTP range requests — the old tileserver-gl service was retired 2026-08-13 once all three clients had cut over.

Recording (mobile)

  • iOS (Core/Tracking/ActivityRecorder.swift): the iOS 17 CLLocationUpdate.liveUpdates(.fitness) async loop. Sampling gate: ≥2 s since last point AND (first-of-segment OR moved ≥5 m); fixes with horizontal accuracy >50 m are dropped.
  • Android (core/tracking/RecordingService.kt): a foreground LifecycleService (FOREGROUND_SERVICE_LOCATION) backed by Room.
  • Ghost racing: race a prior attempt with live gap computation and spoken auto-announcements (off / every minute / every km), optionally triggered by the volume buttons.
  • Return-to-start: detects a dwell back at the start point and fires a chime + spoken prompt + actionable notification — deliberately never auto-stops (it feeds on raw fixes, because the sampling gate starves while stationary).
  • Listen-while-recording: a companion sheet offers resume-a-book / pick-a-book / device music; audio cues duck the narration.
  • Two-way sync: uploads pending recordings, flushes delete tombstones, then pulls down routes the device lacks — so a reinstall doesn't show an empty history.

Routes, analysis, trimming

  • Backend: api/recorded-routes (CRUD + /analysis + /trim) under SpikerSoft.Api/Domain/Map/RecordedRoutes/, plus a trails corpus (by-state / near / bounds / geometry), directions, and geocoding.
  • Analysis (walk/run/rest segmentation, splits, rests, sections) is materialized lazily server-side on first read; web renders it at /tracker/activities/:id.
  • Trimming cuts leading/trailing junk with a dual-handle range slider over a dimmed map preview (POST recorded-routes/{id}/trim).

Paths — repeatable courses with attempts

A path merges recorded routes into a canonical course; routes attach to it as numbered attempts (api/recorded-paths: CRUD + /attempts + /match-check + /analysis). Features: record-from-path, cross-attempt comparison, and a best-combined "potential" computed across attempts. match-check is a read-only similarity probe offered at save time — the server never auto-attaches a route to a path. The web per-path report (/tracker/paths/:id) renders an inline-SVG mini-map, deliberately maplibre-free.


Godot Games Client

spikersoft-games-godot is the unified Godot 4.7 (GDScript, mobile renderer) game client that embeds inside both native mobile apps — one engine, multiple game modes.

Modes

Mode Status Transport
Space Playable MVP: connect → create/list ship → join zone → 6DOF flight → peers → shoot → radar/survey → HUD GameServer WSS + MessagePack
Dungeon Crawler Menu stub GameServer (planned)
Voxel Menu stub GameServer (planned)
Hex Tower Defence Menu stub API SignalR /hubs/hex-tower-defence (not GameServer)

Known gap: both mobile Games hubs present a Chess card and launch the Godot host with mode: "chess", but no chess mode exists in the Godot repo yet — the launch lands on the main menu. (Multiplayer chess is already gated behind a "Coming soon" alert; single-player is not.)

Space-mode client features: Descent-style 6DOF mobile controls (left stick slide, right stick pitch/yaw, roll buttons, thrust/reverse/fire/boost), a mobile-only cruise-throttle latch, a ship-anchored camera rig ported exactly from the web client, off-screen ship indicators at web-HUD parity, unified targeting across ships/radar contacts/celestial bodies, celestial survey scanning with a report panel (threat/age/bio/robotic/composition), and a destroyed overlay with ~5 s server auto-respawn.

Wire protocol (GameServer)

wss://gameserver.spikersoft.com/ws?token=<JWT> — Keycloak JWT in the query string; MessagePack maps, camelCase keys, type discriminator; server ProtocolVersion = 2. Full reference: docs/PROTOCOL.md in the repo.

  • Single-writer send queue; Move commands coalesced at 20 Hz to match the server's rate limit.
  • Buffers are raised before connect (4 MiB inbound) because the server batches a whole tick into one binary frame and the zone-join burst blows past Godot's 64 KiB default.
  • Commands: ListShips, CreateShip, JoinZone, LeaveZone, Move, Shoot, Target, ScanBody, SetRadarMode, ActiveScan, ToggleStealth, FireWeapon, DeployMine, Heartbeat (heartbeat is play-time tracking, not keepalive).
  • Wire gotchas every client must honor: Move.direction* is ship-local (the server applies the ship's rotation — sending world vectors rotates thrust twice); velocity is SET from direction * speed * throttle (coast/drift is produced client-side by decaying throttle); ZoneJoined carries playerId, not entityId; EntityBatchDelta uses binary packedDeltas; AsteroidMoved is a batch; RadarContactUpdate is a full snapshot; ToggleStealth is currently a server-side no-op.

Embedding in the mobile apps

  • Android: GamesActivity extends GodotActivity; JWT + mode/vs/difficulty are injected through SpikerSoftHostPlugin (a GodotPlugin exposing get_access_token(), get_mode(), request_exit(), …). A Gradle syncGodotProject task copies the sibling ../spikersoft-games-godot checkout into app/src/main/assets on every build.
  • iOS: SwiftGodotKit loads Resources/games.pck (exported with godot --headless --export-pack). SwiftGodotKit has no plugin channel, so the handoff is file-based: Swift writes host_session.json ({token, mode, vs, difficulty}) before engine construction; Godot reads it as a one-shot and deletes it so the JWT never sits on disk. Exit requests flow back the same way (Godot writes user://host_exit_request.json; the SwiftUI side polls and dismisses).
  • The shells draw no chrome over the game (nav/status bars hidden, full-bleed) — the only way out is the in-game Exit Games. All 2D UI sits inside a safe-area control inset to DisplayServer.get_display_safe_area() for notch/Dynamic-Island handling.
  • Desktop/editor: paste a Keycloak token on the main menu or pass --token=; hosted sessions hide the paste UI.

Note: this repo currently has no CI — in-engine test scripts only (tests/wire_selftest.gd, tests/arena_smoke.gd).


Desktop Stacker (spikersoft-stacker)

A local, offline-first focus-stacking application (Zerene/Helicon class) for macro photography — one self-contained executable per OS (macOS / Windows / Linux). It began as a port of the platform's former PhotoStack pipeline (artpipe align.py + fusion.py, retired 2026-09-25) to a fast desktop workflow; algorithm parity with the Python implementation is verified by a test harness in the repo.

Workflow stages

  1. Camera — remote capture from OM System / Olympus Wi-Fi cameras: BLE wake-up, live view with tap-to-focus, remote trigger of in-camera Focus Bracketing, auto-download of the new ORFs.
  2. Import — .ORF and other RAW/TIFF/JPEG; instant previews from embedded JPEGs; Olympus FocusStepCount ordering and duplicate curation.
  3. Align — phase-correlation seeding + chained pyramidal ECC-style affine refinement against a reference frame.
  4. Mass crop — auto intersection rect of all aligned frames, adjustable on the master; "Crop" bakes warp+crop into every frame so no border replication reaches fusion.
  5. Stack — five fusion engines: Laplacian pyramid, depth map, complex wavelet, Spiker, and the content-aware Patch engine, validated against a ground-truth harness.
  6. Touch up — dual synced previews (source frame | fused result); a soft brush copies source pixels into the result at full 16-bit resolution, with stroke-replay undo.
  7. Export — 16-bit TIFF / JPEG + a JSON .sstack project sidecar.

Architecture

A Rust workspace of four crates: stacker-core (the pipeline — no UI deps; rawler RAW decode, rustfft, rayon, memmap2), stacker-gpu (wgpu compute kernels from one WGSL codebase targeting Metal/Vulkan/DX12, with a CPU fallback for every kernel), stacker-camera (Olympus Wi-Fi CGI protocol + UDP live view + optional BLE wake, plus a mock camera for tests), and stacker-app (egui/eframe desktop UI).

Free app, optional account. Stacker is free — nothing to buy, no registration key, and every stage above works offline without an account. Account → Sign in with spikersoft.com (the website's Keycloak SSO session, via a loopback PKCE flow) links the app to a spikersoft.com account, which unlocks Publish to spikersoft.com (source frames + finished stack with provenance and Darwin Core identification into the user's gallery, optional blog post, optional public share page at /p/{token}) and removes the small spikersoft.com/stacker credit that unlinked copies add to exported share-set slides. That credit points at the free app page, never at donations, and the finished TIFF / JPEG export never carries it. The public /stacker page lists per-platform installers proxied by api.spikersoft.com from the repo's rolling Gitea Release (latest).

Beyond publishing, the app is self-contained: no S3 client, and every pipeline stage runs locally. It is the platform's only stacker since 2026-09-25 (the in-browser Photo Stack lane and its artpipe model were retired); a published stack re-enters the web side as an Art Studio concept (see Art Studio).


Frontend Architecture: Feature-Oriented Boundaries

The Angular workspace was migrated through a multi-phase refactor (Phases 4–6, completed 2026-05-09 — see docs/architecture/inventory.md and docs/architecture/boundaries.md) from a monolithic libraries/tools/ lib into buildable libraries grouped by layer (84 project.json libraries today and growing). Each library has its own ng-package.json, vitest.config.ts, project.json, and tests run isolated. Lazy-loaded routes per feature → smaller initial bundle (the Phase 6 sub-phases collectively brought the gzipped main.js to ~302 kB).

Layers and clusters

Every library carries at least one layer:* tag in its project.json. The boundary categories on disk are:

Cluster Layer tag Count Purpose
libraries/domain/ layer:domain 11 Pure data models + thin services bound to backend resources, no UI: blog, book, child-account, fundraiser, geo, mrz, photograph, pre-registration, profile, sponsor, users
libraries/features/ layer:feature 32 Top-level user-facing features that own routes/components/services: the dev-tools-* suite (20 tools — C#/Python/JavaScript/regex/x86/C/C++/SQL playgrounds, decompiler, encoding, conversions-monaco, image-to-avif/ico, qr-code, diff, diagram, blockly, duckdb, ipv4, quick-type), plus art-studio, blog, child-account-dialog, fundraiser, games-clue-for-sql, geo-globe, parent-dashboard, photo-gallery, sponsor, sponsor-cards, status, trellis-3d-generator (orphaned — no route; kept for reference, new modeling goes through Art Studio)
libraries/platform/ layer:platform 19 Cross-cutting infrastructure consumed by features: language-runner, lesson-catalog, progress-sync, pyodide-runtime, clang-runtime, monaco-editor, tool-storage, tool-file-menu, intro-tour, intro-launching, js-step-debugger, step-debugger-core, activity-tracking-api/-impl (port + implementation split), anon-session, content-locale, playground-visual-progress, loading, avif-encoder
libraries/shared/ layer:shared 7 Small reusable units that don't belong to a single feature/domain: api-config, js-formatting-options, lesson-panes, lesson-platform, practice-decks, save-load-dialogs, utils/{crc32,file-hash}
libraries/ui/ layer:ui 7 Pure presentational atoms: avatar-stack, award-chips, confirm-dialog, destination-chips, image-paste, mrz-crop, user-picker
libraries/game/ layer:game 3 Game-runtime libs that pair a feature with heavy runtime deps: gl-hud, hex-tower-defence, wasm-voxel
Top-level legacy various 5 Pre-Phase-4 libs that haven't been re-clustered into the new directories yet but already carry the right layer tag: keycloak-admin (layer:feature), marks-site-models + spikersoft-models (layer:domain), spikersoft-environment (layer:platform), spikersoft-theme (layer:ui)

Allowed dependency arrows

Enforced by @nx/enforce-module-boundaries (severity error since Phase 5) on every PR. The full constraint set lives in spikersoft-angular/eslint.config.cjs; the abridged version a feature author cares about:

Source layer May depend on
app feature, ui, platform, domain, runtime, game, editor, shared, generated
feature feature, domain, ui, runtime, game, editor, platform, shared, generated
ui ui, platform, shared
domain domain, shared, generated
platform platform, shared
game game, runtime, shared
shared shared

The shape of the rule, in one diagram:

flowchart TB
    app[app: projects/spikersoft]
    feature[feature: libraries/features, libraries/keycloak-admin]
    domain[domain: libraries/domain, libraries/marks-site-models, libraries/spikersoft-models]
    platform[platform: libraries/platform, libraries/spikersoft-environment]
    ui[ui: libraries/ui, libraries/spikersoft-theme]
    game[game: libraries/game]
    shared[shared: libraries/shared]

    app --> feature
    app --> domain
    app --> platform
    app --> ui
    app --> game
    app --> shared

    feature --> domain
    feature --> platform
    feature --> ui
    feature --> game
    feature --> shared

    ui --> platform
    ui --> shared
    domain --> shared
    platform --> shared
    game --> shared

The arrows go only downward. A domain lib that tries to import from feature or platform fails CI; platform cannot import feature; shared cannot import anything but shared. This is what the boundary refactor bought: the dep graph is now mechanically prevented from collapsing back into the old "everything imports tools" shape.

Cross-cluster ports

Where a feature genuinely needs to consume a service from another cluster without violating the arrows, the lib pair uses a port (an injection token) declared in shared/lesson-platform and provided at the wrapping shell. Examples:

  • PROGRESS_SYNC_PORT — consumed by platform/language-runner so the shared playground shell never hard-imports the Monaco-coupled ProgressSyncService concrete (the feature/dev-tools-csharp-runner shell wires it up at provider time).
  • AVIF_ENCODER port — domain/blog and feature/dev-tools-image-to-avif consume the encoder through a port declared in shared, so the domain lib never depends directly on platform/avif-encoder.

If a new feature seems to require an arrow the layering forbids, the right answer is almost always to extract a port or a contract type into shared/lesson-platform (or its sibling shared/api-config) rather than to widen the rule. See docs/architecture/boundaries.md for the canonical write-up of the rule, baseline counts, and the burn-down protocol that closed the last ~22 violations.


Project Structure

Frontend (spikersoft-angular/)

spikersoft-angular/
├── projects/
│   └── spikersoft/             # Main application
│       └── src/app/
│           ├── _components/
│           │   ├── team/                # Staff profile cards (from Keycloak roles)
│           │   ├── employment/          # Hiring page with open positions + application dialog
│           │   ├── admin/
│           │   │   ├── application-review/   # Admin review for ambassador applications
│           │   │   └── location-management/  # Admin CRUD for locations & positions
│           │   └── _games/
│           │       ├── space-game/        # Orbital mechanics, spacecraft, fleet management
│           │       ├── wasm-voxel-game/   # Voxel world with robot programming
│           │       │   ├── robot-programming-panel/  # Blockly/Rete in-world editor
│           │       │   │   └── program-library/      # Save/load/share programs
│           │       │   └── fleet-panel/              # Multi-robot fleet management
│           │       ├── dungeon-crawler/   # Multiplayer RPG with stronghold PvE
│           │       ├── chess/             # Chess (local, AI, online multiplayer)
│           │       ├── fishing-game/      # Fishing simulation
│           │       └── ...                # Puzzle & learning games
│           └── _services/
│               ├── game-server/  # GameServerService (WebSocket, commands, events)
│               │   └── robot-program-cache.service.ts  # Offline program editing
│               ├── location/     # LocationService (API-driven country list)
│               ├── position/     # PositionService (open positions + auto ambassadors)
│               └── team/         # TeamService (staff member profiles)
├── libraries/                  # Buildable Nx libs grouped by layer (see Frontend Architecture section)
│   ├── domain/                 # layer:domain — pure data + thin services bound to backend resources (11 libs)
│   │   ├── blog/  book/  child-account/  fundraiser/  geo/  mrz/
│   │   └── photograph/  pre-registration/  profile/  sponsor/  users/
│   ├── features/               # layer:feature — top-level user features w/ routes (32 libs)
│   │   ├── art-studio/                 # Art Studio shell + all its views (43 subdirs)
│   │   ├── photo-gallery/              # Personal photo gallery (wall/cards/map, bursts, lightbox)
│   │   ├── geo-globe/  sponsor-cards/  status/
│   │   ├── dev-tools-csharp-runner/    # C# Playground wrapping shell (config + adapter)
│   │   ├── dev-tools-python-runner/    # Python Playground wrapping shell
│   │   ├── dev-tools-javascript-runner/# JS Playground wrapping shell
│   │   ├── dev-tools-reg-ex/           # Regex Playground (chapters + cheatsheet)
│   │   ├── dev-tools-x86-playground/  ...# 20 dev-tools-* + blog, sponsor, fundraiser, ...
│   │   └── …                            # see libraries/features/ for full list
│   ├── platform/               # layer:platform — cross-cutting infra (19 libs)
│   │   ├── language-runner/    # Language-agnostic playground SHELL (consumed by the wrapping shells)
│   │   ├── lesson-catalog/     # Lesson catalog + prerequisites (8 languages)
│   │   ├── progress-sync/      # Server↔in-browser router + offline IndexedDB queue
│   │   ├── pyodide-runtime/    # Pyodide loader (Python in-browser)
│   │   ├── clang-runtime/      # Browser Clang/LLD/WASI toolchain (C and C++ in-browser)
│   │   ├── content-locale/  playground-visual-progress/  anon-session/
│   │   ├── monaco-editor/  tool-storage/  tool-file-menu/  intro-tour/  intro-launching/
│   │   └── js-step-debugger/  step-debugger-core/  activity-tracking-api/  activity-tracking-impl/  loading/  avif-encoder/
│   ├── shared/                 # layer:shared — small reusable units (7 libs)
│   │   ├── lesson-platform/    # Cross-cluster contracts (PROGRESS_SYNC_PORT, SourceFile, ...)
│   │   ├── lesson-panes/       # Lesson + tutorial + sidebar panes
│   │   ├── practice-decks/     # Practice product source of truth (flashcards/knots/UML)
│   │   ├── api-config/  js-formatting-options/  save-load-dialogs/
│   │   └── utils/              # crc32, file-hash
│   ├── ui/                     # layer:ui — presentational atoms (7 libs)
│   │   └── avatar-stack/  award-chips/  confirm-dialog/  destination-chips/  image-paste/  mrz-crop/  user-picker/
│   ├── game/                   # layer:game — game runtime libs (3 libs)
│   │   ├── gl-hud/             # In-scene WebGL HUD framework (troika-three-text, no DOM)
│   │   ├── hex-tower-defence/  # Hex TD client (server-authoritative via SignalR hub)
│   │   └── wasm-voxel/         # TypeScript Minecraft port (voxel engine)
│   │
│   │  # Legacy top-level libs (pre-Phase-4 boundary refactor; tagged but not re-clustered)
│   ├── keycloak-admin/         # layer:feature — Keycloak administration UI
│   ├── marks-site-models/      # layer:domain — Shared models for field-service customer management
│   ├── spikersoft-environment/ # layer:platform — Environment configuration
│   ├── spikersoft-models/      # layer:domain — Shared TypeScript models
│   └── spikersoft-theme/       # layer:ui — Glassmorphic theme + styling utilities
├── nx.json                     # Nx workspace config
└── package.json

Backend (spikersoft-backend/)

spikersoft-backend/
├── SpikerSoft.Api/             # Main REST API + SignalR hubs
├── SpikerSoft.GameServer/      # Real-time game server (30 Hz)
│   ├── Zones/
│   │   ├── SpaceZone.cs        # Orbital mechanics, spacecraft physics
│   │   ├── VoxelZone.cs        # Block world, robot management
│   │   └── CampZone.cs         # RPG zones (camp, dungeon, stronghold)
│   ├── Services/
│   │   ├── RobotExecutionEngine.cs         # Robot instruction interpreter
│   │   ├── MongoRobotProgramRepository.cs  # Program persistence
│   │   └── GameLoopService.cs              # 30Hz fixed-timestep loop
│   └── Network/
│       ├── MessagePackSerializer.cs  # Binary protocol (v2)
│       └── ConnectionManager.cs      # WebSocket session management
├── SpikerSoft.Business/        # Business logic (CQRS + MediatR) — profile, sponsor, art studio, tracker, tm, …
│   └── Domain/GameServer/      # Game domain logic
├── SpikerSoft.Business.CodeExecution/  # Lesson + grading bounded context (split out of Business)
│   └── Domain/
│       ├── Lessons/
│       │   ├── LessonStrategyBase.cs            # Abstract base for lesson definitions
│       │   └── Curriculum/                      # Tier{NN}_* (102 C# lessons) + Python/ JavaScript/ Regex/ C/ Cpp/ Sql/ X86/
│       └── CodeExecution/
│           ├── Execution/RoslynCodeExecutor.cs  # Shared compile + run engine (server + WASM)
│           └── Hints/StudentCodeHintAnalyzer.cs # Pre-Roslyn pattern checks (+ per-language siblings)
├── SpikerSoft.Games.HexTowerDefence/   # Server-authoritative hex TD reducer (C# port of the TS game)
├── SpikerSoft.Wasm/            # .NET 10 WebAssembly bundle of Roslyn + lesson strategies
│   ├── WasmCompilerEntry.cs    # [JSExport] surface (Ping, GetAttempt, ExecuteLessonCode, ExecuteFreePlayCode, AddReferenceImage)
│   └── main.js                 # Worker boot script — pre-fetches PE bytes, feeds Roslyn
├── SpikerSoft.Common/          # Shared models and interfaces
│   └── Models/
│       └── GameServer/         # Commands, Events, Entities, RobotInstruction
├── SpikerSoft.Data/            # Data access layer
│   └── Mongos/                 # MongoDB documents (incl. game state, lessons, userLessonProgress)
├── SpikerSoft.Contracts.SignalR/ # SignalR hub contracts
├── SpikerSoft.AI.MCPServer/    # AI/ML model server
├── SpikerSoft.EventHandlers.*/ # 28 distributed event processor/worker projects (incl. CodeExecution, ArtPipeProcessor, GpuCoordinator)
├── SpikerSoft.Workers.*/       # Non-handler worker libs (Decompile, Notifications, Ocr)
├── *.Tests/                    # Per-project test projects (36), collected by SpikerSoft.UnitTests.slnf
└── SpikerSoft.sln

Getting Started

Prerequisites

Frontend:

  • Node.js (see .node-version for required version)
  • pnpm package manager
  • fnm (Fast Node Manager) recommended

Backend:

  • .NET 10 SDK (10.0.100 or later)
  • Docker Desktop with Swarm support
  • MongoDB, Redis, RabbitMQ (via Docker or local)
  • Keycloak instance

Quick Start

Frontend

cd spikersoft-angular

# Install dependencies
pnpm install

# Start development server
pnpm run serve:spikersoft-development

# Run tests
pnpm run test-spikersoft

# Build for production
pnpm run build

Backend API

cd spikersoft-backend

# Restore dependencies
dotnet restore

# Run API in development
dotnet run --project SpikerSoft.Api

# Or use Docker Compose
docker-compose up

Game Server

cd spikersoft-backend

# Run game server
dotnet run --project SpikerSoft.GameServer

# With specific port
dotnet run --project SpikerSoft.GameServer -- --port 7777

Configuration

Environment-Specific Settings

Both frontend and backend support environment-specific configuration:

Frontend:

  • environment.ts - Development
  • environment.production.ts - Production

Backend:

  • appsettings.json - Base configuration
  • appsettings.Development.json - Development overrides
  • appsettings.Production.json - Production settings
  • appsettings.Windows.json / appsettings.Linux.json - Platform-specific

Required Services

Service Default Port Purpose
MongoDB 27017 (host: 27117) Primary database (sharded cluster via mongo-router)
Redis 6379 Caching, SignalR backplane, vector search
RabbitMQ 5672 / 15672 (mgmt) Message queue with DLQ + retry tiers
Keycloak 8080 Identity provider (OAuth2 / OIDC)
Seq 5341 Structured log aggregation
Jaeger 4317 Distributed tracing (OTLP gRPC)
InfluxDB 8086 Time-series metrics and dashboards
MinIO 9000 S3 object storage: photos, art-asset artifacts, practice packs, PMTiles terrain (map-tiles/terrain.pmtiles — replaced the retired TileServerGL)
OpenBao 8200 Secrets management (3-node raft HA at bao.spikersoft.com; services and CI fetch secrets via AppRole)
DNS Server 5380 Self-hosted DNS management
Email (SMTP) 587 Mail server with DKIM, DMARC, SPF
Checkr API N/A (external) Background checks — USA (optional, falls back to Manual)
Sterling API N/A (external) Background checks — International (optional, falls back to Manual)

Account Types & Age Requirements

SpikerSoft supports three logical account categories:

Account Type Age Description
Parent 18+ Adult account that can create and manage child accounts. Has full platform access and a parental dashboard for overseeing children's activities, progress, and travel arrangements.
Child 10–17 Managed account created by a parent. Requires parental approval (staff-reviewed). Access to learning experiences, interest tracking, and (if eligible) travel programs.
Independent Solo Traveler 18+ Functionally identical to a Parent account but without linked children. Any adult account without child accounts is an independent solo traveler by default — no separate registration flow is needed.

Age-Gated Rules

Rule Minimum Age Details
Create a child account 10 Children under 10 are not eligible for accounts.
Travel programs (domestic or international) 12 Children ages 10–11 can have an account for learning and interests, but travel programs require age 12+. A "domestic travel only" option is available, and the system enforces this on both frontend and backend.
Child account upper bound 17 (under 18) Adults (18+) should register directly as a Parent or Independent Solo Traveler.
Child account conversion warning 18 At 18, child accounts are no longer considered minors legally. A dismissable warning banner is shown prompting the user to prepare for account conversion.
Child account mandatory conversion 19 At 19, the user must convert their child account to an independent individual account. A non-dismissable dialog is shown on login. If declined, the account is disabled in Keycloak and marked as Inactive.

Children Tab Visibility

The "Children" tab on the profile page is visible for all adult accounts (18+) that are not child accounts themselves. This is controlled by a computed canManageChildren field on the profile DTO, calculated from the user's age and IsChildAccount status. Removing all linked children does not hide the tab -- adults can always add new children.

Child Account Aging-Out

When a child account user's age naturally progresses past the child threshold, the system handles the transition automatically:

  1. At 18: IsMinor is set to false on the next profile load. A warning banner appears in the navigation bar informing the user that their account will need to be converted before age 19. The banner is dismissable per session.
  2. At 19: The AccountStatus is set to RequiresConversion. A mandatory (non-dismissable) dialog is shown on login with two options:
    • Convert: Sets IsChildAccount = false, clears ParentalControls, sets AccountStatus = Active. The account becomes a full independent individual account with all data preserved.
    • Decline: The Keycloak user is disabled (enabled: false) and AccountStatus is set to Inactive. The user is logged out.

Parent--Child System

  • Account Creation Flow: Parents use the parental dashboard to create child accounts via a multi-step wizard: Travel/Passport → Child Info → Health → Emergency Contacts → Credentials → Review.
  • Passport & Travel: An optional passport OCR feature (Tesseract + MRZ parsing) can auto-populate passport fields from an uploaded photo. Children who only travel domestically can opt out of passport requirements entirely.
  • Staff Review: All child account requests go through staff review before activation. Staff access the review queue at /admin/child-account-review, which is protected by a RoleGuard requiring the Admin or Staff Keycloak realm role.
  • Encrypted PII: Passport data (number, full name, DOB, issue/expiry dates) is encrypted at rest using envelope encryption.
  • Interest Profiles: Both parents and children maintain interest profiles with star ratings across dynamically managed areas of interest (e.g., Hiking, Robotics, Photography) that inform curriculum and experience recommendations.
  • Withdraw Pending Requests: Parents can withdraw (cancel) pending child account requests before staff reviews them. The request is permanently deleted. Available from both the parent dashboard and the profile's Children tab.
  • Remove Linked Children: Parents can remove an approved child account. This deletes the child's Keycloak user, deletes their provisioned email mailbox (via PostfixAdmin), removes their profile from MongoDB, and revokes the original request. The parent's IsParent flag is preserved (once a parent, always a parent). If the email mailbox deletion fails (e.g., database offline), a DeleteEmailMailbox provisioning task is created for staff to retry or manually resolve from the System Issues page.

Role-Based Access

Role Scope Grants
Admin Keycloak realm role Full access: Keycloak admin panel, child account review, health info for any child
Staff Keycloak realm role Child account review (approve/reject), passport viewing (audit-logged), health info access

Both roles are Keycloak realm roles and can be assigned to users via the Keycloak Admin Panel (Roles tab → assign to user). The frontend RoleGuard reads roles from the JWT realm_access.roles claim. The backend checks User.IsInRole() for both Admin/admin and Staff/staff (case-insensitive).


System Issues & Provisioning Tasks

The System Issues page (/admin/staff-issues) provides staff and admin users with visibility into third-party integration failures — such as email mailbox provisioning — and tools to resolve them. The system is designed so that third-party failures never block core operations (e.g., child account approval always succeeds, even if the email server is unreachable).

How It Works

  1. Failure Logging: When a third-party operation fails (e.g., creating a username@spikersoft.com mailbox during child account approval), the error is captured as a ProvisioningTask in MongoDB with status Failed, along with all metadata needed to retry the operation later.
  2. Menu Badge: A badge counter on the "System Issues" menu item shows staff/admin users how many outstanding (failed) tasks need attention. This uses a lightweight GET /api/admin/provisioning-tasks/count endpoint.
  3. Issue Dashboard: The staff issues page lists all failed provisioning tasks with details (error message, related entity, timestamps, retry count). Staff can:
    • Retry — Automatically re-attempt the failed operation (e.g., re-try creating the mailbox).
    • Mark Resolved — Record that the issue was handled manually (e.g., mailbox created via PostfixAdmin), with optional resolution notes.
  4. Resolution History: A toggleable "Resolution History" section shows all previously resolved tasks, including who resolved them, when, and any notes. This provides a full audit trail of system issues and their resolutions.

Provisioning Task Lifecycle

Operation Fails → ProvisioningTask (Failed) created
       ↓
Staff sees badge → opens System Issues page
       ↓
┌──── Retry ────────────────────────────────────────┐
│  Success → status = Resolved, badge decrements    │
│  Failure → stays Failed, error updated, retry++   │
└───────────────────────────────────────────────────┘
       or
┌──── Mark Resolved ────────────────────────────────┐
│  status = ManuallyResolved, resolver + notes saved │
│  Badge decrements, task moves to history           │
└───────────────────────────────────────────────────┘

API Endpoints

Method Endpoint Purpose
GET /api/admin/provisioning-tasks List all failed tasks
GET /api/admin/provisioning-tasks/count Count of failed tasks (for badge)
GET /api/admin/provisioning-tasks/history List all resolved/manually-resolved tasks
POST /api/admin/provisioning-tasks/{id}/retry Retry a failed task
POST /api/admin/provisioning-tasks/{id}/resolve Mark a task as manually resolved

All endpoints require authentication and an Admin or Staff role.


Profile & Travel

The profile page has grown well beyond Personal + Travel. Current tabs (projects/spikersoft/src/app/_components/profile/tabs/), each with its own guided intro.js tour:

Tab Purpose
Personal Identity, mailing address, home country, sponsorship toggle
Children Child account management (adults only)
Travel Travel map, wishlists, documents
Photos Personal photo uploads (feeds the photo gallery)
Photography Camera-bag registry derived server-side from EXIF serial numbers of uploads — nothing self-reported
Interests Star-rated areas of interest (drives recommendations, mobile launcher folders, geography filters)
Skills Earned skill badges (see Skills & Badges)
Schedule Weekly availability (feeds the calendar system)
Site Settings Appearance + the Offline WASM preload panel
Parental Parental controls and permissions (e.g. the Photography permission that gates a minor's photo tagging)

Profile forms have been migrated to Angular Signal Forms (personalForm().dirty() etc.) with an aggregate unsaved-changes guard across tabs.

Personal Tab — Mailing Address & Home Country

The Personal tab includes a structured mailing address section (Street 1, Street 2, City, State/Province, Postal Code, Country) and a Home Country dropdown. The home country setting is used by the domestic travel map to automatically load the correct country view.

Travel Map

The profile's Travel tab provides an interactive Leaflet-based map for tracking travel history, wishlists, and parental approvals.

Travel Preferences

Users select their travel interests from the Interests tab via two checkboxes:

Checkbox Effect
International Travel Enables the world map view on the Travel tab
Domestic Travel Enables the home-country sub-region map on the Travel tab

When both are selected, a toggle lets the user switch between International and Domestic views.

Available Countries

SpikerSoft currently operates in 13 countries. Only these countries are interactive on the world map; all others appear dimmed with a "Not available yet" tooltip:

Canada, United States, Mexico, Belize, Honduras, Colombia, Brazil, Peru, Ecuador, Argentina, Chile, Uruguay, Cuba

Country Drill-Down

When International Travel is enabled, an "Explore Country" dropdown appears above the world map. Selecting a country renders a sub-region (state/province) map between the dropdown and the world map, allowing drill-down without leaving the international view.

For domestic-only travelers, the domestic map automatically loads the user's home country from the Personal tab — no dropdown is needed.

Visited Status (Split Tracking)

Each country or sub-region supports two distinct visited categories:

Status Color Meaning
Visited (External) Green Traveled there independently, outside SpikerSoft
Visited (SpikerSoft) Purple Traveled there through a SpikerSoft experience
Wishlist Blue Wants to visit

Clicking a country/region cycles through: Wishlist → Visited (External) → Visited (SpikerSoft) → Clear.

Domestic Map

When "Domestic Travel" is enabled and a home country is selected on the Personal tab, the Travel tab shows an admin-level-1 (state/province) GeoJSON map for that country. Sub-region GeoJSON files are stored in assets/geo/states/{ISO_A3}.geo.json for all 13 available countries. The map dynamically reloads when the home country selection changes.

Travel Documents

Users can upload, download, and delete travel documents (visas, travel permits, vaccination records, and other files) on the Travel tab. Documents are stored using encrypted GridFS storage via the SecureDocumentStorageService and are associated with the user's profile. Supported file types include PDF and common image formats (JPEG, PNG, WebP). Each document has a type classification and an optional label.

Parental Approval

For child accounts (mode: "minor"), wishlisted countries appear as "Requested" until a parent approves them from the parent dashboard (mode: "parent-approve").

Unsaved Changes Guard

All profile forms (Personal, Interests, Travel, Appearance) are protected by an unsaved changes guard:

  • Tab switching: Prompts save/discard/cancel when switching between profile tabs with dirty forms.
  • Route navigation: A canDeactivate route guard intercepts navigation away from the profile page.
  • Browser close/refresh: A beforeunload handler warns users about unsaved changes.

Development Guidelines

Code Style

Frontend (Angular):

  • Standalone components (no NgModules)
  • Signals for state management
  • OnPush change detection
  • See .cursor/rules/spikersoft-angular/ for detailed guidelines

Backend (C#):

  • CQRS pattern with MediatR
  • Repository pattern for data access
  • .NET 10 / C# 14 features encouraged
  • See .cursor/rules/spikersoft-backend/ for detailed guidelines

Testing

Frontend:

pnpm run test-spikersoft           # Unit tests for spikersoft only (Nx → Vitest)
pnpm run test-all                  # All test targets (5 projects: spikersoft, tools, keycloak-admin, spikersoft-theme, wasm-voxel)
pnpm run test-all:detect-leaks     # Same projects, but with VITEST_DETECT_ASYNC_LEAKS=true → forks pool flags dangling timers / unhandled promises

test-all:detect-leaks flips every project into Vitest's forks pool with detectAsyncLeaks: true. Each project's vitest.config.ts checks the env var and toggles the pool accordingly so the regular test-all keeps the faster threads pool. Cross-platform env-var setting uses cross-env (devDependency).

There is no e2e script in spikersoft-angular/package.json; add an Nx E2E target if you introduce browser E2E.

Backend:

dotnet test                        # All tests
dotnet test --filter "Category=Unit"  # Unit tests only

Where issues live

File every ticket in the repo whose code has to change. Not in a central tracker.

Kind of work Where it goes
.NET services, API, GameServer, CQRS, EF/Mongo spikersoft-backend
Angular app, browser games, playgrounds, libs spikersoft-angular
Swarm stacks, Traefik, MinIO, Keycloak, OpenBao, registry, networking spikersoft-infrastructure
Python art pipeline, model backends, its Dockerfiles spikersoft-artpipe
Native mobile apps spikersoft-ios / spikersoft-android
Godot client spikersoft-games-godot

There is no central tracker. spikerj/spikersoft-issues was retired on 2026-08-07 and now holds this README only — do not file there.

The reason is mechanical: Gitea auto-closes an issue when a merged PR says fixes #N only within the same repo. A ticket filed in a central tracker can never close itself, so completed work silently accumulates as an open backlog. That is exactly what happened — the umbrella tracker reached 234 open issues before it was broken up.

Rules that follow from this:

  • A ticket must be closeable by merging in one repo. If it isn't, it's really several tickets.
  • Work spanning repos becomes one narrowly-scoped issue per repo. There is no epic parent — a unit is held together by two things instead:
    1. a shared title prefix, e.g. [Scheduling] Backend: recurrence expansion, so the whole unit is one search away; and
    2. a sibling cross-link block in every issue body listing the unit by full reference.
  • Reference issues across repos with the full owner/repo#N form. A bare #N resolves against the repo you're writing in, which silently points at an unrelated ticket.
  • When an epic-sized idea has genuinely undecided design, file a decision issue stating the concrete options and what must be settled. That is a real, closeable deliverable; a vague "implement X" stub is not.

QA: Gitea issues and the AI agent (MCP)

Using the Gitea MCP in Cursor so QA (and devs) can work tickets through the AI agent without leaving the IDE:

  1. Enable the integration — In Cursor Settings → MCP, ensure the Gitea server is on and configured (Instance URL, access token, and path to the gitea-mcp command if you use the standalone binary). The token must have API scope to read and create issues in the org/repos you use.
  2. Confirm the session — The Gitea tools appear in the project only when the MCP is connected. If a chat reports that the Gitea server is missing, re-open MCP settings and toggle or reconnect, then start a new agent message.
  3. What to ask the agent — Natural-language requests map to the MCP, for example: “List open issues in spikerj/spikersoft-backend”, “Create an issue in the repo that owns this file for …”, “Add a comment on spikerj/spikersoft-angular#12 summarizing the repro”, or “Search repos named …” (the exact tool surface depends on your gitea-mcp build; list/search/create/edit issues and comments are the usual workflows). Per Where issues live, name the target repo explicitly — the agent should not default to a central tracker.
  4. Good practices — Prefer filing reproduction steps, build or environment, and expected vs actual behavior in the issue body so the agent can quote them back accurately. For sensitive data, do not paste secrets into issues; use references to internal logs or redacted snippets.

This complements manual use of the Gitea web UI: the same https://git.spikersoft.com data, with faster handoff from chat, terminal output, and code context while triaging or closing the loop on QA.

Observability

  • Logging: Serilog → Seq (structured logging with correlation IDs)
  • Tracing: OpenTelemetry → Jaeger (OTLP gRPC on port 4317)
  • Metrics: OpenTelemetry exporters → InfluxDB v2.7
  • Health: /healthz endpoint with 13 infrastructure checks (see Health Check & Observability)

All cross-service calls include correlation IDs for distributed tracing.


Deployment

Production Stack

  • Orchestration: Docker Swarm
  • Load Balancer: Traefik v3.6 with Let's Encrypt SSL
  • MongoDB: Sharded cluster (3 shards × 3 replicas + router + config servers)
  • Redis: 6-node cluster (3 masters + 3 replicas)

Container Images

# Build API image
docker build -t spikersoft-api -f SpikerSoft.Api/Dockerfile .

# Build Game Server image
docker build -t spikersoft-gameserver -f SpikerSoft.GameServer/Dockerfile .

# Build CodeRunner (sandboxed C# grader worker) image
docker build -t spikersoft-coderunner -f SpikerSoft.EventHandlers.CodeExecution/Dockerfile .

# Build SpikerSoft.Wasm (browser-side Roslyn bundle) — published to Gitea Generic Package by spikersoft-wasm.yml
dotnet publish SpikerSoft.Wasm -c Release   # writes to ../spikersoft-angular/projects/spikersoft/src/assets/dotnet/

# Build Angular image (downloads SpikerSoft.Wasm bundle from Gitea Generic Package in CI)
docker build -t spikersoft-angular -f spikersoft-angular/Dockerfile .

Documentation

Component README / Section
Angular Frontend spikersoft-angular/README.md
Backend API spikersoft-backend/README.md
Game Server spikersoft-backend/SpikerSoft.GameServer/README.md
Integrated Game Ecosystem See main README — Integrated Game Ecosystem
Robot & Visual Programming See main README — Robot & Visual Programming and ARCHITECTURE-ROBOT-VISUAL-PROGRAMMING.md
C# Coding Curriculum & Playground See main README — C# Coding Curriculum & Playground; WASM bundle: spikersoft-angular/projects/spikersoft/src/assets/dotnet/README.md; Wrapping shell: spikersoft-angular/libraries/features/dev-tools-csharp-runner/README.md; Shared playground SHELL: spikersoft-angular/libraries/platform/language-runner/README.md
Event Handlers See main README — Event Handlers; shared infra: spikersoft-backend/SpikerSoft.EventHandlers.Infrastructure/README.md; code worker: spikersoft-backend/SpikerSoft.EventHandlers.CodeExecution/README.md
Chess Game See main README — Chess Game section
Dungeon Crawler spikersoft-angular/projects/spikersoft/src/app/_components/_games/dungeon-crawler/README.md
Team & Hiring See main README — Team Page, Hiring Page, Data-Driven Locations sections
Sponsorship See main README — Sponsorship & Donation System
Fundraisers See main README — Fundraiser System
Info Vault See main README — Info Vault
Blog System See main README — Blog System
Reading Journey See main README — Reading Journey & Book System
Real-time Communication See main README — Real-time Communication
Art Studio & artpipe See main README — Art Studio; pipeline internals: spikersoft-artpipe/CLAUDE.md; Angular lib: spikersoft-angular/libraries/features/art-studio/README.md
Asset-to-Game Integration See main README — Asset-to-Game Integration
Photography & Gallery See main README — Photography & Gallery
Native Mobile Apps See main README — Native Mobile Apps; repos: spikersoft-ios/, spikersoft-android/
Activity Tracker See main README — Activity Tracker, Routes & Paths
Godot Games Client See main README — Godot Games Client; protocol: spikersoft-games-godot/docs/PROTOCOL.md
Desktop Stacker See main README — Desktop Stacker; repo: spikersoft-stacker/README.md
Time & Materials See main README — Time & Materials; repo: spikersoft-time-and-materials/README.md
Marks Field Service (legacy) See main README — Marks Field Service
Ops, Fleet & Status See main README — Ops, Fleet & Status
Health & Observability See main README — Health Check & Observability

Chess Game

The platform includes a full-featured chess game with three play modes.

Game Modes

Mode Description
2 Players — Same Computer Classic local play, two players share one screen
Versus AI Play against a client-side AI with three difficulty levels
Queue vs SpikerSoft Members Real-time online multiplayer coordinated via SignalR

AI Difficulty Levels

  • Easy — Random legal move selection
  • Normal — Minimax with alpha-beta pruning (depth 2), basic piece-value evaluation + center control bonus
  • Hard — Minimax (depth 3) with piece-square positional tables (pawn structure, knight outposts, king safety)

The AI runs entirely client-side — no backend compute required. After the human moves, the ChessAIService evaluates the position in a deferred setTimeout to keep the UI responsive.

Online Multiplayer

Online play uses a dedicated ChessHub SignalR hub (/hubs/chess) with Keycloak authentication.

Matchmaking flow:

  1. Player joins the queue (can also queue while playing an AI game)
  2. When two players are queued, the server pairs them, assigns random colors, and notifies both
  3. A confirmation dialog appears — accepting starts the online match (pausing any in-progress AI game)
  4. Moves are sent to the server and relayed to the opponent in real time
  5. If a player disconnects, a 60-second timer starts; if they don't reconnect, the opponent wins by abandonment

Available actions during an online game: Offer Draw, Accept/Decline Draw, Resign.

Game Persistence

Completed online games (checkmate, resignation, draw, abandonment) are persisted to MongoDB as ChessGame documents, storing both players, all moves, result, and timestamps.

Custom Piece Sets (Art Studio)

Chess pieces can be replaced with Art-Studio-generated 3D sets: a chess-set asset group fans one theme prompt out into six per-piece generations (api/artstudio/groups, kind ChessSet; slot keys piece:{kind} equal the loadout slot ids), the finished set can be shared to the gallery and moderated, and an approved set is equipped into the game via the per-game art loadout (api/game/chess/art-loadout, 12 slots — 6 per side). The 3D board hot-swaps piece prototypes with byte-budget enforcement and falls back to the bundled classic mesh if a custom model is too large or fails to load. See Asset-to-Game Integration.

Key Files

File Purpose
chess.component.ts/html/scss UI component with mode selection, board, dialogs
chess-game.service.ts Core game logic — move validation, castling, en passant, promotion
chess-ai.service.ts Client-side AI engine (minimax + alpha-beta pruning)
chess-online.service.ts SignalR service for online multiplayer
ChessHub.cs Backend SignalR hub — matchmaking, move relay, disconnect handling
ChessGame.cs MongoDB document for completed game history

Team Page

The /team route displays profiles of all SpikerSoft staff members. Profiles are fetched by querying Keycloak for users with the "Staff" and "Admin" realm roles, then merging with MongoDB UserProfile data (avatar, handle, bio).

  • Backend: TeamController (GET /api/team) calls KeycloakAdminService.GetRoleMembersAsync() to list role members, then joins with the user-profiles collection.
  • Frontend: TeamComponent uses TeamService to load members and displays them in a responsive glassmorphic card grid. Each card shows an avatar (or generated initials), name, handle, and bio excerpt.
File Purpose
TeamController.cs API endpoint merging Keycloak role members with MongoDB profiles
KeycloakAdminService.GetRoleMembersAsync() Queries Keycloak Admin API for users in a given role
team.service.ts Frontend service fetching GET /api/team
team.component.ts/html/scss Staff profile card grid with loading and error states

Hiring Page

The /employment route is a master-detail careers page (it replaced a flat grid of ~33 near-identical cards that had no detail view, no filters, and no per-role URL):

  • Filter bar — country / work mode / section, mirrored to query params so filtered views are shareable.
  • Role listbox + routed detail pane — each role has its own URL (/employment/:slug), so a specific opening can be linked directly.
  • Geo-globe — a WebGL globe (the shared @spikersoft/feature-geo-globe library, also used by the sponsor family page and the geography explorer) highlights where SpikerSoft works.

Position sources remain two-fold:

  1. Ambassador Positions — Auto-generated for every active Location that does not have an assigned ambassador, so the page always reflects current staffing gaps.
  2. Other Roles — Manually created positions (developer, designer, teacher, etc.) managed through the admin Location Management page.
  • Backend: PositionsController (GET /api/positions) merges manually created OpenPosition documents with auto-generated ambassador entries for unassigned locations.
  • Frontend: EmploymentComponent + employment-filters / employment-list / employment-detail children, backed by PositionCatalogService.

Ambassador Application Workflow

Clicking an open position card launches a multi-step application dialog (ApplicationDialogComponent). Applicants must be logged in.

Intake Form Steps:

  1. Position & Passport — Read-only position summary, optional passport image scan via OCR (reuses the same IPassportOcrService / Tesseract OCR pipeline as the child account flow).
  2. Personal Information — Name, DOB, sex, nationality, citizenship, government ID (SSN / National ID / Tax ID), email, phone. Fields auto-populate from passport scan results.
  3. Address History — Current and previous addresses with date ranges.
  4. Work & Education — Employment history and education entries.
  5. References — Minimum 2 professional/personal references.
  6. Consent & Submit — Background check authorization and terms acceptance.

Application Status Lifecycle:

Submitted → UnderReview → BackgroundCheckPending → BackgroundCheckComplete → Approved → Hired

Applications can be Rejected at any review stage with a reason.

Admin Review:

Staff/Admin users access the review panel at /admin/application-review (menu item: "Applications" with pending count badge). From there they can:

  • Begin review, initiate background checks (provider selected automatically by country), approve/reject, and hire.
  • The Hire action creates a Keycloak account with the Staff role, creates a PositionAssignment, and marks the application as Hired.
File Purpose
PositionsController.cs Merges manual positions with auto-generated ambassador positions
ApplicationController.cs Application submission, passport scan, admin review, background check, hire endpoints
AmbassadorApplication.cs MongoDB entity for applications with encrypted PII
OpenPosition.cs MongoDB model for manually managed positions
position.service.ts Frontend service for positions CRUD
application.service.ts Frontend service for application API calls
employment.component.ts/html/scss Master-detail careers page (filters + listbox + routed detail + globe)
application-dialog.component.ts Multi-step application stepper dialog
application-review.component.ts Admin review panel with status workflow

Third-Party Background Check Providers

The background check system uses a pluggable provider architecture. Each provider implements IBackgroundCheckProvider and is resolved per-country via BackgroundCheckProviderFactory.

Supported Providers

Provider Coverage API Documentation Notes
Checkr USA docs.checkr.com API-first platform; industry standard for US background checks. Requires API key from Checkr dashboard.
Sterling International (Latin America, Canada, etc.) sterlingcheck.com Broadest international coverage for non-US countries.
Manual Fallback (all countries) N/A No external API. Staff perform checks externally and record results in the admin panel. Used when no automated provider is configured for a country.

Configuration

Country-to-provider mappings and API keys are configured in appsettings.json. API keys must not be committed — use environment variables or a secrets manager in deployment.

"BackgroundCheck": {
  "DefaultProvider": "Manual",
  "CountryProviders": {
    "US": { "Provider": "Checkr", "ApiKey": "" },
    "International": { "Provider": "Sterling", "ApiKey": "" }
  }
}

Adding a New Provider

  1. Create a class implementing IBackgroundCheckProvider in SpikerSoft.Business/Services/BackgroundCheck/Providers/.
  2. Register it in DI (ServiceCollectionExtensions.cs).
  3. Add the country mapping in appsettings.json under BackgroundCheck:CountryProviders.

API Key Management

Background check API keys follow the same pattern as GoogleMaps:ApiKey: empty strings in appsettings.json with actual values injected via environment variables (BackgroundCheck__CountryProviders__US__ApiKey) or a secrets vault in production.


Postal Code / City / State Lookup

The platform includes a standardized postal code lookup system powered by GeoNames open data. When a user enters a postal code in any address form (ambassador application, profile, marks addresses), the system auto-populates the city and state fields and presents a state dropdown for the selected country.

Data Source

Source URL License
GeoNames Postal Codes download.geonames.org/export/zip/ Creative Commons Attribution 4.0

Data covers all 13 operating countries (US, CA, MX, BZ, HN, CO, BR, PE, EC, AR, CL, UY, CU).

Architecture

  • MongoDB collection: postal-codes stores parsed GeoNames entries (country, postal code, city, state, coordinates)
  • CQRS Queries: LookupPostalCodeQuery and GetStatesForCountryQuery in SpikerSoft.Business/Domain/PostalCodes/
  • Controller: PostalCodesController at api/PostalCodes
  • Frontend: PostalCodeService provides lookupPostalCode() and getStatesForCountry() with caching

API Endpoints

Endpoint Auth Purpose
GET /api/PostalCodes/lookup?countryCode={cc}&postalCode={pc} Anonymous Returns matching city/state entries
GET /api/PostalCodes/states/{countryCode} Anonymous Returns distinct states for a country
POST /api/PostalCodes/seed?countryCode={cc} Admin Downloads and imports GeoNames data
GET /api/PostalCodes/status Staff Returns record counts per country

Seeding Data

After deployment, an admin must seed the postal code data:

# Seed all operating countries
curl -X POST https://your-api/api/PostalCodes/seed -H "Authorization: Bearer <admin-token>"

# Seed a single country
curl -X POST "https://your-api/api/PostalCodes/seed?countryCode=US" -H "Authorization: Bearer <admin-token>"

Attribution

This product includes data from GeoNames (geonames.org), licensed under Creative Commons Attribution 4.0.


Data-Driven Locations

All country/location data is now stored in MongoDB instead of being hardcoded. The Location model holds country codes, names, active status, and optional ambassador assignments.

Key changes:

  • The AVAILABLE_COUNTRIES constant in country-map.component.ts is replaced with an availableCountries input, allowing dynamic country lists from the API.
  • The ProfileComponent fetches active locations from LocationService on init and passes them to the country map and travel dropdowns.
  • The LocationsController auto-seeds the initial 13 countries (CAN, USA, MEX, BLZ, HND, COL, BRA, PER, ECU, ARG, CHL, URY, CUB) on first request if the collection is empty.
File Purpose
Location.cs MongoDB model for locations
LocationsController.cs Full CRUD + auto-seed for locations
location.service.ts Frontend service for location CRUD
country-map.component.ts Accepts dynamic availableCountries input
profile.component.ts Loads locations from API instead of hardcoded array

Location Management

Staff and admin users can manage locations and open positions via /admin/location-management, accessible from the "Location Management" menu item in the user dropdown.

  • Locations Tab: View all locations, toggle active/inactive, add new countries, delete locations.
  • Positions Tab: View manually created positions, create new roles, toggle active status, delete positions. Auto-generated ambassador positions (from unassigned locations) appear on the hiring page but are not directly editable here.
File Purpose
location-management.component.ts/html/scss Admin page with tabbed location and position management
routes.ts Route registered at admin/location-management with AuthGuard + RoleGuard
menu-bar.menu.ts Navigation is data-driven: MENU_SECTIONS in this one file is the source of truth for both the desktop dropdowns and the mobile drawer, so items can no longer drift between the two surfaces. Sections: Learn (incl. Coding → Languages / Patterns / Version Control / Testing), Games, Art Studio, Development, About, Management. Items carry gates (auth/staff/canReviewArt/canBlog/notChild/readingInterest/videoCall), badge ids, and drawer-specific labels

Sponsorship & Donation System

SpikerSoft is a nonprofit organization. The sponsorship system enables donors to fund travel experiences for approved individuals, children, and families — under the "Sponsor a Journey" banner. The public landing page was rebranded family-first ("Family Exploration & Learning" / "Explore the World, Together"; the stat row leads with the Families count) and /sponsor now offers three view modes — Families, Families by destination, Individuals by destination — plus a "Location funds" section. The individual flow below came first; families and funds are documented in Families, Family Funds & Location Funds.

How It Works:

  1. Enable Sponsorship — Parents or individuals 18+ enable the sponsorship toggle in the Sponsorship section of their Profile > Personal tab, configuring display preferences (name mode, bio, photo, interests, destinations, custom message). Parents can also enable sponsorship per-child via the sponsorship toggle on each child's card in the Children tab.
  2. Staff Approval — Staff members review and approve sponsorship profiles via Admin > Sponsorship Management.
  3. Trip Cost Assignment — Staff sets estimated travel costs per wishlisted destination for each approved profile.
  4. Public Donation Page — A public /sponsor page (no login required) displays approved profiles with their bio, interests, destinations, and funding progress.
  5. Stripe Checkout — Donors can donate to an individual or to the SpikerSoft organization. Payments are processed via Stripe Checkout with automatic nonprofit receipts.
  6. Organization Donations — Organization-wide donations are distributed evenly among all eligible (approved, not fully funded) profiles at the time of donation. New applicants do not retroactively receive prior donations.
  7. Overfunding — When an individual is fully funded (total raised >= total trip cost), additional donations automatically redirect to the organization pool.
  8. QR Code & Sharing — Each sponsor profile page has a "Generate QR Code" button for creating business-card-ready QR codes linking to the donation page.

Account Types & Sponsorship Eligibility:

  • Only parents and individuals 18+ can enable their own sponsorship (in the Personal tab's Sponsorship section)
  • Parents can toggle per-child sponsorship via the child's card in the Children tab
  • Child accounts cannot toggle their own sponsorship status
  • Staff must approve before profiles go public

Privacy Controls (Parent-Configurable):

  • Display name mode: First name + last initial, handle/alias only, or full name
  • Photo mode: Original, Cartoon (recommended for minors), or Hidden
    • Cartoon mode uses SixLabors.ImageSharp to generate a stylized cartoon version of the avatar (Gaussian blur + color quantization + edge detection composite), stored as a separate GridFS file
    • This protects the child's identity while still showing a personalized image on the public donation page
  • Toggles for showing: bio, interests, destinations

Stripe Configuration:

Configuration is in appsettings.json under the Stripe section:

"Stripe": {
  "PublishableKey": "pk_test_...",
  "SecretKey": "sk_test_...",
  "WebhookSecret": "whsec_...",
  "NonprofitTaxId": ""
}
Key Description
PublishableKey Stripe publishable key (safe for frontend; exposed via GET /api/sponsor/config)
SecretKey Stripe secret key (NEVER commit to source control in production — use environment variables or a secrets manager)
WebhookSecret Stripe webhook signing secret used to verify incoming webhook payloads
NonprofitTaxId Organization EIN included on donation receipts for tax-deductible contributions

Webhook Setup (Stripe Dashboard):

Stripe webhooks notify the backend when a payment is completed. Without a properly configured webhook, donations will remain in Pending status indefinitely.

  1. Navigate to Stripe Dashboard → Developers → Webhooks.
  2. Click Add endpoint.
  3. Set the Endpoint URL to your publicly accessible backend URL:
    • Production: https://your-domain.com/api/sponsor/webhook
    • Local development: Use Stripe CLI to forward events (see below).
  4. Under Events to send, select: checkout.session.completed.
  5. Click Add endpoint.
  6. Copy the Signing secret (whsec_...) from the endpoint details page and set it as Stripe:WebhookSecret in appsettings.json (or your environment config).

Local Development with Stripe CLI:

For testing webhooks locally without a public URL:

# Install Stripe CLI: https://stripe.com/docs/stripe-cli#install
stripe login

# Forward webhook events to your local backend
stripe listen --forward-to https://localhost:5001/api/sponsor/webhook

# The CLI prints a webhook signing secret (whsec_...) — copy it into appsettings.Development.json

The backend gracefully handles a missing WebhookSecret — if the value is empty, webhook payloads are parsed without signature verification and a warning is logged. This is acceptable during development but must be configured in production.

Webhook Event Flow:

Donor completes Stripe Checkout
  → Stripe fires checkout.session.completed event
  → POST /api/sponsor/webhook receives the event
  → Signature verified against WebhookSecret (if configured)
  → Matching Donation record updated: Status → Completed, PaymentIntentId saved
  → For Organization donations: amount distributed evenly among eligible profiles
  → Duplicate webhooks are safely ignored (idempotent)

Donation Lifecycle:

Status Meaning
Pending Checkout session created, awaiting payment
Completed Webhook confirmed successful payment
Failed Payment failed or was cancelled

Test vs. Production Keys:

  • Use pk_test_ / sk_test_ keys during development — no real charges are made.
  • Switch to pk_live_ / sk_live_ keys for production. Update the webhook endpoint URL to your production domain and create a new webhook signing secret.
  • Stripe provides test card numbers (e.g., 4242 4242 4242 4242) for simulating payments.

API Endpoints:

Public (no auth):

  • GET /api/sponsor/profiles — List approved sponsor profiles
  • GET /api/sponsor/profiles/{id} — Individual profile detail
  • GET /api/sponsor/profiles/{id}/progress — Donation progress
  • POST /api/sponsor/checkout — Create Stripe Checkout session
  • POST /api/sponsor/webhook — Stripe webhook handler
  • GET /api/sponsor/config — Stripe publishable key

Authenticated:

  • GET /api/sponsor/settings — Own sponsorship settings

Staff only:

  • GET /api/sponsor/profiles/pending — Pending profiles
  • POST /api/sponsor/profiles/{id}/approve — Approve profile
  • DELETE /api/sponsor/profiles/{id}/approve — Revoke approval
  • PUT /api/sponsor/profiles/{id}/trip-costs — Set trip costs
  • GET /api/sponsor/donations — All donations

Lifecycle:

Parent enables sponsorship → Staff approves → Staff sets trip costs → Profile appears on /sponsor → Donors contribute → Stripe processes payment → Webhook confirms → Donation recorded

Families, Family Funds & Location Funds

Families are first-class sponsorship subjects alongside individuals:

  • Family page — /sponsor/family/:familyKey is an anonymous page showing the family's shared travel dream on the geo-globe (shared destinations accented vs. individual wishes) with per-member cost breakdowns. Family cards (libraries/features/sponsor-cards/) show a persistent goal bar and a Donate link; hovering a member swaps the card's side panel to that member's tabbed details (Locations / About / Awards).
  • Family funds — /sponsor/donate-family/:familyKey pools gifts into a family fund that staff disburse only toward the family's shared trip, never a member's solo trip (FamilyFundsController: ledger, disburse, manual adjust — all staff-side).
  • Location funds — /sponsor/fund/:code lets donors fund a destination rather than a person. Travelers who dream of that destination earn from the pool by completing activities (/sponsor/earn): staff define earn rules, travelers submit claims, staff approve/reject completions, and prizes/ledger/reports round out the admin surface (FundsController).

Key files: SpikerSoft.Api/Domain/Sponsor/{FamilyFundsController,FundsController}.cs, spikersoft-angular/projects/spikersoft/src/app/_components/sponsor-family/, libraries/features/sponsor/src/lib/components/{family-donate,fund-donate,sponsor-earn}/, libraries/features/sponsor-cards/.


Fundraiser System

While sponsorship connects donors to travelers, the Fundraiser system teaches members how to raise their own funds — giving students an introduction to business while working toward their travel goals.

Concept

Members create fundraisers tied to a specific trip destination. Each fundraiser models a real-world business activity (buying and reselling products, bake sales, services, or custom approaches). The system provides:

  • Profit Calculator — Enter item cost, set a selling price via slider or manual input (minimum = cost, maximum = 10x cost), and instantly see profit per unit, margin %, and how many units are needed to reach the goal.
  • "What If" Simulator — A units-sold slider lets members preview partial progress: "If I sell 15 units, I'll earn $X and be Y% toward my goal."
  • Solicitation Map — A Leaflet + leaflet-draw map where members draw polygon zones for where they plan to sell, with recommendation pins for high-traffic areas powered by the Overpass API.
  • Area Recommendations — The backend queries OpenStreetMap for nearby POIs (shops, schools, parks, transit stops) and scores them by density and accessibility.

Fundraiser Types

Type Description
Product Resale Buy items wholesale and sell at a profit
Bake Sale Sell homemade baked goods
Service Based Offer services like car washes or lawn care
Custom Design a unique fundraiser approach

Creation Flow

A 4-step dialog guides members through fundraiser creation:

  1. Type — Select fundraiser type; displays the trip cost as the fundraising goal.
  2. Items — Enter item name and cost (in dollars), use the profit calculator to set a selling price, and save items. The calculator shows units-to-goal and a simulation slider.
  3. Zones — Draw solicitation areas on the map; view AI-suggested high-traffic locations.
  4. Review — Summary of items, profit projections, goal coverage, and zone count before submission.

Access Points

  • Destination Cards — A storefront icon button on each destination card header opens the create-fundraiser dialog pre-filled with that destination's country and trip cost.
  • Profile Menu — "Fundraisers" menu item under Info Vault navigates to /fundraisers.
  • Fundraiser Dashboard (/fundraisers) — Lists all fundraisers with quick stats (total raised, active count, average margin), status filter chips, and progress bars. Click a card to view full detail.

Fundraiser Lifecycle

Status Meaning
Draft Created but not yet active
Active Currently running
Paused Temporarily halted
Completed Goal reached or manually completed
Cancelled Abandoned (soft-deleted)

Backend Architecture (CQRS)

The fundraiser feature follows the existing MediatR CQRS pattern:

Commands: CreateFundraiser, UpdateFundraiser, RecordSale, DeleteFundraiser Queries: GetFundraisers, GetFundraiserById, GetAreaSuggestions Validators: FluentValidation for all commands (country must be alpha-3, selling price >= cost, etc.)

The IFundraiserAreaService interface abstracts the Overpass API integration for area recommendations, with fallback suggestions when the external API is unavailable.

API Endpoints:

Method Endpoint Purpose
GET /api/fundraiser List user's fundraisers (optional country, status filters)
GET /api/fundraiser/{id} Get fundraiser detail
POST /api/fundraiser Create fundraiser
PUT /api/fundraiser/{id} Update fundraiser
PUT /api/fundraiser/{id}/record-sale Record item sales
DELETE /api/fundraiser/{id} Cancel/soft-delete fundraiser
GET /api/fundraiser/area-suggestions Get area recommendations (lat, lng, radiusKm)

All endpoints require authentication.

Data Model

The Fundraiser entity stores in the fundraisers MongoDB collection with nested sub-documents for items (cost, selling price, quantities, computed profit), solicitation zones (GeoJSON polygons), and area recommendations. Indexes on UserId, Status, and (UserId, DestinationCountry) support efficient queries.

Key Files

Layer Files
Entity SpikerSoft.Data/Mongos/Fundraiser.cs
Commands SpikerSoft.Business/Domain/Fundraiser/Commands/
Queries SpikerSoft.Business/Domain/Fundraiser/Queries/
Validators SpikerSoft.Business/Domain/Fundraiser/Validators/
Controller SpikerSoft.Api/Domain/Fundraiser/FundraiserController.cs
Area Service SpikerSoft.Api/Domain/Fundraiser/FundraiserAreaService.cs
Frontend Models _models/fundraiser.model.ts
Frontend Service _services/fundraiser/fundraiser.service.ts
Profit Calculator _components/fundraiser/profit-calculator/
Solicitation Map _components/fundraiser/solicitation-map/
Create Dialog _components/fundraiser/create-fundraiser-dialog/
Dashboard _components/fundraiser/fundraiser-dashboard/

Info Vault

The Info Vault is a personal learning repository where members save, organize, and revisit resources they discover across the platform. Parents can view their children's vault contents.

Item Types

Type Description
External URL Save any web link with optional full-page HTML cache/snapshot
Internal Reference Bookmark to in-platform content (facts, blog posts, books)
Note Rich-text notes for personal learning reflections
File Upload Upload documents and images (stored in GridFS bucket vault-files)

Features

  • Full-page snapshots — Cache external URLs as complete HTML snapshots for offline access
  • Search & filter — Full-text search across all vault items with type and category filters
  • Favorites — Star important items for quick access
  • Stats dashboard — Item counts by type, storage usage, activity trends
  • Parent access — Parents can browse a child's vault items via GET /api/vault/child/{childUserId}

API Endpoints

Method Endpoint Purpose
GET /api/vault List vault items (paginated, filterable)
POST /api/vault Create a vault item
PUT /api/vault/{id} Update a vault item
DELETE /api/vault/{id} Delete a vault item
GET /api/vault/stats Vault statistics
GET /api/vault/search?q= Full-text search
POST /api/vault/{id}/favorite Toggle favorite
POST /api/vault/{id}/cache Cache/snapshot an external URL
POST /api/vault/upload Upload a file (multipart)
GET /api/vault/files/{gridFsId} Download a vault file
GET /api/vault/child/{childUserId} List a child's vault items (parent access)

All endpoints require authentication.

Key Files

File Purpose
VaultController.cs API controller with CQRS handlers
VaultItem.cs MongoDB entity (collection: vault-items)
GridFsVaultFileService.cs GridFS file storage for uploads
info-vault.component.ts Angular vault UI with search, filters, favorites
vault.service.ts Frontend HTTP service

Blog System

Members create travel and learning blog posts that connect to the platform's geography and fact system. Posts go through a staff moderation workflow before publishing, and uploaded media passes through an async processing pipeline.

Moderation Workflow

Author creates post → Status: Draft
       ↓
Author submits for review → Status: Pending
       ↓
Staff reviews at /admin/blog-moderation
       ↓
┌──── Approve → Status: Published (visible on platform)
└──── Reject → Status: Rejected (author notified with reason)

LinkedFact Integration

Blog posts can be linked to geographic facts via LinkedFact references (factId, countryCode, countryName). This creates a bidirectional connection: the geography explorer shows related blog posts for a fact, and blog posts link out to the interactive map.

Media Processing Pipeline

Uploaded blog media flows through an async RabbitMQ pipeline in SpikerSoft.EventHandlers.BlogMediaProcessor:

  1. Received — File accepted and queued
  2. ClamAV Scan — Virus/malware scanning
  3. Metadata Extraction — EXIF data, dimensions, format detection
  4. Strip — Remove sensitive metadata (GPS coordinates, camera info)
  5. Move — Transfer to final storage location

Each step updates the media's ProcessingStatus, and failures are isolated per-step.

API Endpoints

Method Endpoint Purpose
GET /api/blog/posts List posts (paged, filtered by status/country/tag)
GET /api/blog/posts/{id} Get post detail
GET /api/blog/posts/by-fact/{factId} Posts linked to a geographic fact
GET /api/blog/posts/by-country/{countryCode} Posts about a country
POST /api/blog/posts Create post (JSON)
POST /api/blog/posts/upload Create post with media (multipart, up to 12 files)
PUT /api/blog/posts/{id} Update post
DELETE /api/blog/posts/{id} Delete post
POST /api/blog/posts/{id}/media/upload Upload additional media
POST /api/blog/posts/{id}/comments Add a comment
POST /api/blog/posts/{id}/approve Approve post (Staff/Admin)
POST /api/blog/posts/{id}/reject Reject post (Staff/Admin)
GET /api/blog/posts/pending Pending moderation queue (Staff/Admin)
GET /api/blog/posts/my Author's own posts

Key Files

File Purpose
BlogController.cs API controller with moderation endpoints
BlogPost.cs MongoDB entity with media, comments, LinkedFacts
LinkedFact.cs Geography fact reference sub-document
BlogMediaOrchestrator.cs RabbitMQ pipeline coordinator
blog.service.ts Frontend HTTP service

A personal photo gallery built for real camera workflows — per-frame uploads of RAW .ORF plus developed TIFF/JPEG with EXIF retained end-to-end, not just phone snapshots.

libraries/features/photo-gallery/ renders three views: a thumbnail wall, cards, and a geotagged map (photo-map), with a lightbox. The gallery is stack-aware: frames consumed by a Stacked or Touchup photograph (published from the Desktop Stacker) collapse behind a representative burst tile (grouped by upload session), and a provenance endpoint returns the source frames in burst order (GetPhotographProvenance). A photo-info dialog exposes sanitized metadata.

Tags

  • Manual tags: PUT api/photos/{id}/tags replaces the owner's tag list (max 50 tags × 64 chars). For minors, tagging is gated on the parental Photography permission.
  • AI auto-tags: the Florence-2 GPU lane tags photographs automatically; POST photos/{id}/auto-tags/reject moves a tag into RejectedAutoTags so re-tagging can never resurrect it.
  • GET api/photography/tags returns the caller's tag universe with counts, feeding the filter chips.

Gear registry

The profile's Photography tab shows the user's cameras and lenses — derived server-side from EXIF serial numbers of their uploads (GET api/photography/gear); nothing is self-reported.

Pipeline

  • SpikerSoft.EventHandlers.PhotographProcessor runs the async photo pipeline: receive → ClamAV scan → metadata extract → move to final storage (MinIO).

Clipboard paste

@spikersoft/ui-image-paste provides a document-level ssImagePaste directive: Ctrl/Cmd+V with images on the clipboard emits File[] into the surface's existing validation funnel (with accept-style filtering and auto-renaming of generic image.png screenshots). Wired into 11+ upload surfaces — blog posts, info vault, profile Photos tab, photo gallery, art prompt composer, passport scan, QR code, image converters, geography facts, and the child-account dialog.

Key files: SpikerSoft.Api/Domain/Photography/PhotographyController.cs, SpikerSoft.Business/Domain/Photography/Queries/{GetMyPhotographs,GetPhotographProvenance}/, spikersoft-angular/libraries/features/photo-gallery/, libraries/ui/image-paste/.


Reading Journey & Book System

The Reading Journey is the platform's digital library and learning hub. Members upload books (PDF or EPUB), which pass through an AI-powered processing pipeline that generates metadata, embeddings, and comprehension quizzes. The reader supports bookmarks, progress tracking, and multiple reading profiles.

Book Processing Pipeline

User uploads PDF/EPUB → Staff approval queue
       ↓
Staff approves → Processing pipeline begins
       ↓
┌──── MetadataExtractor → Extract title, author, page count, TOC
├──── Embeddings → Chunk text, generate vector embeddings (LLamaSharp)
├──── QuizGeneration → LLM-powered comprehension quizzes per chapter
└──── SecurityScanner → Content safety check
       ↓
SignalR notifications update the user in real time (BookProcessingStage)
       ↓
Book available in library

Reader Features

  • EPUB reader — Chapter navigation, page rendering, embedded resource loading
  • PDF viewer — Full document rendering with page tracking
  • Progress tracking — Automatic page + scroll position saving per book
  • Bookmarks — Save and annotate specific locations
  • Reading preferences — Font size, theme, layout customization
  • Reading profiles — Multiple reading profiles per user (e.g., "Study", "Leisure")
  • Recently read — Quick access to books in progress
  • Quizzes — AI-generated comprehension quizzes tied to book chapters

API Endpoints

Book Management (api/book):

Method Endpoint Purpose
GET /api/book/all List all accessible books
GET /api/book/{id} Book detail
POST /api/book Create book metadata
POST /api/book/process/upload-pdf Upload PDF for processing
GET /api/book/{id}/epub-chapters EPUB chapter list
GET /api/book/{bookId}/epub-page Render an EPUB page
GET /api/book/{bookId}/epub-resource Serve EPUB embedded resources
GET /api/book/{id}/file Download original file
GET /api/book/public Public book catalog
GET /api/book/my-books User's uploaded books

Reader (api/reader):

Method Endpoint Purpose
GET/PUT /api/reader/progress/{bookId} Read/save reading progress
GET /api/reader/recently-read Recently read books
GET/PUT /api/reader/preferences Reading preferences
GET /api/reader/bookmarks/{bookId} Bookmarks for a book
POST /api/reader/bookmarks Create bookmark
DELETE /api/reader/bookmarks/{bookmarkId} Delete bookmark
GET/POST /api/reader/profiles List / create reading profiles
DELETE /api/reader/profiles/{profileId} Delete a reading profile

Semantic search & vector indexes

api/books/search (GET + POST + stats, BookSearchController) runs semantic search over the embeddings the pipeline generates, and api/vector-indexes (VectorIndexController) gives staff CRUD/inspection over the Redis vector indexes backing it.

Configurable quiz generation

Staff tune quiz generation at book-approval time (quiz-options-dialog in admin book management): model preset (Fast / Balanced / Quality), target difficulty, question style, questions-per-page, and max pages. The options are serialized onto the workflow (QuizOptionsJson on ApproveUpload) and replayed on retry, so a re-run grades with the same settings.

Key Files

File Purpose
BookController.cs Book CRUD, upload pipeline, EPUB endpoints
ReaderController.cs Progress, bookmarks, preferences, profiles
reading-journey.component.ts Frontend library + upload + quiz UI
reader-shell.component.ts Book reader shell (EPUB + PDF)
reader.service.ts Frontend reader HTTP service

Real-time Communication

SpikerSoft provides four real-time communication channels: video calling, live streaming, platform-wide chat, and 1:1 direct messages.

Video Calling

Peer-to-peer browser video calls (WebRTC) with authenticated, first-party signaling — the old PeerJS public broker (which had no auth and no permission gating) is gone:

  • Signaling runs over the authenticated VideoCallHub SignalR hub at /hubs/video-call: opaque SDP/ICE forwarding, room pairing, and an abandonment grace window.
  • VideoCallController issues short-lived coturn TURN credentials (HMAC use-auth-secret, default 600 s TTL) so media can relay through our own TURN server when a direct path fails.
  • One-on-one calls with camera/mic controls and a call picker dialog; access is gated by a videoCall feature permission.
File Purpose
SpikerSoft.Contracts.SignalR/VideoCallHub.cs Signaling hub — SDP/ICE forwarding, room pairing
SpikerSoft.Api/Domain/VideoCall/VideoCallController.cs Short-lived TURN credential issuance
video-call.component.ts Call UI with local/remote video streams

Live Streaming

Live video streaming using OvenPlayer (WebRTC client) connected to an OvenMediaEngine instance. The media server runs as a separate Docker service behind Traefik.

  • WebRTC source: wss://stream.spikersoft.com/app/stream/
  • Low-latency streaming for events and demonstrations
  • OvenPlayer embedded in the Angular StreamComponent
File Purpose
stream.component.ts OvenPlayer integration with WebRTC source

Chat

Real-time messaging via SignalR hubs. Two hub versions exist:

  • ChatHub (/hubs/chat) — Original chat hub
  • MultiTenantChatHub (/hubs/chat-v2) — Multi-tenant chat with organization-scoped rooms

Chat is entirely hub-based with no REST API; all message exchange happens over WebSocket.

File Purpose
ChatHub.cs SignalR hub for real-time messaging
MultiTenantChatHub.cs Multi-tenant SignalR hub (v2)
chat.component.ts Angular chat UI
chat.service.ts SignalR connection and message handling

Direct Messages

Distinct from chat rooms: 1:1 private messaging with a REST surface plus live delivery.

  • A global chat FAB + drawer (messaging-fab / messaging-drawer) with per-conversation unread counts.
  • The contact roster is relationship-derived (parent/child family links), and MessagingAccessRules enforces a role/family-scoped permission matrix — who may message whom is a policy decision, not an open directory.
  • Delivery rides the existing NotificationsHub (ReceiveDirectMessage), so no extra WebSocket connection.
Method Endpoint Purpose
GET /api/messaging/contacts Who the caller may message
GET /api/messaging/conversations Conversation list with unread counts
GET /api/messaging/conversations/{id}/messages Message history (paged)
POST /api/messaging/messages Send a message
POST /api/messaging/conversations/{id}/read Mark read
GET /api/messaging/unread-count Global unread badge

Notifications

A centralized notification system bridges async backend events to real-time client updates via SignalR:

  • NotificationsHub (/hubs/notifications) — General platform notifications
  • Calendar notifications (/hubs/calendar-notifications) — Calendar-specific reminders
  • RabbitMQ bridge — SignalRNotificationConsumerService forwards RabbitMQ messages to connected clients via the hub
  • Retry tiers — Failed notifications retry through notifications.signalr.retry.1/2/3 queues before hitting the DLQ

Event handlers (quiz generation, embeddings, uploads, etc.) publish notifications to the notifications.signalr RabbitMQ queue, which the bridge forwards to the appropriate SignalR clients.


Event Handlers

SpikerSoft uses a distributed event handler architecture where specialized microservices consume messages from RabbitMQ queues. Each handler is a standalone .NET worker service built on the shared EventHandlerHostBuilder infrastructure.

Handler Inventory

Handler Purpose
Infrastructure Shared library providing EventHandlerHostBuilder with Serilog, MongoDB, RabbitMQ, and OpenTelemetry wiring (not a runnable service)
ArtPipeProcessor GPU art-pipeline worker — runs artpipe stages/models under GPU leases (see Art Studio); also hosts the QR Art and photo auto-tag lanes
ArtStudioMetrics Single-replica CQRS projection folding art.asset.lifecycle events into the Art Studio metrics read model
BlogMediaProcessor Async blog media pipeline: receive → ClamAV scan → metadata extract → strip EXIF → move to final storage
BookManagement Book lifecycle side effects triggered by processing events
CalendarReminders Calendar reminder notification dispatch
CodeExecution Sandboxed user code execution worker (request/response queues, 4 concurrent consumers; also the lesson-regrade RPC lane)
Decompile RabbitMQ direct-reply-to RPC into ICSharpCode.Decompiler (the decompiler dev tool)
DockerMonitor Docker host monitoring and event publishing to Redis
Embeddings Vector embedding generation for book text chunks (LLamaSharp, GPU-scheduled)
FileMovement File transfer between staging and final storage locations
GameEvents Game state persistence and Redis hot-path dual-writes for the game server
GpuCoordinator Cluster-wide GPU VRAM lease broker, queue scheduling, and /gpu/status HTTP API (see Art Studio)
ImageDescription.Python Vision-model image description for book images (Qwen-VL, GPU-scheduled)
InfluxDashboard InfluxDB metrics ingestion and DashboardHub SignalR streaming
KeycloakEvents Keycloak identity event synchronization into the platform
LessonVideoProcessor Lesson video processing + moderation pipeline
MetadataExtractor PDF/EPUB metadata extraction (Aspose, VersOne.Epub)
NodeAgent One replica per swarm node: tails the systemd journal + samples host health/disk into system.events (see Ops, Fleet & Status)
Notifications SMS / voice / phone-lookup / verify lanes (notifications.{sms,voice,verify,phone}.*)
Ocr Passport-OCR RPC lane (ocr.passport.*)
PhotographProcessor Photo pipeline: receive → scan → metadata extract → move (see Photography & Gallery)
QuizGeneration LLM-powered quiz generation from book content (GPU-scheduled)
Scheduler Scheduled task execution (GeoIP updates, HTTP callbacks)
SecurityMonitor Redis-backed stateful detectors over system.events producing security alerts
SecurityScanner Uploaded content security scanning
SystemRemediation Rule engine over system events: dedupes incidents, dispatches curated SAFE remediations or alerts operators
UploadCoordinator Orchestrates the full book/upload lifecycle across processing stages

Plus three non-handler worker libraries consumed by the handlers: SpikerSoft.Workers.{Decompile,Notifications,Ocr}.

Production Queue Architecture

The queue set is code-derived and grows with the handler fleet — treat the handler inventory above as the map, not a frozen queue list. The major lane families:

Lane family Purpose
code.execution.* + lesson.regrade.* Sandboxed grading (live submit + offline re-grade RPC)
book.create.requests, image.extraction, metadata.extraction.requests, quiz.generation, upload.lifecycle, file.operations, security.scan.requests Book/upload processing pipeline
game.events (+ game.events.dlx), game.persistence.events (+ dlq) Game server persistence
art.asset.*, art.model.*, artpipe.* Art Studio stage dispatch + per-model queues + lifecycle events
gpu.lease.* GPU coordinator lease protocol
photograph.* Photo pipeline
decompile.*, ocr.passport.* Direct-reply-to RPC lanes
lesson.video.* Lesson video processing
notifications.signalr (+ retry tiers .retry.1/2/3 + .dlq), notifications.{sms,voice,email,verify,phone}.* Notification delivery
system.events, security.alert.* Host-fleet monitoring and remediation
calendar.reminders, keycloak.events Calendar + identity sync

Key Files

File Purpose
EventHandlerHostBuilder.cs Shared builder with Serilog, Mongo, Rabbit, telemetry
SignalRNotificationConsumerService.cs RabbitMQ → SignalR notification bridge
NotificationsHub.cs SignalR hub for client notifications
DashboardHub.cs SignalR hub for InfluxDB metrics streaming

Marks Field Service (legacy CRM)

The original single-tenant field-service CRM for the Mark Wilson generator-service business: residential and commercial customers with embedded addresses, generators, and service history, plus a Google Maps workflow for visualizing customer locations. Its multi-tenant successor is Time & Materials below — the two surfaces coexist; this is not a completed migration.

Features

  • Customer CRUD — Create, view, update residential and commercial customer records
  • Google Maps integration — Visualize customer locations and plan service routes
  • Equipment tracking — Generator and propane tank inventory per customer
  • Service contracts — Track service agreements and maintenance schedules
  • Address management — Structured addresses with postal code lookup integration

Scope

The backend surface is far larger than one controller: MarkWilsonsCustomerController (route /api/mark-wilson/customer) plus supporting domains ServiceCalls, ServiceCallReasons, Employees, Parts, Labors, Milages, FuelTypes, GeneratorBrands, AddressTypes, PhoneTypes. The frontend lives under _components/marks-site/ (marks-landing-page, marks-customer-list, marks-generator, marks-propane-tank, marks-service-call, marks-service-contract, marks-address).

Auth note: because this CRM shares the main Keycloak realm with student/consumer accounts, all of it is gated Admin/Staff — a plain [Authorize] here once let any platform user read customer PII, fixed in spikersoft-issues#688's follow-ups.

Key Files

File Purpose
CustomersController.cs API controller for customer CRUD (MarkWilsonsCustomerController inside)
marks-customer.component.ts Customer management UI
marks-map.component.ts Google Maps customer visualization
libraries/marks-site-models/ Shared TypeScript models

Time & Materials (api/tm)

The multi-tenant, multi-asset-type successor to the Marks CRM: a full field-service/billing bounded context in the backend plus its own native Android app (spikerj/spikersoft-time-and-materials).

The Android app

One binary, two roles — and the role is data (on TmProfile), not a Keycloak role, so any authenticated realm user can onboard:

Role Bottom nav Capabilities
Service tech Schedule · Jobs · Customers · Map · More Customers, typed assets, jobs with time/materials/travel entries, availability calendar, route plans, agreements, invoices
Customer Appointments · Book · Equipment · More Own sites/equipment, self-booking against a preferred tech or the first open slot, agreements, invoices

Tech stack mirrors the main Android app (Compose M3, Hilt, Retrofit, Room, WorkManager, MapLibre 13.4.1, AppAuth), with its own Keycloak PKCE client spikersoft-time-and-materials on the same realm.

Backend (SpikerSoft.Api/Domain/TimeAndMaterials/)

Seven controllers over MediatR CQRS and Mongo Tm* documents (Profile, Business, Customer, Job, Invoice, Agreement, Availability, Material, RoutePlan, AssetType):

Area Controller Highlights
Profile & onboarding TmProfileController onboard, join-business, role change
Customers & assets TmCustomersController customers, pluggable asset-type catalog (TmAssetTypeCatalog — "generator" is just one entry, with type-specific checklist items like load_test/hours)
Jobs TmJobsController jobs + time/material/travel entries + attachments
Scheduling TmSchedulingController availability, open slots (TmSlotCalculator), appointments with reschedule/cancel
Agreements TmAgreementsController service agreements
Route plans TmRoutePlansController day-route planning (TmRoutePlanner)
Invoices TmInvoicesController invoicing + status

The app's one non-/tm call is the shared Geocoding controller. There is no web frontend for /api/tm yet — the Android app is the only consumer.


Health Check & Observability

The platform exposes a comprehensive /healthz endpoint that validates the entire infrastructure stack. This endpoint powers uptime monitoring and aids rapid diagnosis during incidents.

Health Checks (13 total)

Registered in SpikerSoft.Api AddHealthChecksConfiguration (order in code: RabbitMQ, MongoDB, MongoDB Indexes, Redis Cluster, Vector Search, Keycloak, InfluxDB, Email Server, DNS Server, DKIM, DMARC, SPF, Postal Code Data).

Check What It Validates
MongoDB Cluster reachability via mongo-router, ping latency, database list
MongoDB Indexes Expected indexes exist on the collections that need them
Redis Cluster All 6 nodes connected (3 masters + 3 replicas), cluster state ok, slot coverage
RabbitMQ Queue health, consumer counts, message depth, DLQ status
Vector Search Redis vector index exists (default name idx:BookPageChunk, overridable via VectorSearch:IndexName)
Keycloak Authentication endpoint reachable at ids.spikersoft.com
InfluxDB Ping, bucket existence, write/query round-trip at influxdb.spikersoft.com
Email Server SMTP connection to mail.spikersoft.com:587, STARTTLS, authentication
DNS Server Self-hosted DNS at 192.168.0.105:5380 responding
DKIM DKIM record valid for spikersoft.com (selector: mail, RSA key)
DMARC DMARC policy active (quarantine) with aggregate reporting
SPF SPF record valid with -all (hard fail for unauthorized senders)
Postal Code Data MongoDB postal-codes collection has rows; Degraded if empty (seed via POST /api/PostalCodes/seed) — see PostalData_HealthCheck

Observability Stack

┌─────────────────────────────────────────────────────────────┐
│                      OBSERVABILITY                           │
├─────────────────────────────────────────────────────────────┤
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │   Serilog    │  │ OpenTelemetry│  │  InfluxDB    │      │
│  │      ↓       │  │      ↓       │  │   v2.7.12   │      │
│  │     Seq      │  │    Jaeger    │  │  (metrics)   │      │
│  │  (logging)   │  │  (tracing)   │  │              │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
│                                                              │
│  ┌──────────────────────────────────────────────────┐      │
│  │  /healthz — 13 infrastructure checks              │      │
│  │  RabbitMQ DLQ monitoring + retry tier escalation   │      │
│  │  Correlation IDs across all cross-service calls    │      │
│  └──────────────────────────────────────────────────┘      │
└─────────────────────────────────────────────────────────────┘

Email Deliverability

The health check validates the full email authentication chain:

  • SPF — v=spf1 a mx ip4:204.197.150.99 -all (hard fail policy)
  • DKIM — RSA-signed with selector mail on spikersoft.com
  • DMARC — v=DMARC1; p=quarantine with aggregate reports to dmarc@spikersoft.com

This ensures @spikersoft.com emails (child account provisioning, notifications) are trusted by recipients and not flagged as spam.


Ops, Fleet & Status

Beyond app-level health checks, the platform watches its own host fleet and surfaces incidents both to operators and to the public.

Host-agent pipeline

NodeAgent (1 per swarm node)          SecurityMonitor              SystemRemediation
  tails systemd journal        →   Redis-backed stateful    →   rule engine over events:
  samples host health/disk         detectors producing            dedupes SystemIncident read-models,
  → publishes system.events        SecurityAlerts                 dispatches a curated SAFE remediation
                                                                  command back to the node, or alerts operators
  • SpikerSoft.EventHandlers.NodeAgent — journal tail + host health/disk sampling into system.events.
  • SpikerSoft.EventHandlers.SecurityMonitor — stateful detectors (Redis-backed) that turn raw events into SecurityAlerts.
  • SpikerSoft.EventHandlers.SystemRemediation — the rule engine; only curated, safe remediation commands are ever dispatched automatically.

Ops console & public status

  • Ops console — read-only Mongo read-models over incidents and security alerts (OpsIncidentsController, OpsSecurityAlertsController). Authorization is deliberately stricter than the usual admin tools: IsDevOpsOrAdmin, with staff explicitly denied.
  • Public status page — /status (libraries/features/status/) is a no-auth, read-only page for school operators: current platform state, no actions. Distinct from the admin console.

More Platform Features

Shorter notes on features that have their own code surface but don't need a full section:

Skills & Badges

Skill definitions with per-user awards (SkillsController): award-by-interest lookup, manual awards, and auto-award checking. Surfaced as the profile Skills tab, ui-award-chips throughout the UI, and /admin/skill-management.

Calendar & availability

A /calendar route + CalendarController, fed by the profile Schedule tab's weekly availability; reminders dispatch through the CalendarReminders handler.

Geography explorer globe

The geography explorer now lands on the shared WebGL globe (behind @defer) with fact-count hover labels; the old sidebar-cards view is one of four switchable layouts, and the interest filter seeds from the user's rated profile interests.

Tree of Knowledge

/tree-of-knowledge (+ /tree-of-knowledge/domain/:domainKey) visualizes per-domain proficiency computed from tracked activity across the platform's learning surfaces.

Admin surfaces

Beyond those documented above: /admin/activity-coverage, /admin/platform-adoption, /admin/interests, /admin/geography-facts, /admin/skill-management, /admin/contact-messages, /admin/lesson-video-moderation, and the five /admin/art-* routes (Art Studio).

Assorted routes

/ai (AI assistant), /classes, /links, /mission, /syllabus, /sms-consent, /our-hardware (Dreamstream cluster dashboard), /organization/confirm, /parental-approve/:token, /chemistry, and the exploratory /brain-forge, /cortex-quest, /wisdom-well.


TODO / Future work

Planned or exploratory integrations not yet in the product stack:

  • Apache Guacamole (HTML5 remote desktop) — Run a Guacamole server and pair it with a custom Angular client UI: a from-scratch or heavy rewrite of the official web client, using the guacamole-client next branch frontend as a behavioral and protocol reference (not necessarily a line-for-line port), aligned with SpikerSoft’s stack and theming.
  • OpenSC2K-style city sim (optional game) — Investigate embedding or forking ideas from OpenSC2K (WebGL / Phaser) as a SimCity 2000–style learning game or creative sandbox, subject to licensing, asset, and product-fit review (the upstream project is GPL-3.0; ship only compliant code and assets).
  • PHP in the browser (WASM) — Add a PHP learning / playground path parallel to the existing C# and Python storylines, using php-wasm (PHP in the browser via WebAssembly), preloaded and status-tracked like other WASM runtimes in the Offline panel where applicable.
  • TypeScript curriculum (extension of the JavaScript track) — JavaScript playground + lessons are already shipping (Node server + QuickJS-WASM browser). The follow-on TypeScript curriculum reuses the same runtime via LessonMetadata.Preprocessor = LessonPreprocessor.TypeScript: both Node and the QuickJS worker call ts.transpileModule before grading. Add a "Transpile to JavaScript" action in TypeScript lessons that uses the TypeScript compiler in the browser (same API as the canonical typescript package ships in lib/typescript.js — the implementation currently referenced at unpkg.com/typescript@latest/lib/typescript.js). Dependency policy: add typescript as a first-party pnpm dependency and serve the browser bundle from our own build/static assets (or vendor the built file in-repo), so no runtime dependency on a third-party CDN for the compiler; unpkg is only the upstream reference for which artifact to align with.

Contributing

  1. Follow the coding guidelines in .cursor/rules/
  2. Write tests for new features
  3. Update documentation as needed
  4. Submit pull requests for review

License

Licensing is not uniform across the monorepo; use the SPDX / files in each part of the tree:

Earlier text here referred to an “Academic License” and a single LICENSE at repo root — there is no shared root license file in this workspace layout; rely on the paths above.


Helping every student discover what moves them — with families learning together and sponsors funding the journey.

S
Description
No description provided
Readme
520 KiB
0 Stars 1 Watchers 0 Forks