A small learning management system for non-profits that have to prove staff and volunteers completed required training.
For the learner: sign in, see what's due, click Start, finish, close the tab. No course catalogue, no profile, no gamification.
For the administrator: upload a SCORM package, put people in groups, assign training with a deadline and a renewal interval, and hand an auditor a report saying exactly who completed what and when.
It exists because the alternatives are either enterprise products priced per seat, or Moodle — which is a fine piece of software and vastly more than a small charity needs to record that eleven people did their safeguarding training.
Node 22.6+ · SQLite · no database server · no external services required
| SCORM 1.2 | A real runtime. Upload the .zip from Articulate Storyline or Rise, iSpring, Lectora, Captivate, or any SCORM 1.2 tool. The LMS reads imsmanifest.xml, serves the package, and records completion, score, bookmark and time from the course's own signals. |
| Completion isn't self-reported | An administrator cannot mark someone complete. The record is written only when the course reports it. If a package declares a mastery score, a lower score is recorded as not passed. |
| Resume | Close the tab mid-course and pick up where you left off. suspend_data and lesson_location are kept per person. |
| Groups | Assign to everyone, to a group, or to one person. |
| Annual renewals | Set training to repeat every 12 months. On completion the next cycle is scheduled and reappears 30 days before it's due. Each cycle is a separate dated record, so "completed annually for three years" is provable. |
| Email reminders | People are emailed when training is assigned, again as the deadline nears, and chased while overdue — on a schedule set per assignment. Deduplicated in the database, so a restart can't send anything twice. |
| Audit report | A compliance matrix on screen, a CSV export, and a printable report with per-course summaries, per-person detail, traceable record IDs and a signature line. |
| Activity log | Append-only. Sign-ins, uploads, assignment changes, completions, report exports. Nothing in the app edits or deletes it. |
| Multi-tenant | Every record scoped to an organization; one organization's administrators never see another's. A platform owner can switch between them — useful if you run training on behalf of several charities. |
| Self-service accounts | People are emailed a one-time link to choose their own password. No administrator ever handles a password. Forgotten passwords reset by email. |
npm install
npm run seed # creates the database and a demo organization
npm run dev # http://localhost:3000Sign in as admin@example.org / admin1234, or as a learner with maria@example.org /
welcome1234.
No SCORM package to hand? npm run sample-course writes one — a valid SCORM 1.2 package with a
question and a mastery score, so you can watch a pass and a fail flow through to the report.
DEPLOY.md covers Render (about $10/month, entirely in a browser), Docker, and bare Node, plus email, backups and configuration.
SECURITY.md — please read the first section before you let anyone upload a course. It matters more than anything else in this file.
- docs/for-learners.md — one page to send to staff completing training
- docs/for-administrators.md — for whoever runs training at an organization
Both are Markdown so you can adapt the wording and send them as your own.
src/lib/
db.ts SQLite connection, schema and migrations
auth.ts scrypt passwords, signed session cookies, route guards
tokens.ts one-time links for setting and resetting passwords
ratelimit.ts sign-in throttling
scorm.ts zip extraction, imsmanifest.xml parsing, MIME types
scorm-api.ts the SCORM 1.2 runtime object — no React, no DOM, unit tested
enrollments.ts turns assignment rules into per-person enrollments, incl. renewals
reporting.ts the compliance matrix and its totals
notify.ts which reminder is due for whom, and sending it
mail.ts Resend, and the email templates
audit.ts append-only event log
src/app/
page.tsx learner dashboard — the whole learner experience
learn/[id]/ full-screen player; installs window.API, then the iframe
record/[id]/ printable certificate of completion
api/scorm/[courseId]/... serves package files (auth and enrollment checked)
api/attempt/[id]/ receives runtime state; the only place completion is decided
admin/ courses, people, groups, assignments, reports, email, activity log
Data model. An assignment is a rule: this course, for these people, due in this many days,
repeating every so often. syncEnrollments() turns rules into enrollments — one per person per
course per cycle, each with its own due date and status. Each enrollment has one attempt holding
the SCORM runtime state. Reports read the enrollment currently in force; older cycles remain as
history.
Decisions worth knowing about
- Stopping an assignment deletes only unfinished enrollments. Completed records are never removed, so past compliance stays provable.
- Archiving a course keeps every completion attached to it.
- Deactivating a person keeps their history and drops them from the compliance percentage.
- Moving someone between organizations leaves their completed records with the organization where they were earned, and they still appear in that organization's report.
- A scheduled future renewal doesn't count against your compliance rate until it opens.
npm testRoughly 220 checks: the SCORM runtime against the spec's error codes and data model, backup and
restore against a stand-in for Backblaze, sign-in and invitation flows driven the way a browser
drives them, multi-tenant isolation, the reminder engine against a stand-in for Resend, and a few
guards against mistakes already made once — a GET that changed state, a timestamp comparison that
hid anything dated today, a button that swallowed its own form data.
Some tests need npm run build first and skip themselves without it.
- SCORM 1.2 only. SCORM 2004 and xAPI/cmi5 packages won't run. Most compliance vendors still ship 1.2; ask for it.
- Single SCO per package. Multi-SCO packages launch the first one; there's no sequencing.
- Uploads over ~100 MB need object storage rather than a local disk, because most hosts cap request bodies around there.
- SQLite suits a few hundred people per organization comfortably. Well past that, move to Postgres — the queries are plain SQL.
- Due dates are computed in UTC, so "due today" can read a day off for users far from it.
- No alerting if a nightly backup fails. It appears in the logs; nobody is told.
Issues and pull requests welcome. npm test should pass, and please add a test alongside a fix —
most of the suite exists because something broke in a way nobody noticed.
MIT — see LICENSE.