The API-first LMS for building learning systems

A headless, composable learning platform in modern TypeScript. Fastify, Drizzle, and Zod under the hood. Org-scoped multi-tenancy, a typed SDK, and swappable adapters. Build whatever frontend you want.

$ npm create headless-lms
app/lib/lms.ts
import { createClient } from "@headless-lms/sdk"

const lms = createClient({
  baseUrl: process.env.LMS_URL,
  token: process.env.LMS_TOKEN,
})

// Fully typed against the OpenAPI spec
const course = await lms.courses.create({
  orgId,
  title: "Intro to Distributed Systems",
})

await lms.entitlements.grant({
  studentId,
  courseId: course.id,
})

A complete learning platform, headless by design

The full domain of an LMS, exposed as a typed API. Compose the pieces you need and swap the ones you don't.

Course builder
Author structured course content; students work through it activity by activity.
Progress tracking
Per-student, per-activity completion, rolled up into course progress and reporting.
Entitlements
Grant and revoke student access to content with a first-class access model.
Multi-tenant
One deployment serves many orgs. Every student, course, and session is org-scoped.
Media & file assets
Object storage with presigned upload and download URLs, behind a swappable adapter.
Integrations
Drop a plugin folder into your installation and it's live at startup. Write your own.
MCP endpoint
AI agents connect over OAuth and operate the LMS through the same domain layer.
Typed SDK & OpenAPI
Routes validate against shared Zod schemas; the SDK is generated from the spec.
Transactional email
Invitation and auth mail, swappable behind an adapter you control.

A backend that ships as a library

The backend ships as @headless-lms/server: a framework-free domain core behind a Fastify HTTP layer. Every client — including AI agents — talks to the same domain layer.

Layered by design
A framework-free domain core sits behind a Fastify HTTP layer, persisted with Drizzle and Postgres.
Composable installations
An installation composes what it wants with sane defaults. Swap storage and email adapters freely.
Secure by default
Authentication, org-scoped multi-tenancy, encrypted credential storage, and validated I/O throughout.

Clients

Admin dashboard
Student portal
Your frontend
AI agents (MCP)

HTTP layer — Fastify

Zod-validated requests & responses
OpenAPI spec
Generated typed SDK

Domain core — framework-free

Courses
Progress
Entitlements
Orgs & sessions

Adapters & persistence

Drizzle / Postgres
Object storage
Email
Plugins
routes/courses.ts
// Routes validate against shared Zod schemas
export const createCourse = defineRoute({
  method: "POST",
  path: "/orgs/:orgId/courses",
  input: CourseCreateSchema,   // validated request
  output: CourseSchema,        // validated response
  handler: async ({ input, ctx }) => {
    return ctx.courses.create(input)
  },
})

// The OpenAPI spec + SDK are generated from these.
// pnpm gen:sdk

One source of truth, from schema to SDK

Define a route once with Zod schemas. Headless LMS validates every request and response, generates the OpenAPI spec, and produces a fully typed SDK you can build any frontend on.

  • Requests and responses validated against shared Zod schemas
  • OpenAPI spec generated from your routes
  • Typed SDK generated from the resulting spec
  • Interactive OpenAPI reference at /docs on a running API

Reference apps and an AI-native surface

Ships with a Next.js admin back-office and student portal built on the public API — plus an MCP endpoint so AI agents are first-class clients.

Admin back-office
A Next.js dashboard for courses, students, entitlements, and reporting — built entirely on the public API.
Student portal
A Next.js app where students log in and take their courses, built on the typed SDK.
MCP endpoint
Agents authenticate over OAuth and operate the LMS through the exact same domain layer as the SDK and dashboards — no parallel code path.
mcp.http
# AI agents connect over OAuth and operate the LMS
# through the same domain layer as every other client.

POST /mcp
Authorization: Bearer <oauth-token>

> tools/call enroll_student
  { "orgId": "...", "studentId": "...", "courseId": "..." }

Spin up your own LMS in one command

Create a standalone installation that owns its config and plugins, and deploys anywhere Node and Postgres run.

bash
npm create headless-lms