{"id":50673029,"url":"https://github.com/pelotech/xapi-lrs","last_synced_at":"2026-06-08T13:03:15.325Z","repository":{"id":357687103,"uuid":"1172364999","full_name":"pelotech/xapi-lrs","owner":"pelotech","description":"xAPI-conformant Learning Record Store (LRS)","archived":false,"fork":false,"pushed_at":"2026-06-01T19:49:51.000Z","size":377,"stargazers_count":0,"open_issues_count":12,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-01T21:24:55.720Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pelotech.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-04T08:18:40.000Z","updated_at":"2026-05-14T16:17:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pelotech/xapi-lrs","commit_stats":null,"previous_names":["pelotech/xapi-lrs"],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/pelotech/xapi-lrs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pelotech%2Fxapi-lrs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pelotech%2Fxapi-lrs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pelotech%2Fxapi-lrs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pelotech%2Fxapi-lrs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pelotech","download_url":"https://codeload.github.com/pelotech/xapi-lrs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pelotech%2Fxapi-lrs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34063159,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-08T02:00:07.615Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-08T13:03:14.557Z","updated_at":"2026-06-08T13:03:15.318Z","avatar_url":"https://github.com/pelotech.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# xapi-lrs\n\nA production-ready, xAPI 1.0.3 conformant Learning Record Store built on [Hono](https://hono.dev) + PostgreSQL (or [PGlite](https://pglite.dev) for zero-dependency local use).\n\n## Features\n\n- Full xAPI 1.0.3 compliance (statements, documents, agents, activities)\n- Statement validation per xAPI Data spec sections 2.2-2.6 and 4.0\n- Multipart/mixed attachment support\n- Server-Sent Events (SSE) for real-time statement streaming\n- JWT and Basic Auth (credential-based) authentication\n- Admin UI with dashboard, credential management, and statement browser\n- OpenTelemetry metrics (Prometheus exporter)\n- PostgreSQL with pg_notify for event-driven architecture\n- **PGlite mode**: run with an embedded in-process database — no PostgreSQL required\n\n## Quick Start\n\n### With PostgreSQL\n\n```bash\n# Start PostgreSQL\ndocker compose up -d postgres\n\n# Install dependencies\npnpm install\n\n# Apply database schema\npnpm db:migrate\n\n# Start in development mode\npnpm dev\n```\n\n### With PGlite (no PostgreSQL required)\n\nPGlite embeds a full PostgreSQL engine in-process via WASM. No external database or Docker needed.\n\n```bash\npnpm install\n\n# In-memory database (data lost on restart):\nDATABASE_DRIVER=pglite pnpm dev\n\n# Persistent database (data survives restarts):\nDATABASE_DRIVER=pglite PGLITE_DATA_DIR=./data/pglite pnpm dev\n```\n\nThe schema is applied automatically on first start. The admin account is bootstrapped as described in [Configuration](#configuration) below.\n\n\u003e **Limitations of PGlite mode:**\n\u003e\n\u003e - Single connection — concurrent transactions are serialized. Suitable for local development and low-concurrency workloads; not recommended for production.\n\u003e - SSE uses in-process delivery (`db.listen`) instead of cross-process `LISTEN/NOTIFY` — works correctly within a single Node.js process.\n\u003e - `AUTO_MIGRATE` and `pnpm db:migrate` are ignored in PGlite mode (migrations are applied directly from committed SQL files).\n\nThe LRS will be available at `http://localhost:8081` and the admin server at `http://localhost:8091`.\n\n## Configuration\n\nAll configuration is via environment variables. See `.env.test` for defaults.\n\n| Variable                            | Default     | Description                                           |\n| ----------------------------------- | ----------- | ----------------------------------------------------- |\n| `LRS_PORT` / `PORT`                 | `8081`      | xAPI HTTP port                                        |\n| `LRS_ADMIN_PORT` / `ADMIN_PORT`     | `8091`      | Admin/health/metrics port                             |\n| `DATABASE_DRIVER`                   | `pg`        | Database driver: `pg` (PostgreSQL) or `pglite`        |\n| `PGLITE_DATA_DIR`                   | (none)      | PGlite data directory; omit for in-memory             |\n| `PGHOST`.                           | `localhost` | PostgreSQL host                                       |\n| `PGPORT`                            | `5432`      | PostgreSQL port                                       |\n| `PGDATABASE`                        | `xapi_lrs`  | PostgreSQL database                                   |\n| `PGUSER`                            | `xapi_lrs`  | PostgreSQL user                                       |\n| `PGPASSWORD`                        | (empty)     | PostgreSQL password                                   |\n| `DATABASE_URL`                      | (none)      | Full connection string (overrides PG\\* vars)          |\n| `JWT_ISSUER`                        | (none)      | JWT issuer for token validation                       |\n| `JWT_AUDIENCE`.                     | (none)      | JWT audience for token validation                     |\n| `JWKS_URI`.                         | (none)      | JWKS endpoint URI                                     |\n| `OIDC_DISCOVERY_URL`                | (none)      | OIDC discovery URL (auto-discovers JWKS)              |\n| `LRS_ADMIN_USER`                    | (none)      | Bootstrap admin username                              |\n| `LRS_ADMIN_PASSWORD`                | (none)      | Bootstrap admin password                              |\n| `ADMIN_SESSION_SECRET`              | (random)    | Session secret (required in production)               |\n| `LOG_LEVEL`                         | `info`      | Log level (silent/fatal/error/warn/info/debug/trace)  |\n| `CORS_ORIGIN`                       | `*`         | CORS allowed origin                                   |\n| `LRSQL_STMT_GET_DEFAULT`.           | `50`        | Default `GET /statements` page size when no `limit`   |\n| `LRSQL_STMT_GET_MAX`.               | `50`        | Hard cap on `GET /statements` `limit` (silent clamp)  |\n| `SHUTDOWN_TIMEOUT_MS`               | `30000`     | Hard deadline for graceful shutdown before exit       |\n| `PG_STATEMENT_TIMEOUT_MS`           | `30000`     | Per-statement DB query timeout (`0` disables)         |\n| `PG_IDLE_IN_TRANSACTION_TIMEOUT_MS` | `60000`     | Idle-in-transaction connection timeout (`0` disables) |\n\n### Health checks\n\nOn the admin port (`LRS_ADMIN_PORT`, default `8091`):\n\n| Path       | Purpose                        | Returns 503 when                                                  |\n| ---------- | ------------------------------ | ----------------------------------------------------------------- |\n| `/healthz` | Liveness probe                 | (never, unless the process is deadlocked)                         |\n| `/readyz`  | Readiness probe                | shutting down, DB unreachable, or pg_notify listener disconnected |\n| `/ready`   | Deprecated alias for `/readyz` |\n\nOn SIGTERM/SIGINT the server flips `/readyz` to 503, aborts long-lived SSE streams, waits for in-flight HTTP requests, stops the pg_notify listener, drains the DB pool, and exits — with a hard `SHUTDOWN_TIMEOUT_MS` deadline as a safety net.\n\n## Scripts\n\n| Script                  | Description                                 |\n| ----------------------- | ------------------------------------------- |\n| `pnpm dev`              | Start with hot reload (tsx watch)           |\n| `pnpm build`            | Compile TypeScript to `dist/`               |\n| `pnpm start`            | Run compiled output                         |\n| `pnpm test`             | Run unit tests                              |\n| `pnpm test:integration` | Run integration tests (requires PostgreSQL) |\n| `pnpm test:conformance` | Run ADL conformance suite                   |\n| `pnpm typecheck`        | Type-check without emitting                 |\n| `pnpm lint`             | Lint with oxlint                            |\n| `pnpm fmt`              | Format with oxfmt                           |\n| `pnpm db:migrate`       | Run database migrations                     |\n| `pnpm docker:build`     | Build Docker image                          |\n| `pnpm docker:up`        | Build and start full stack (postgres + lrs) |\n| `pnpm docker:down`      | Stop the stack                              |\n\n## Architecture\n\n```\nsrc/\n  admin/          # Admin UI (htmx + Pico CSS)\n  auth/           # JWT verification, credential auth\n  helpers/        # Enrichment, ETag, SQUUID utilities\n  middleware/     # Authentication \u0026 authorization middleware\n  repositories/   # PostgreSQL data access (statements, documents, agents)\n  routes/         # Hono route handlers (xAPI endpoints)\n  sse/            # Server-Sent Events (pg_notify → SSE)\n  xapi/           # Statement validator, multipart parser, signature verification\n  xapi-types/     # xAPI type definitions\n  app.ts          # Hono app factory\n  config.ts       # Environment-driven config with Zod validation\n  db.ts           # PostgreSQL pool management\n  server.ts       # Process entrypoint\n```\n\n## Supply Chain\n\nContainer images published to `ghcr.io/pelotech/xapi-lrs` are signed with [Sigstore cosign](https://docs.sigstore.dev/) (keyless / OIDC) and carry SLSA build provenance attestations. Release images additionally have SPDX and CycloneDX SBOMs attached as Sigstore attestations and as downloadable release artifacts.\n\nVerify an image (substitute the tag):\n\n```bash\n# Signature\ncosign verify ghcr.io/pelotech/xapi-lrs:0.4.0 \\\n  --certificate-identity-regexp 'https://github.com/pelotech/xapi-lrs/.+' \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com\n\n# Build provenance\ncosign verify-attestation ghcr.io/pelotech/xapi-lrs:0.4.0 \\\n  --type slsaprovenance \\\n  --certificate-identity-regexp 'https://github.com/pelotech/xapi-lrs/.+' \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com\n\n# SBOM (releases only)\ncosign verify-attestation ghcr.io/pelotech/xapi-lrs:0.4.0 \\\n  --type spdxjson \\\n  --certificate-identity-regexp 'https://github.com/pelotech/xapi-lrs/.+' \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com\n```\n\nSBOM files are also attached to each GitHub Release as `xapi-lrs-\u003cversion\u003e-sbom.spdx.json` and `xapi-lrs-\u003cversion\u003e-sbom.cdx.json`.\n\n## License\n\nApache 2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpelotech%2Fxapi-lrs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpelotech%2Fxapi-lrs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpelotech%2Fxapi-lrs/lists"}