TypeScript SDK, iframe player, and React components for SCORM, xAPI, and cmi5. Pluggable transport — works with the
@scormflow/serverbackend, your own backend, or fully client-side via LocalStorage. End-to-end typed.
A small family of npm packages for running SCORM, xAPI, and cmi5 content inside any modern web app. The packages are designed to be plug-and-play and decoupled — you can use the React component, drop down to the player, or just import the SDK. You can wire them to our backend, your backend, or skip the backend entirely and persist learner state in localStorage.
If you're building an LMS, an internal training portal, a compliance app, a course preview tool, or anything that needs to host a SCORM/xAPI/cmi5 package and track progress — this is the toolkit.
- Three packages, one stack —
sdk,player,react. Use one, two, or all three. - Pluggable transport —
RestTransport,LocalStorageTransport,MemoryTransportship in the box. Bring your own (Firebase, Supabase, GraphQL, gRPC) by implementing one interface. - End-to-end typed — types are generated from the backend's OpenAPI spec and published as
@scormflow/types. No drift between server and client. - Standards-first — SCORM 1.2, SCORM 2004, xAPI, and cmi5 share one normalized runtime surface.
- Framework-agnostic core — the SDK has zero React/Vue/Angular dependencies. The React package is opt-in.
- Tiny — target sizes: SDK < 10 KB gz, Player < 20 KB gz, React < 15 KB gz. ESM + CJS dual-build, fully tree-shakeable.
| Package | What it is | npm |
|---|---|---|
@scormflow/sdk |
Framework-agnostic TypeScript client. Pluggable ScormTransport + REST / LocalStorage / Memory transports. |
@scormflow/sdk |
@scormflow/player |
Iframe player + runtime bridge. Injects window.API (SCORM 1.2) and window.API_1484_11 (SCORM 2004). Vanilla JS, no React required. |
@scormflow/player |
@scormflow/react |
React hooks + <ScormPlayer/> component + <ScormProvider/>. |
@scormflow/react |
@scormflow/types |
TypeScript types generated from the backend's OpenAPI spec. | @scormflow/types |
Pick the package that matches how you're embedding SCORM. The React package depends on the SDK and the player — install just @scormflow/react if you're building a React app:
npm install @scormflow/react
# or
pnpm add @scormflow/react
# or
yarn add @scormflow/reactOther entrypoints:
# Vanilla / non-React app
npm install @scormflow/sdk @scormflow/player
# SDK only (write your own UI)
npm install @scormflow/sdkRequirements: Node ≥ 20 for tooling. The runtime works in any evergreen browser.
import {
ScormProvider,
ScormPlayer,
ScormClient,
RestTransport,
} from '@scormflow/react';
const client = new ScormClient({
transport: new RestTransport({
baseUrl: 'https://your-engine.example.com',
apiKey: process.env.NEXT_PUBLIC_SCORMFLOW_KEY!,
}),
});
export default function CoursePage({ attemptId }: { attemptId: string }) {
return (
<ScormProvider client={client}>
<ScormPlayer
attemptId={attemptId}
className="w-full h-screen"
onComplete={(r) => console.log('done', r)}
onProgress={(p) => console.log('progress', p)}
/>
</ScormProvider>
);
}For demos, previews, offline learning, or "open the .zip and just play it" embeds.
import {
ScormProvider,
ScormPlayer,
ScormClient,
LocalStorageTransport,
} from '@scormflow/react';
const client = new ScormClient({
transport: new LocalStorageTransport({ namespace: 'demo' }),
});
<ScormProvider client={client}>
<ScormPlayer attemptId="local-1" packageUrl="/courses/intro.zip" />
</ScormProvider>import { mountScormPlayer } from '@scormflow/player';
import { ScormClient, RestTransport } from '@scormflow/sdk';
const client = new ScormClient({
transport: new RestTransport({ baseUrl, apiKey }),
});
const player = mountScormPlayer({
container: '#scorm-root',
attemptId: 'attempt_abc',
client,
onComplete: (result) => console.log(result),
});Implement the ScormTransport interface against Firebase, Supabase, GraphQL, gRPC, or anything else. The player and React layer depend only on this interface — never on the REST transport.
import type { ScormTransport } from '@scormflow/sdk';
class MyFirebaseTransport implements ScormTransport {
async initialize(attemptId: string) { /* ... */ }
async commit(attemptId: string, values: Record<string, unknown>) { /* ... */ }
async terminate(attemptId: string, values?: Record<string, unknown>) { /* ... */ }
}
const client = new ScormClient({ transport: new MyFirebaseTransport(db) });A ScormTransport is the bridge between the in-browser runtime and wherever you want learner state to live. The SDK ships three; you can write your own.
| Transport | Use case | Backend required? |
|---|---|---|
RestTransport |
Default. Talks to a @scormflow/server backend. |
Yes |
LocalStorageTransport |
Browser-only persistence. Demos, offline learning, embedded courses, previews. | No |
MemoryTransport |
Tests and ephemeral previews. State vanishes on refresh. | No |
The runtime surface (initialize, commit, terminate) works with any transport. Course management, attempt lifecycle, and analytics are REST-transport-only — they only make sense when you're using the server.
// Provider
<ScormProvider client={scormClient}>
<App />
</ScormProvider>
// Drop-in player
<ScormPlayer
attemptId={attemptId}
packageUrl="/courses/intro.zip" // optional — only for client-only mode
onComplete={(result) => ...}
onProgress={(progress) => ...}
onError={(err) => ...}
className="w-full h-screen"
/>
// Hooks
const { progress, status, score, suspend, resume } = useScorm(attemptId);
const { course, isLoading } = useCourse(courseId);
const { stats } = useCourseAnalytics(courseId);
const { upload, isUploading, progress } = useScormUpload();import { ScormClient, RestTransport } from '@scormflow/sdk';
const client = new ScormClient({
transport: new RestTransport({ baseUrl, apiKey }),
});
// REST-transport-only
const course = await client.courses.upload(file);
const attempt = await client.attempts.start({ courseId, learnerId });
const stats = await client.analytics.forCourse(courseId);
// Any transport (used by the player)
await client.runtime.initialize(attempt.id);
await client.runtime.commit(attempt.id, { 'cmi.score.raw': '85' });
await client.runtime.terminate(attempt.id);Errors are typed: ScormHttpError, ScormNetworkError, ScormTimeoutError, ScormAbortError. Retries with exponential backoff are built in for idempotent operations.
┌─────────────────────────────────────────────────────────────┐
│ Your app (React / Next.js / Vite / vanilla) │
│ │
│ @scormflow/react ──► @scormflow/player ──► │
│ ▲ (iframe + window.API) │
│ │ │
│ @scormflow/sdk ◄── ScormTransport ──► Backend │
│ (any) │
└─────────────────────────────────────────────────────────────┘
- The runtime bridge is a clean-room implementation of the SCORM 1.2 and 2004 JavaScript APIs. No third-party SCORM libraries are bundled.
- Because we follow the spec, any spec-compliant course works — including content authored with pipwerks, scorm-again, Captivate, Storyline, Lectora, iSpring, Articulate Rise, etc. We don't maintain a compatibility matrix; the spec is the contract.
Checkboxes reflect the actual state of main. Source of truth for scope: ../scope.md.
- Monorepo (pnpm workspaces) + TypeScript strict
-
@scormflow/typespackage + generation from OpenAPI -
@scormflow/sdkpackage skeleton (ESM + CJS + .d.ts) -
ScormTransportinterface -
RestTransportimplementation -
LocalStorageTransportimplementation -
MemoryTransportimplementation - HTTP client with retry + exponential backoff
- Typed error hierarchy (
ScormError,ScormHttpError,ScormNetworkError, ...) - Unit tests (Vitest) for transports + HTTP client
-
ScormClienttop-level facade (courses/attempts/runtime/analytics) - Automatic token refresh on
RestTransport - Request / response interceptors
-
@scormflow/player— iframe mount + runtime injection (window.API/window.API_1484_11) - Player: commit debouncing +
beforeunloadauto-save withkeepalive - Player: SCORM 1.2 CMI surface
- Player: SCORM 2004 CMI surface
-
@scormflow/react—ScormProvidercontext -
@scormflow/react—<ScormPlayer/>component -
@scormflow/react—useScorm,useCourse,useCourseAnalytics,useScormUploadhooks - Player UI (header, footer, progress bar, loading + error states, CSS-variable theming)
- Examples:
nextjs-app,vite-react,vanilla-html - First npm release for all four packages
- Offline mode — IndexedDB statement queue + reconciliation
- xAPI client surface (statements, state, activity profile)
- cmi5 client surface (launch URL parsing, AU lifecycle)
- Vue adapter —
@scormflow/vue - Svelte adapter —
@scormflow/svelte - CLI tool —
npx scormflow validate course.zip,extract,create,preview - Analytics widgets — pre-built React dashboard components
- Certificate viewer —
<ScormCertificate attemptId={id} />
- Session replay viewer (
<SessionReplay attemptId={id} />) - AI insights widgets (BYO provider + key)
- A11y — keyboard nav, screen reader announcements, focus management
- i18n — built-in locale support for player UI
- Course version diff viewer
- Universal content gateway client (SCORM / xAPI / cmi5 / H5P / HTML / video / PDF)
pnpm install
pnpm types:generate # regenerate @scormflow/types from ../scorm-engine/openapi/openapi.yaml
pnpm build # build all packages
pnpm dev # watch all packages in parallel
pnpm typecheck
pnpm testEach package has its own build, dev, typecheck, test scripts that the root delegates to via pnpm -r --filter './packages/*'.
Issues and PRs welcome. Before opening a PR:
- Run
pnpm typecheck && pnpm testat the root. - Match the existing style.
- New SDK surface area should ship with a test.
- The OpenAPI spec lives in the backend repo — if you need a type that doesn't exist yet, open a PR there first and we'll regenerate.
MIT — see LICENSE.
scorm-engine— the backend. Headless runtime engine, REST API, built-in LRS (planned), analytics.scorm-engine-demo— reference LMS built on both repos (planned).