Cardona CT LabAI Tooling · Design & Accessibility
Learning Content Management System · DoD / Simulation Training

CourseForge LCMS

A web-native authoring and SCORM publishing platform


Executive Summary

CourseForge is a web-native Learning Content Management System for building interactive training and publishing it as standards-compliant SCORM packages, the format Learning Management Systems in defense, government, and simulation training consume. Authors compose lessons in the browser; CourseForge renders them to a self-contained SCORM ZIP that runs on any conformant LMS, including air-gapped and strict-CSP networks.

The platform pairs a block-based authoring model, where authors stack reusable content blocks (text, media, quiz, hotspot, interactive video, 3D model, Adobe Animate) into frames, with a publishing pipeline that inlines every runtime and bundles every asset, so a course has no runtime CDN dependency. It ships alongside a family of focused side tools (ForgeShell, ForgePack, and ForgeScaffold) that feed the same ecosystem. The whole system is built and iterated through an AI-native workflow with Claude Code. The live system is public at courseforge.dev.

The Problem

Authoring tools for compliance and simulation training tend to fall into two camps. Heavyweight desktop suites (Storyline, Captivate) are powerful but produce opaque, often bloated output and assume a Windows authoring seat. Lightweight web builders are easy but rarely emit clean, offline-safe SCORM, and almost none handle real interactive media: 3D models, hotspot video, or Adobe Animate timelines.

Defense and simulation training adds two hard constraints most tools ignore. Content frequently runs on isolated networks where no external request will ever resolve, and it must meet Section 508 / WCAG 2.1 AA accessibility. A package that pulls a script from a CDN, or a 3D viewer with no keyboard path, is a non-starter. CourseForge is built so the default output satisfies both.

The Authoring Model

A course is a tree: course → module → lesson → frame → blocks. Modules and lessons group the material the way an instructional designer already thinks about it; a frame is a single screen; a block is one reusable content component. Authors build the tree in place, adding a course, module, lesson or frame at the level it belongs to and naming it in the same motion rather than filling out a form first.

Removing a grouping is treated as a different kind of act than adding one, because it is. Deleting a module deletes every lesson and frame beneath it, so the confirmation states the real count before it happens, requires the author to type the name of anything that still contains frames, and says plainly that it cannot be undone. It also reports what will break elsewhere: branch blocks and menu items reference frames by id, so the confirmation names the surviving frames whose links point into the subtree about to be removed. A structural edit that quietly breaks navigation two lessons away is the failure worth designing against.

Authors stack blocks to compose a frame, and the same block types render identically in the editor preview and in published SCORM. The block library covers the range a real lesson needs:

Text
Rich copy with headings, lists, inline formatting, and hot-text that swaps the frame image on click.
Media
Images and video with captions and alt text, plus an audio companion player that sits inline, as an edge bar, or as a compact corner mini player over an image.
Callout
Free-floating annotation labels with a leader line, positioned over an image by dragging in the live preview.
Quiz
Seven question types (multiple-choice, true / false, multiple-select, drag-and-drop, click-to-pair matching, sequencing, fill-in-the-blank) with a tiered feedback ladder, all wired to SCORM completion / score reporting.
Hotspot
Clickable regions over an image in an exploratory "click to learn" mode or a graded visual multiple-choice mode, with a full keyboard path.
WCN
Warning / Caution / Note advisories, shown inline or as an acknowledge-to-proceed hazard modal.
Branch
Condition-driven navigation that routes learners to different frames.
Interactive Video
Stop-points that gate progression, each with its own authored prompt driven into the shell prompt zone, plus embedded quizzes and hotspots (including invisible practice hotspots).
3D Model
Orbitable GLB with annotation pins and part highlighting (Forge3D).
Adobe Animate
.OAM timelines with stops, hotspots, and a command cue stream (ForgeScript).
Navigation Shell
A shared navigation frame wrapping the lesson, designed in Figma and imported by ForgeShell.

Because every block is a self-describing component with its own JSON schema, the editor, the live preview, and the SCORM packager all read the same source of truth. There's no separate "export format" to drift out of sync.

Blocks are placed by a small layout system rather than by free positioning. A frame picks one of four layouts: full (a single element fills the content area), text left or text right (a 50/50 vertical split), and text top (text across the upper third, the interactive element below). Each layout exposes a text zone and a media zone, and a split holds at most one occupant of each. That cap is what keeps a frame legible: an author cannot quietly stack two elements into a zone sized for one and discover the overflow after publishing.

Which zone a block occupies is decided by how it actually renders, not by how it is categorized. A quiz, for example, is a media zone occupant, because every renderer places it opposite the text. That distinction matters in practice: it means a question stem and its quiz sit together on one frame, which is the ordinary shape of an assessment and was previously disallowed by a rule that described a layout the renderers never produced.

Zone occupants are then split again by whether they should touch the edge of the content area. Photographic and dimensional media (images, video, 3D models, and embedded Animate output) render full bleed, because cropping is part of how they read. Authored interface elements (assessments, branching prompts, and audio players) are inset by the same margin the project uses elsewhere, because a question or a control pressed flat against the frame edge reads as a rendering fault rather than a design. The inset is one value defined once and consumed by every renderer, so the editor preview and the published SCORM package cannot drift apart on it.

Interactive blocks are edited directly on the frame in the live preview rather than in a side panel: callout labels and hotspot regions are dragged, scaled, and aimed in place, exactly as they will publish. A content-tree audit flags each frame's state at a glance (missing required assets, empty, or marked optional and excluded from the completion count), and a whole course can be exported and re-imported as one JSON file for backup, versioning, or moving between environments, with every block's content preserved verbatim.

Assessment: seven question types, one feedback ladder

The quiz block covers multiple-choice, true / false, multiple-select (choose every correct option, scored as an exact set), drag-and-drop (match each item to its target), click-to-pair matching (pair items to targets with no dragging, a keyboard-first alternative to drag-and-drop), sequencing (drag items into the right order), and fill-in-the-blank (inline dropdowns). All seven are renderer twins: identical behavior and SCORM scoring in the live authoring preview and in the published SCO. Every type runs the same tiered feedback ladder, where a wrong answer escalates across the allowed attempts from feedback, to a hint, to revealing the correct answer, with an optional per-question randomize. Pass / fail and score report back to the LMS.

Scoring aggregates honestly. Quizzes are the only thing that moves the grade, and multiple quizzes combine into a single score rather than the last one overwriting the others. An Adobe Animate element is treated as a click-through example: it counts toward completion, never toward the score, so a lesson with no questions completes without reporting a misleading zero to the LMS. Completion and score are tracked separately, and a required in-video question holds the lesson incomplete until the learner answers it.

A Likert survey item sits on the completion side of that same line. It presents an ordered agreement scale, records the learner's choice as a SCORM interaction for reporting, and counts toward completion, but it has no correct answer and never touches the score. It is the survey counterpart to the seven graded types: an honest way to collect an opinion without pretending it is a grade.

The hotspot block mirrors that split. Its original exploratory "click to learn" mode is joined by a graded visual multiple-choice mode: the learner clicks the correct region, and a wrong or off-target click counts as an attempt. The whole visual task carries a keyboard path (tab through labeled regions, activate with Enter), so a point-and-click question stays 508-operable. Regions can also be made invisible but clickable, for find-the-area practice and test activities where the target is not shown.

AI-assisted authoring (ForgeAgent)

A natural-language prompt drafts a complete frame. The request and the block schema go to the model, which returns one structured frame definition validated against the same per-block rules the editor enforces, with a single repair round-trip on a miss. The author gets a real, editable frame to refine, not a wall of text to reformat.

Block & Frame Types

The full library at a glance. Each type renders identically in the editor preview and in published SCORM, and each is authored in place on the frame.

Navigation Shells & ForgeShell

Training rarely ships as bare frames. It lives inside a navigation shell: a branded chrome with NEXT / PREV / MENU, a progress indicator, and a content area the lesson renders into. CourseForge treats that shell as a first-class, reusable artifact.

Shell development starts in Figma. A designer lays the shell out as a Figma frame (see the CourseForge Shell Figma file), naming each layer for the zone it represents. ForgeShell, the companion app, imports that design and types each layer into a known zone (content area, nav buttons, progress, menu) by closest-match on the layer names, then emits a shell bundle. To keep that match reliable, ForgeShell ships an artist naming guide with an in-app guide popout, so designers label their Figma layers with the zone keywords the importer looks for. A "Send to CourseForge" handoff posts the shell straight into the CourseForge shell library, where any course can adopt it.

The shell's own NEXT / PREV / MENU buttons drive the runtime through a small message bridge, so the chrome and the lesson stay decoupled: a shell can be reskinned in Figma and re-imported without touching content, and content can move between shells. This Figma-to-runtime path means the visual design and the training content are authored independently and composed at publish time.

The shell also carries the course glossary. An author maintains one course-level list of terms, each with a short definition and an optional longer entry. That single list drives two reader experiences at once: the renderer auto-links the first mention of each term in body text to a keyboard-reachable definition tooltip, and a right-slide browse drawer lists every term for reference. Auto-matching is deliberately conservative: first mention per frame only, whole words, never inside a heading, a link, or code, with a per-term opt-out for words too common to link and an explicit tag to force a link where the author wants one. The same term definitions render identically in the editor preview and the published SCO, because the matcher is a single rule shared by both.

Forge3D: Interactive 3D Blocks

The 3D Model block renders an interactive GLB asset directly inside a training frame. Learners orbit and zoom with mouse, touch, or keyboard; authors place annotation pins on the model surface by clicking in the editor, with no external tooling. Per-mesh part highlighting lets a lesson call out a sub-component (e.g. a valve or a lid) with a label and description.

A standalone Forge3D conversion app (Electron + headless Blender) prepares assets: ingesting FBX / glTF with external textures and animations and re-exporting clean, self-contained GLB with modern metallic-roughness materials. At publish time the viewer runtime, Three.js, the glTF + Draco loaders, and the Draco WebAssembly decoder are all bundled into the SCORM ZIP and loaded local-first, so 3D works on air-gapped networks. There's a dedicated Forge3D page with a live viewer.

ForgeScript: The Adobe Animate Runtime

Subject-matter animation is often authored in Adobe Animate and exported as an .oam bundle. ForgeScript is a standard runtime (forge-oam.js) that plays those timelines as a native block. An author marks timeline stops and attaches hotspots to movie-clip instances; the runtime emits an even/odd command cue stream (stop = pause + prompt, start = play / advance) that the CMS consumes to gate progression and drive the media bar.

Authoring kit

ForgeScript ships an authoring kit (a custom Animate publish template, the runtime, frame scripts, and a standalone test harness) so animators can validate an .oam against the real runtime before it ever reaches CourseForge. Hotspot styling (color, hover, border, pulse) is a project-wide setting baked into the published players.

Publishing & Delivery

The packager renders every frame's blocks through one pipeline and writes a conformant SCORM 1.2 or SCORM 2004 ZIP (a Web bundle is also available). The defining property is self-containment: interactive blocks generate vanilla-JS runtimes inlined into the SCO (no React at runtime), and every dependency is bundled.

Draco-compressed models are the one case that asks the host CSP to permit WebAssembly ('wasm-unsafe-eval'); uncompressed GLBs need no such allowance.

Accessibility

Accessibility is a default, not a pass. Output targets Section 508 / WCAG 2.1 AA: interactive blocks expose proper roles and ARIA, the 3D viewer is fully keyboard-operable (orbit, zoom, reset, and annotation cycling) with a descriptive canvas label, and even the visual tasks have a non-pointer path: graded hotspot questions tab through labeled regions and activate with Enter, so a click-the-target question is fully operable from the keyboard. Focus management returns focus to the triggering control after a popover closes, and animations honor prefers-reduced-motion. Color choices are checked for contrast, and a high-contrast viewing mode mirrors the rest of the lab's tools. A formal VPAT is on the roadmap.

Security & Hardening

An authoring platform lives on two trust boundaries: it ingests untrusted uploads (SCORM and Adobe Animate archives, media ZIPs, Figma shell exports) and it renders authored content back into a live preview and a published package. CourseForge treats both as hostile input by default, and every guard ships with a test that reproduces the attack it stops.

Archive ingest is bounded before extraction. Every archive that enters the system is checked for entry count, total decompressed size, and compression ratio and rejected if any limit is exceeded, so a small malicious upload cannot expand to exhaust disk or memory (a "zip bomb"). Paths carried inside an archive, whether an Adobe Animate manifest entry point or an imported shell asset filename, are contained to their extraction root by component-aware path resolution, so a crafted archive cannot traverse out of its sandbox to read or overwrite a file elsewhere on the server.

Authored content is sanitized on the way out. Rich text an author writes passes through a single shared sanitizer boundary before it renders, so a pasted <script> or inline event handler never becomes live markup in the editor preview. The standalone Web bundle escapes the project and frame names it writes into the player chrome, so a course title can never inject script into the exported HTML.

The API fails closed and quiet. Server errors return a generic message while the underlying exception and stack trace are logged server-side and never surfaced to the client, and a publish request validates its target format before a job is created, so a malformed request cannot strand a running job.

Write access is gated by owner login. When a deployment sets an owner username and password, every state-changing request (content edits, uploads, publish, demo reset) requires an authenticated session: the owner signs in and the server issues a signed HttpOnly, SameSite session cookie that page scripts cannot read, so a framed package can never lift it the way it could a token kept in browser storage. Guests keep full read and preview access but cannot write. A separate shared token still authenticates command-line and CI callers, and with nothing configured the editor stays fully open for local and demo use, so a deployment never hard-breaks by omission.

The rendered preview is a second trust boundary. Every response carries X-Content-Type-Options: nosniff, and a report-only Content-Security-Policy watches the editor for anything an enforced policy would later block. Uploaded navigation shells and interactive-media packages run inside sandboxed frames, and their companion assets are served with correct, explicit content types rather than guessed ones.

Executable author packages are isolated on their own origin. A navigation shell or an Adobe Animate package is, by design, arbitrary author-supplied HTML and JavaScript. Even inside a sandbox, a same-origin frame shares the editor's cookies and can reach back into its DOM and API, so the sandbox alone is not a real boundary for code the platform itself renders. CourseForge closes that gap by serving every executable package from a dedicated, credential-free origin (packages.courseforge.dev), now live. That origin carries an enforcing Content-Security-Policy whose load-bearing line is connect-src 'none': a package can run its own code but cannot call the editor API, phone home, or exfiltrate anywhere. frame-ancestors names only the editor origin, so nothing else may frame a package; Cross-Origin-Resource-Policy: cross-origin, an Origin-Agent-Cluster header, and a disabled document.domain keep the two origins from re-coupling into a shared agent; and the package origin never issues a cookie, so there is no credential on it to steal. The one capability the editor still needs, a read-only content audit of the framed package (used by the accessibility and completeness checks), runs over a narrow MessageChannel bridge with an explicit handshake rather than by reaching across a shared origin. The final cutover, pointing every package frame at that origin and dropping the last same-origin escape hatch, is feature-flagged and rolling out behind an end-to-end gate.

Packaging is bounded for the target runtime, not just the author. Because a published course has to unzip and run intact on a locked-down LMS, the publish preflight also rejects package internals that would break there: a hard gate blocks any archive path long enough to exceed a strict Windows path limit once extracted, so an author-named Animate asset buried deep in a folder tree cannot produce a course that silently fails to unpack on a target system.

Defense in depth, verified by tests

Each hardening measure carries an isolated regression test that reproduces the original failure, an oversized archive, a traversal path, an over-long extracted package path, an injected title, an unauthenticated write, a package attempting to reach its framer, and asserts the guard holds, so the protections cannot silently regress as the platform evolves.

The Tooling Ecosystem

CourseForge sits at the center of a family of focused tools that share its design language and feed its pipeline:

Each is independently deployable yet interoperable: ForgePack conditions media for blocks, ForgeShell produces the shells courses run inside, and Forge3D prepares the GLB assets the 3D block consumes.

Architecture & Stack

A Flask / Python server backs a React authoring client, with the block schema as the contract between editor, preview, and packager. Course data persists in a relational store with migrations; the SCORM packagers are Python modules that compose each SCO's HTML, inline the block runtimes, and bundle assets. The whole platform, and every side tool, deploys on Railway.

Live system
Output
SCORM 1.2 · SCORM 2004 · Web bundle
Server / Client
Flask (Python) · React
Compliance
508 · WCAG 2.1 AA · offline / strict-CSP

What This Demonstrates

CourseForge spans the full stack of a real product: a schema-driven authoring model, a browser-based editor with live preview, three custom interactive runtimes (3D, Adobe Animate, interactive video), a seven-type assessment engine with a tiered feedback ladder and keyboard-operable graded hotspots, a standards-compliant publishing pipeline with hard offline / CSP constraints, and accessibility as a baseline. It's surrounded by an ecosystem of tools that share its design language and feed its pipeline. Every notable decision (self-contained SCORM, the decoupled shell bridge, the cue-stream runtime, per-mesh part highlighting) is deliberate and has a rationale. Built and iterated AI-natively with Claude Code, where the limiting factor is judgment, not typing.