Source:
docs/adr/0013-yet-analytics-sql-lrs-for-self-hosted-learning-record-store.md
0013 — Use Yet Analytics SQL LRS as the self-hosted Learning Record Store
Context and Problem Statement
The framework emits xAPI statements at high frequency — code executions, quiz interactions, hint usage, session bookends (ADR 0017 documents the canonical statement shapes). These statements must land in a Learning Record Store (LRS) that speaks xAPI 2.0, supports cmi5 routing, runs self-hosted on our Hetzner / Coolify target (ADR 0018), and is actively maintained.
Browsers must never hold LRS credentials; xAPI must be routed through a thin proxy service so secrets stay server-side (research §4.5).
Decision Drivers
- Self-hosted. Our default deployment is OSS + self-hosted; a SaaS-only LRS (even a cheap one) is wrong for customers on private networks and is a recurring cost.
- xAPI 2.0 / IEEE 9274.1.1 conformance. The new xAPI 2.0 spec is the IEEE standard; the LRS must conform, not stall on 1.0.3.
- Active maintenance. The LRS is a long-lived data store; an abandoned LRS becomes a migration problem.
- cmi5 support. cmi5 is one of our primary standards (ADR 0003); the LRS must route cmi5
launched/initialized/terminatedstatements correctly. - License. Permissive, or at least compatible with our default MIT core (ADR 0014).
- Coolify-friendly. Single Docker image, standard Postgres backend, straightforward env-var config.
- Tooling ecosystem. The LRS should integrate with common BI / dashboard tools so analytics (Phase 4) is buildable.
Considered Options
- A: Yet Analytics SQL LRS — Apache 2.0, Clojure + Postgres, xAPI 2.0 / IEEE 9274.1.1 conformant, single Docker image.
- B: Trax LRS — Laravel + Vue, MIT, native cmi5, nice admin UI.
- C: Learning Locker Community Edition.
- D: Commercial SaaS LRS (Watershed, Veracity, SCORM Cloud LRS).
- E: Build our own minimal LRS.
Decision Outcome
Chosen option: A — Yet Analytics SQL LRS as the default self-hosted LRS. Trax LRS is an acceptable alternative when a customer prefers its native cmi5 UI. Learning Locker Community is explicitly rejected.
Operational shape
- Image: single official Docker image from Yet Analytics.
- Backend: Postgres 16 (shared with the FastAPI service's existing Postgres or a dedicated one for isolation — both are valid).
- Deployment: Coolify service on the same Hetzner box as the API in the single-tenant default (ADR 0018).
- Credentials: the LRS's per-credential-pair ACL model is used; each tenant (or each course) gets a dedicated basic-auth credential with the minimum
statements/read+statements/writescopes needed. - Proxy pattern: browsers POST xAPI to the framework's
/xapiendpoint on the FastAPI service; the service authenticates the learner (SSO session), rate-limits, and forwards with the server-side LRS credential. Browsers never hold LRS credentials.
Learning Locker rejection (explicit)
Research §3.4 documents: "Learning Locker Community — effectively abandoned since 2021; HT2 Labs was acquired by Learning Pool and moved the project enterprise-only." Adopting it would tie us to a frozen codebase or a commercial contract. Rejected.
Statement shape commitments (from research §4.5)
The LRS must store these five canonical statement families:
- executed-code — custom verb; stdout / stderr / exit in extensions.
- passed / failed coding challenge — ADL registered verbs
http://adlnet.gov/expapi/verbs/passedand.../failed;result.score.scaled∈ [0,1]; per-test breakdown inresult.extensions. - used-hint — custom verb;
hintIndex+costin extensions for mastery-style score degradation. - reset-cell — custom verb; correlates subsequent executions with the reset.
- cmi5 session bookends —
launchedfrom the LMS,initializedon lesson open,terminatedonnavigator.sendBeaconat page unload.
Storage discipline
- Batch rapid-fire statements at the proxy (e.g. 500 ms debounce per learner per cell) to reduce LRS write amplification.
- Store only a
sha256of the source on every execution; store full source only on terminalpassed/failedevents to bound storage (research §4.5). - Retention policy: keep raw statements for 90 days by default; aggregate older data into summary statements; operator-configurable in
lernkit.config.ts. (Per ADR 0022 the substrate is single-tenant — retention is per-deployment, not per-tenant.) - Summary statements at session end are the authoritative record for reporting (survives retention pruning of raw statements).
Consequences
- Portability, good: Apache 2.0 license, Docker image, Postgres backend — works on any Linux host under Coolify.
- Portability, good: xAPI 2.0 / IEEE 9274.1.1 conformant — interoperable with every other xAPI-2.0 tool.
- Functionality, good: cmi5 routing works out of the box; Trax-grade cmi5 UX is not needed because our LMS acts as the cmi5 launcher.
- Security, good: proxy pattern keeps credentials server-side; LRS credentials never reach the browser.
- Performance, mixed: Clojure + Postgres handles the statement volume for 1,000 MAU comfortably; high-volume deployments may split the LRS onto its own Hetzner host per the ADR 0018 "When we split this box" triggers.
- Clarity, good: the data model is standard xAPI — no proprietary extensions to learn beyond the five statement families we document.
- Clarity, bad: Clojure operator experience is rarer than Node/Python — operators unfamiliar with JVM tuning may need a short ramp.
Pros and Cons of the Options
A — Yet Analytics SQL LRS — chosen
- Good: Apache 2.0; active maintenance; xAPI 2.0 / IEEE 9274.1.1.
- Good: single Docker image; Postgres backend; Coolify-friendly.
- Bad: Clojure operator experience; mitigated by standard JVM monitoring (it is not black-box).
B — Trax LRS
- Good: native cmi5, attractive admin UI.
- Good: MIT + Laravel + Vue — friendlier operator stack.
- Bad: somewhat smaller community vs Yet Analytics.
- Verdict: valid alternative; not the default but acceptable per-customer.
C — Learning Locker Community
- Bad: abandoned; enterprise fork is paid. Explicit reject per research.
D — Commercial SaaS LRS
- Bad: ongoing cost; data lives off-prem; not compatible with private-network customer requirements.
- Good: ops-free. Valid as a customer-paid add-on, not as default.
E — Build our own
- Bad: xAPI conformance is a long-tail of edge cases; Yet Analytics already passes the ADL conformance suite. Building our own diverts engineering from the differentiating product work.
Proxy service contract
The /xapi proxy on FastAPI exposes:
POST /xapi/statements— accepts a batch of statements from an authenticated learner; applies tenant context; forwards to LRS with the server-side credential.GET /xapi/statements— scoped to the current learner / course; used by the learner dashboard (Phase 4).POST /xapi/state,POST /xapi/activities/profileetc. — forwarded with scope checks for the few endpoints cmi5 requires.
Hard rule: the LRS credential is never served to the browser, never in JS bundles, never in .env files checked in. It lives in the FastAPI service's
runtime secret store only.
Validation
- Conformance: the deployed LRS passes the ADL xAPI 2.0 conformance suite (run in staging, not on every PR).
- Proxy scope tests: a learner authenticated as tenant A cannot read statements belonging to tenant B via the proxy (integration test).
- Batch behavior: 10 rapid-fire
executed-codeevents from one learner result in ≤2 LRS POSTs after debounce. - Retention job: after a simulated 91-day retention window, raw statements older than 90 days are aggregated into summary statements; summary statements preserve completion/score outcomes.
- Credential audit: a grep pattern in CI fails the build if any file in the frontend bundle contains an LRS credential string.
More Information
- Research §3.4 "Tooling choices" — LRS row.
- Research §4.5 "xAPI tracking of code execution".
- Research §10 Risk #10 "xAPI statement explosion and storage cost".
- Yet Analytics SQL LRS: https://github.com/yetanalytics/lrsql.
- Trax LRS: https://www.trax-lrs.org/.
- Related ADRs: 0003 (cmi5 standard), 0004 (Tracker interface), 0008 (FastAPI hosts the xAPI proxy), 0017 (test framework covers LRS integration), 0018 (Coolify/Hetzner deployment).
- Open question: do we ship a Trax preset alongside Yet Analytics SQL LRS to ease customer choice? Deferred to Phase 3 / 4.