New major sections: Art Studio (pipeline, GPU coordinator, per-model lanes, storage), Asset-to-Game Integration, Photography & Gallery, Native Mobile Apps, Activity Tracker/Routes/Paths, Godot Games Client, Desktop Stacker, Time & Materials, Direct Messages, Ops/Fleet/Status, C/C++/SQL/x86 tracks, sponsorship families/funds, and a More Platform Features roundup. Stale-fact sweep: Angular 22/TS 6/Nx 23, native apps no longer 'Future', PWA shipped, PeerJS replaced by VideoCallHub + coturn TURN, TileServerGL retired for PMTiles-on-MinIO, health-check set corrected, 102 C# lessons, lesson code moved to SpikerSoft.Business.CodeExecution, per-project test projects via SpikerSoft.UnitTests.slnf, library counts 84 across 6 layers, 28 event handlers inventoried, queue lanes updated, hiring page master-detail, data-driven menu bar, Marks CRM vs api/tm relationship. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DoTHPhXe2qAAeQfs2itQb9
SpikerSoft Platform
A travel-based assisted learning platform that fundraises for children's global exploration. SpikerSoft bridges outdoor adventure with modern technology -- structured education meets hands-on STEM, where families explore the world together and sponsors fund the journey.
Think Boy Scouts meets Geek Squad in the jungle.
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 is fully offline — │
│ │ Android app │ │ (Rust, offline) │ no backend connection) │
│ └────────┬─────────┘ └──────────────────┘ │
└───────────┼─────────────────────┼─────────────────────┼─────────────────────┘
│ 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-hudframework (no DOM overlays; see gl-hud) - In-scene options menu with rebindable keys (
KeybindingManager, server-synced viaapi/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 inprojects/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
- Deploy a robot from your spacecraft onto a planet surface
- Program it using Blockly (drag-and-drop blocks) or Rete (node graph editor) -- directly in the game world
- Compile the visual program into a typed instruction list (client-side)
- Upload the instructions to the server for secure execution
- 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
resourceschannel - Gatherer robots listen on
resourcesand 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: Pipeline · Library · Metrics · Builder · Photo Stack.
- 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, andPOST api/artstudio/{id}/select-conceptresumes 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-variantruns a "Material Variation (Paint 2.1)" retexture that preserves the source topology/UVs and produces a sibling asset (blobs copied,VariantOfAssetIdprovenance) — 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.lifecycleevents) 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.
- 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 (RemixOfAssetIdprovenance) and rated. - Build chess piece sets — one theme fans out into six per-piece prompts (
api/artstudio/chess-sets); sets can be shared, moderated, and equipped into the chess game. See Asset-to-Game Integration. - Photo Stack (focus stacking) — pick 2–60 frames from the photo gallery (grouped by upload session), choose a fusion engine, watch
develop → align_fuserun, and get a fused JPEG preview plus 16-bit TIFF downloads. Photo Touch-Up then offers Zerene-style retouching: paint aligned source-frame pixels through a soft brush over the fused composite; the result saves back to the photo gallery as a new Touchup photograph (the original is never modified). - 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-auditcollection). AllowStudentMethodChoicedefaults 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.jsonmanifest 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), photo (photostack), 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; TIFF path for PhotoStack). 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/statuson port 8090 (not exposed through Traefik). - Two permanent GPU lanes: the 4090 node (RTX 4090, 24,576 MB budget, permanent holder of the
artpipe-gpuplacement label — runs the artpipe model stacks and image description) and SERVER (RTX 3070 Ti, 8,192 MB — runs embeddings, quiz generation, and the CPU-only PhotoStack lane). Lease requests carryrequiredNodeIdso 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 |
photostack |
develop + align_fuse, CPU-only (BypassGpuLease=true, pinned to SERVER) |
0 |
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) andart-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/chess-set provenance, method selections + provenance, ratings),art-asset-stage-runs(per-run state/timings/retries),chess-piece-sets,darkroom-develop-jobs,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): imagespng|jpeg|webp|tiff, meshesglb|gltf|obj|fbx|ply|stl|usdz, videomp4|webm; texturing images classify into a PBR map taxonomy. The only wired export action today isexport_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 ontooLarge/loadFailed. Custom meshes are never tinted — each side is its own set. - Chess piece sets are a first-class domain:
CreateChessPieceSetCommandHandlerfans 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-pickerwith lazyimport("@spikersoft/feature-art-studio"), explicit disposal per slot, and atooHeavytoast. - The voxel game is not yet connected to the Art Studio path, and the older
3d-modelscollection (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()returningPASS:/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 byJavaScriptChallenge_ReferenceSolutionPasses. - Regex Playground — 12-chapter curriculum graded fully in-browser against a pre-computed
RegexLessonPlanand 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.
RegexLessonRunnerServiceandRegexLessonGraderService(inlibraries/features/dev-tools-reg-ex) feed the student's pattern through the browser's nativeRegExpagainst 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,RegexLessonGradingExecutorbuilds a JS harness viaRegexLessonHarnessBuilderand runs it on the worker's Node subprocess (the sameJavaScriptLessonExecutorplumbing the JS Playground uses). This is not a casual choice — JavaScript regex differs from .NET'sSystem.Text.RegularExpressionsin ways that matter for the curriculum: thev-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.csprojexcludesRegex*.csfrom 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 throughCCodeRunnerController/CppCodeRunnerController/SqlCodeRunnerController. - x86 playground (
libraries/features/dev-tools-x86-playground/): the Blink-based emulator with a GDB-like register/memory/disassembly debugger; supports GNUas, 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.csprojlink-includesDomain/CodeExecution/Execution/**/*.csfromSpikerSoft.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 inIOptions/ILogger/Microsoft.Extensions.Servicesthat 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 visitTutorialPanels— interactive, narrated walk-throughs presented before the gradable challengeTestCode— 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 asMetadataReference.CreateFromImagereferences — no unwrapping step required. ConcurrentBuildis forced off underOperatingSystem.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
TestCodeuses 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 —
completedLessonsSignalupdates 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
Setunion, never overwritten. This eliminates a race where a slowprogressSync.enqueueround-trip could re-lock a just-completed lesson if the server refresh landed before the persist call did. - Service-worker freshness —
/api/Lessons/progressuses thefreshnessstrategy (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:
- The runner auto-routes new submissions through the WASM grader.
- Successful completions are queued in IndexedDB by
progress-sync.service. - On reconnect, the queue flushes to
POST /api/Lessons/progress/batch(batched items, each withlessonNumber+attemptToken). - 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, thencache.putinto a dedicated named cache (spikersoft-offline-preload-v1) so status checks succeed even when the Angular service worker is off (typicalng 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:OfflineCacheServiceparses the JSON embedded in_framework/dotnet.jsafter eachdotnet 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-trips1+1throughrunPythonAsyncso 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 viaensureReady(). Same residency benefit as Pyodide. - DuckDB lazy-imports
@duckdb/duckdb-wasm, instantiates a throwaway engine in a fresh worker, runsSELECT 1, then tears the worker down — no shared singleton to keep alive (each consumer of DuckDB owns its ownAsyncDuckDB), 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. - Pyodide loads via
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/** RoslynCodeExecutoror anything else underSpikerSoft.Business.CodeExecution/Domain/CodeExecution/Execution/- The hint analyzer
WasmCompilerEntry.csor the[JSExport]surface- The
Microsoft.CodeAnalysis.CSharppackage 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 runsassembleDebug+ 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 17CLLocationUpdate.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 foregroundLifecycleService(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) underSpikerSoft.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;
Movecommands 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 fromdirection * speed * throttle(coast/drift is produced client-side by decaying throttle);ZoneJoinedcarriesplayerId, notentityId;EntityBatchDeltauses binarypackedDeltas;AsteroidMovedis a batch;RadarContactUpdateis a full snapshot;ToggleStealthis currently a server-side no-op.
Embedding in the mobile apps
- Android:
GamesActivity extends GodotActivity; JWT +mode/vs/difficultyare injected throughSpikerSoftHostPlugin(aGodotPluginexposingget_access_token(),get_mode(),request_exit(), …). A GradlesyncGodotProjecttask copies the sibling../spikersoft-games-godotcheckout intoapp/src/main/assetson every build. - iOS: SwiftGodotKit loads
Resources/games.pck(exported withgodot --headless --export-pack). SwiftGodotKit has no plugin channel, so the handoff is file-based: Swift writeshost_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 writesuser://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 toDisplayServer.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, fully offline focus-stacking application (Zerene/Helicon class) for macro photography — one self-contained executable per OS (macOS / Windows / Linux). It ports the platform's PhotoStack pipeline (artpipe align.py + fusion.py) to a fast desktop workflow; algorithm parity with the Python implementation is verified by a test harness in the repo.
Workflow stages
- 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.
- Import —
.ORFand other RAW/TIFF/JPEG; instant previews from embedded JPEGs; OlympusFocusStepCountordering and duplicate curation. - Align — phase-correlation seeding + chained pyramidal ECC-style affine refinement against a reference frame.
- 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.
- Stack — selectable fusion engines: Laplacian pyramid and depth map.
- 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.
- Export — 16-bit TIFF / JPEG + a JSON
.sstackproject 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).
Platform integration: none, by design. There is no S3 client, no Keycloak, no api.spikersoft.com anywhere in the workspace — it shares algorithms with the platform's Photo Stack lane, not services. Output stays local.
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 byplatform/language-runnerso the shared playground shell never hard-imports the Monaco-coupledProgressSyncServiceconcrete (thefeature/dev-tools-csharp-runnershell wires it up at provider time).AVIF_ENCODERport —domain/blogandfeature/dev-tools-image-to-avifconsume the encoder through a port declared inshared, so the domain lib never depends directly onplatform/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-versionfor 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- Developmentenvironment.production.ts- Production
Backend:
appsettings.json- Base configurationappsettings.Development.json- Development overridesappsettings.Production.json- Production settingsappsettings.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:
- At 18:
IsMinoris set tofalseon 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. - At 19: The
AccountStatusis set toRequiresConversion. A mandatory (non-dismissable) dialog is shown on login with two options:- Convert: Sets
IsChildAccount = false, clearsParentalControls, setsAccountStatus = Active. The account becomes a full independent individual account with all data preserved. - Decline: The Keycloak user is disabled (
enabled: false) andAccountStatusis set toInactive. The user is logged out.
- Convert: Sets
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 aRoleGuardrequiring theAdminorStaffKeycloak 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
IsParentflag is preserved (once a parent, always a parent). If the email mailbox deletion fails (e.g., database offline), aDeleteEmailMailboxprovisioning 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
- Failure Logging: When a third-party operation fails (e.g., creating a
username@spikersoft.commailbox during child account approval), the error is captured as aProvisioningTaskin MongoDB with statusFailed, along with all metadata needed to retry the operation later. - 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/countendpoint. - 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.
- 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
canDeactivateroute guard intercepts navigation away from the profile page. - Browser close/refresh: A
beforeunloadhandler 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:
- a shared title prefix, e.g.
[Scheduling] Backend: recurrence expansion, so the whole unit is one search away; and - a sibling cross-link block in every issue body listing the unit by full reference.
- a shared title prefix, e.g.
- Reference issues across repos with the full
owner/repo#Nform. A bare#Nresolves 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:
- Enable the integration — In Cursor Settings → MCP, ensure the Gitea server is on and configured (Instance URL, access token, and path to the
gitea-mcpcommand if you use the standalone binary). The token must have API scope to read and create issues in the org/repos you use. - 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.
- 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 onspikerj/spikersoft-angular#12summarizing the repro”, or “Search repos named …” (the exact tool surface depends on yourgitea-mcpbuild; 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. - 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:
/healthzendpoint 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:
- Player joins the queue (can also queue while playing an AI game)
- When two players are queued, the server pairs them, assigns random colors, and notifies both
- A confirmation dialog appears — accepting starts the online match (pausing any in-progress AI game)
- Moves are sent to the server and relayed to the opponent in real time
- 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: one theme prompt fans out into six per-piece generations (api/artstudio/chess-sets), 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) callsKeycloakAdminService.GetRoleMembersAsync()to list role members, then joins with theuser-profilescollection. - Frontend:
TeamComponentusesTeamServiceto 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-globelibrary, also used by the sponsor family page and the geography explorer) highlights where SpikerSoft works.
Position sources remain two-fold:
- Ambassador Positions — Auto-generated for every active
Locationthat does not have an assigned ambassador, so the page always reflects current staffing gaps. - Other Roles — Manually created positions (developer, designer, teacher, etc.) managed through the admin Location Management page.
- Backend:
PositionsController(GET /api/positions) merges manually createdOpenPositiondocuments with auto-generated ambassador entries for unassigned locations. - Frontend:
EmploymentComponent+employment-filters/employment-list/employment-detailchildren, backed byPositionCatalogService.
Ambassador Application Workflow
Clicking an open position card launches a multi-step application dialog (ApplicationDialogComponent). Applicants must be logged in.
Intake Form Steps:
- Position & Passport — Read-only position summary, optional passport image scan via OCR (reuses the same
IPassportOcrService/ Tesseract OCR pipeline as the child account flow). - Personal Information — Name, DOB, sex, nationality, citizenship, government ID (SSN / National ID / Tax ID), email, phone. Fields auto-populate from passport scan results.
- Address History — Current and previous addresses with date ranges.
- Work & Education — Employment history and education entries.
- References — Minimum 2 professional/personal references.
- 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
Staffrole, creates aPositionAssignment, and marks the application asHired.
| 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
- Create a class implementing
IBackgroundCheckProviderinSpikerSoft.Business/Services/BackgroundCheck/Providers/. - Register it in DI (
ServiceCollectionExtensions.cs). - Add the country mapping in
appsettings.jsonunderBackgroundCheck: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-codesstores parsed GeoNames entries (country, postal code, city, state, coordinates) - CQRS Queries:
LookupPostalCodeQueryandGetStatesForCountryQueryinSpikerSoft.Business/Domain/PostalCodes/ - Controller:
PostalCodesControlleratapi/PostalCodes - Frontend:
PostalCodeServiceprovideslookupPostalCode()andgetStatesForCountry()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_COUNTRIESconstant incountry-map.component.tsis replaced with anavailableCountriesinput, allowing dynamic country lists from the API. - The
ProfileComponentfetches active locations fromLocationServiceon init and passes them to the country map and travel dropdowns. - The
LocationsControllerauto-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:
- 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.
- Staff Approval — Staff members review and approve sponsorship profiles via Admin > Sponsorship Management.
- Trip Cost Assignment — Staff sets estimated travel costs per wishlisted destination for each approved profile.
- Public Donation Page — A public
/sponsorpage (no login required) displays approved profiles with their bio, interests, destinations, and funding progress. - Stripe Checkout — Donors can donate to an individual or to the SpikerSoft organization. Payments are processed via Stripe Checkout with automatic nonprofit receipts.
- 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.
- Overfunding — When an individual is fully funded (total raised >= total trip cost), additional donations automatically redirect to the organization pool.
- 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.
- Navigate to Stripe Dashboard → Developers → Webhooks.
- Click Add endpoint.
- 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).
- Production:
- Under Events to send, select:
checkout.session.completed. - Click Add endpoint.
- Copy the Signing secret (
whsec_...) from the endpoint details page and set it asStripe:WebhookSecretinappsettings.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 profilesGET /api/sponsor/profiles/{id}— Individual profile detailGET /api/sponsor/profiles/{id}/progress— Donation progressPOST /api/sponsor/checkout— Create Stripe Checkout sessionPOST /api/sponsor/webhook— Stripe webhook handlerGET /api/sponsor/config— Stripe publishable key
Authenticated:
GET /api/sponsor/settings— Own sponsorship settings
Staff only:
GET /api/sponsor/profiles/pending— Pending profilesPOST /api/sponsor/profiles/{id}/approve— Approve profileDELETE /api/sponsor/profiles/{id}/approve— Revoke approvalPUT /api/sponsor/profiles/{id}/trip-costs— Set trip costsGET /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/:familyKeyis 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/:familyKeypools 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/:codelets 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:
- Type — Select fundraiser type; displays the trip cost as the fundraising goal.
- 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.
- Zones — Draw solicitation areas on the map; view AI-suggested high-traffic locations.
- 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:
- Received — File accepted and queued
- ClamAV Scan — Virus/malware scanning
- Metadata Extraction — EXIF data, dimensions, format detection
- Strip — Remove sensitive metadata (GPS coordinates, camera info)
- 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 |
Photography & Gallery
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.
Gallery
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 (from the Art Studio Photo Stack lane) 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}/tagsreplaces 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/rejectmoves a tag intoRejectedAutoTagsso re-tagging can never resurrect it. GET api/photography/tagsreturns 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 & Darkroom Companion
SpikerSoft.EventHandlers.PhotographProcessorruns the async photo pipeline: receive → ClamAV scan → metadata extract → move to final storage (MinIO).- Darkroom Companion (
DarkroomController, Mongodarkroom-develop-jobs): a staff-only desktop-agent lane. A machine with Adobe installed polls for parked PhotoStack develop jobs, runs the real Camera Raw develop recipe locally, and uploads 16-bit TIFFs back — the pipeline then resumes atalign_fuse. This exists because faithful RAW development is the one step the server fleet can't do.
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
VideoCallHubSignalR hub at/hubs/video-call: opaque SDP/ICE forwarding, room pairing, and an abandonment grace window. VideoCallControllerissues short-lived coturn TURN credentials (HMACuse-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
videoCallfeature 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 hubMultiTenantChatHub(/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
MessagingAccessRulesenforces 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 —
SignalRNotificationConsumerServiceforwards RabbitMQ messages to connected clients via the hub - Retry tiers — Failed notifications retry through
notifications.signalr.retry.1/2/3queues 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
mailonspikersoft.com - DMARC —
v=DMARC1; p=quarantinewith aggregate reports todmarc@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 intosystem.events.SpikerSoft.EventHandlers.SecurityMonitor— stateful detectors (Redis-backed) that turn raw events intoSecurityAlerts.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, withstaffexplicitly 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
nextbranch 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 callts.transpileModulebefore grading. Add a "Transpile to JavaScript" action in TypeScript lessons that uses the TypeScript compiler in the browser (same API as the canonicaltypescriptpackage ships inlib/typescript.js— the implementation currently referenced atunpkg.com/typescript@latest/lib/typescript.js). Dependency policy: addtypescriptas 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
- Follow the coding guidelines in
.cursor/rules/ - Write tests for new features
- Update documentation as needed
- Submit pull requests for review
License
Licensing is not uniform across the monorepo; use the SPDX / files in each part of the tree:
- Angular app (
spikersoft-angular/package.json):"license": "AGPL-3.0". - Backend (
spikersoft-backend/README.md): see that file and, when present,LICENSE.mdnext tospikersoft-backend/SpikerSoft.sln(the solution lists it as a solution item).
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.
Bringing families together through exploration -- bridging technology and the outdoors so every child can learn by doing, sponsor by caring, and grow by adventuring.