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 |
@scormflow/client |
Umbrella package — SDK at the root, player / React / types on subpaths (@scormflow/client/react, …). One install. |
@scormflow/client |
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 { ScormPlayer } from '@scormflow/react';
import { ScormClient } from '@scormflow/sdk';
const client = new ScormClient({
baseUrl: 'https://your-engine.example.com/v1',
apiKey: process.env.NEXT_PUBLIC_SCORMFLOW_KEY!,
});
export default function CoursePage({
attemptId,
launchUrl,
}: {
attemptId: string;
launchUrl: string;
}) {
return (
<ScormPlayer
attemptId={attemptId}
launchUrl={launchUrl}
transport={client.runtime}
className="w-full h-screen"
onCommit={(r) => console.log('committed', r.summary)}
onError={(err) => console.error(err)}
/>
);
}For demos, previews, offline learning, or "open the .zip and just play it" embeds.
import { ScormPlayer } from '@scormflow/react';
import { LocalStorageTransport } from '@scormflow/sdk';
const transport = new LocalStorageTransport({ namespace: 'demo' });
<ScormPlayer
attemptId="local-1"
launchUrl="/courses/intro/index.html"
transport={transport}
/>;import { mountScormPlayer } from '@scormflow/player';
import { ScormClient } from '@scormflow/sdk';
const client = new ScormClient({ baseUrl, apiKey });
const player = await mountScormPlayer({
container: '#scorm-root',
attemptId: 'attempt_abc',
launchUrl: '/courses/intro/index.html',
transport: client.runtime,
onCommit: (result) => console.log(result),
});
// later: await player.commit() / await player.destroy()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>) { /* ... */ }
}
// Hand it straight to the player…
const transport = new MyFirebaseTransport(db);
<ScormPlayer attemptId={attemptId} launchUrl={launchUrl} transport={transport} />;
// …or plug it into the facade's runtime namespace, keeping REST for the rest.
const client = new ScormClient({ baseUrl, transport });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.
const transport = new OfflineTransport({
remote: new RestTransport({ baseUrl, token: () => attemptToken, retries: 1 }),
onConflict: (attemptId, commits) => console.warn('attempt finished elsewhere', commits),
});Every commit gets a strictly increasing seq and is written to localStorage
(or any StorageLike) before it is sent with POST /attempts/{id}/sync. If the
engine is unreachable the SCO keeps running — commit resolves with
queued: true — and the queue is flushed on the next commit, on the browser's
online event, or by calling transport.flush(attemptId). The engine skips any
seq it has already applied, so a flush whose response was lost is just resent.
Once an attempt has been opened online, initialize also works offline from the
cached snapshot. Commits that arrive after the attempt was finished on another
device are reported to onConflict and dropped (server state wins).
useOfflineSync(transport, attemptId) gives React a { pending, syncing, flush } view.
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.
// Drop-in player
<ScormPlayer
attemptId={attemptId}
launchUrl={launchUrl}
transport={transport} // any ScormTransport, e.g. client.runtime
autoCommitMs={10_000} // debounced background commit; 0 disables
onCommit={(result) => ...}
onError={(err) => ...}
loadingFallback={<Spinner />}
errorFallback={(err) => <ErrorBox error={err} />}
className="w-full h-screen"
/>
// Hooks
const { containerRef, status, error, player, commit, terminate } =
useScormPlayer({ attemptId, launchUrl, transport });
// The data hooks take a ResourceClient — `client.resources` off the facade.
const { data: course, loading, error, refetch } = useCourse(client.resources, courseId);
const { data: stats } = useCourseAnalytics(client.resources, courseId);
const { upload, status, progress, result, reset } = useScormUpload(client.resources);
ScormProvider(client context) is not shipped yet — pass the transport or client explicitly for now. See the roadmap.
import { ScormClient } from '@scormflow/sdk';
const client = new ScormClient({ baseUrl, apiKey });
// REST-only — these need the server
const courses = await client.courses.list();
const course = await client.courses.upload(file, { onProgress: (p) => ... });
const attempt = await client.attempts.start(course.id, { learnerId });
const overview = await client.analytics.overview();
const stats = await client.analytics.course(course.id);
// cmi5 — the engine is the LMS; the AU talks to its LRS directly
const reg = await client.cmi5.register({ courseId: cmi5Course.id, learnerId });
const { url } = await client.cmi5.launch(reg.id, { auId: cmi5Course.aus[0].id });
// …and inside the AU: new Cmi5AuSession().initialize(), passed(), completed(), terminate()
// xAPI attachments — hash the body, declare it, send both as multipart/mixed
const cert = await client.xapi.prepareAttachment({ usageType, display, contentType: 'application/pdf', content: pdfBlob });
await client.xapi.storeStatements({ ...statement, attachments: [cert.attachment] }, { attachments: [cert.body] });
const { attachments } = await client.xapi.queryStatementsWithAttachments({ verb });
// Webhooks — register an endpoint, then verify deliveries in your receiver
const hook = await client.webhooks.create({ url: hookUrl, events: ['course.completed'] });
// receiver: await constructWebhookEvent(rawBody, req.headers.get('scormflow-signature'), hook.secret)
// Any transport (this is what the player drives)
await client.runtime.initialize(attempt.attemptId);
await client.runtime.commit(attempt.attemptId, { 'cmi.score.raw': '85' });
await client.runtime.terminate(attempt.attemptId);Pass transport to run the runtime loop off-server (LocalStorageTransport,
MemoryTransport, or your own) while courses and analytics still use REST:
const client = new ScormClient({ baseUrl, transport: new LocalStorageTransport() });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.
- 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
-
ResourceClient— courses + analytics REST surface, upload with progress -
ScormClienttop-level facade (courses/attempts/runtime/analytics/xapi/cmi5/webhooks) - Request / response interceptors (
onRequest/onResponseonHttpClient) - Automatic token refresh on
RestTransport(onUnauthorizedcallback refreshes the token on a 401 and retries once;TokenProviderstill supported) -
@scormflow/player— iframe mount + runtime injection (window.API/window.API_1484_11) - Player: commit debouncing +
beforeunloadauto-save - Player: SCORM 1.2 CMI surface
- Player: SCORM 2004 CMI surface
-
@scormflow/react—ScormProvidercontext (useScormClient+ typed namespace hooks) -
@scormflow/react—<ScormPlayer/>component -
@scormflow/react—useScormPlayer,useCourse,useCourseAnalytics,useScormUploadhooks -
@scormflow/react—useXapiStatements,useXapiDocument,useXapiActivity,useXapiPersonhooks - 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 (
@scormflow/types,@scormflow/sdk,@scormflow/player,@scormflow/react)
- Offline SCORM commits —
OfflineTransport(durable seq-numbered queue,/syncreconciliation, offline resume) +useOfflineSync - Offline xAPI — IndexedDB-backed statement queue + reconciliation
- xAPI client surface (statements, state, activity + agent profiles, activity/person lookups)
- xAPI attachments (
prepareAttachment, multipart statement upload,…WithAttachmentsreads) - cmi5 LMS surface (
client.cmi5: courses, registrations, launch, waive) + React hooks - cmi5 AU runtime (
Cmi5AuSession: launch-parameter parsing, fetch URL, LaunchData, AU lifecycle) - cmi5 block satisfaction +
cmi5LearnerPreferences(LMS + AU side, React hook) - Webhooks client (
client.webhooks), receiver-side signature verification, delivery-log React hooks - 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, cmi5, webhooks, analytics.scorm-engine-demo— reference LMS built on both repos (planned).
