Skip to content

Online School / LMS Plugin #106

@olliethedev

Description

@olliethedev

Overview

Add an online school plugin that covers the full learning lifecycle: course catalog, lesson player, student progress tracking, and instructor dashboard. Think "minimal Coursera" — focused on the core teaching and learning experience without the complexity of a full LMS platform.

The plugin ships:

  • A course catalog backed by the CMS plugin (courses are CMS content items, similar to the Job Board / E-commerce pattern)
  • A lesson player with support for video, text, and quiz lesson types
  • Enrollment & progress tracking so students can resume where they left off
  • An instructor dashboard for managing courses, lessons, and student activity
  • A student portal showing enrolled courses and completion status

Core Features

Course Catalog

  • Course list page with filters (category, difficulty, duration, free/paid)
  • Course detail page (description, curriculum overview, instructor bio, reviews)
  • Categories / tags
  • Difficulty levels (beginner, intermediate, advanced)
  • Estimated duration and lesson count display
  • Featured / promoted courses

Curriculum

  • Sections (chapters) to group lessons
  • Lesson types: video, text, quiz
  • Ordered lessons with drag-reorder in admin
  • Free preview lessons (publicly accessible without enrollment)
  • Downloadable resources attached to lessons

Enrollment

  • Enroll in free courses (one-click)
  • Payment adapter hook for paid courses (same interface as E-commerce plugin)
  • Enrollment status: enrolled | completed | cancelled
  • Enrollment limits (max students per course) — stretch goal

Progress Tracking

  • Mark lesson as complete
  • Resume from last watched/read position
  • Per-course progress percentage
  • Course completion detection + certificate trigger hook

Quizzes

  • Multiple-choice questions
  • Pass mark configuration per quiz
  • Attempt history and score storage
  • Retake support with configurable attempt limits

Certificates

  • Trigger certificate generation via lifecycle hook (onCourseComplete)
  • Unique certificate ID stored on the enrollment record
  • Public certificate verification page (by certificate ID)

Instructor Dashboard

  • Course CRUD (create, edit, publish, archive)
  • Lesson CRUD per course
  • Student list per course with progress overview
  • Basic analytics (enrollments over time, completion rate)

Student Portal

  • "My Courses" page — enrolled courses with progress bars
  • Continue learning CTA per course
  • Completed courses and certificates

Schema

```ts
import { createDbPlugin } from "@btst/stack/plugins/api"

export const schoolSchema = createDbPlugin("school", {
course: {
modelName: "course",
fields: {
title: { type: "string", required: true },
slug: { type: "string", required: true },
description: { type: "string", required: false },
coverImage: { type: "string", required: false }, // URL
instructorId: { type: "string", required: true }, // user ID
category: { type: "string", required: false },
tags: { type: "string", required: false }, // JSON array
difficulty: { type: "string", defaultValue: "beginner" }, // "beginner" | "intermediate" | "advanced"
durationMins: { type: "number", required: false },
price: { type: "number", defaultValue: 0 }, // 0 = free, cents otherwise
currency: { type: "string", defaultValue: "USD" },
status: { type: "string", defaultValue: "draft" },// "draft" | "published" | "archived"
maxStudents: { type: "number", required: false },
createdAt: { type: "date", defaultValue: () => new Date() },
updatedAt: { type: "date", defaultValue: () => new Date() },
},
},
section: {
modelName: "section",
fields: {
courseId: { type: "string", required: true },
title: { type: "string", required: true },
order: { type: "number", required: true },
},
},
lesson: {
modelName: "lesson",
fields: {
courseId: { type: "string", required: true },
sectionId: { type: "string", required: false },
title: { type: "string", required: true },
type: { type: "string", required: true }, // "video" | "text" | "quiz"
content: { type: "string", required: false }, // text/MDX body or JSON quiz data
videoUrl: { type: "string", required: false },
durationMins: { type: "number", required: false },
order: { type: "number", required: true },
isFreePreview: { type: "boolean", defaultValue: false },
resources: { type: "string", required: false }, // JSON: [{ label, url }]
},
},
enrollment: {
modelName: "enrollment",
fields: {
courseId: { type: "string", required: true },
userId: { type: "string", required: true },
status: { type: "string", defaultValue: "enrolled" }, // "enrolled" | "completed" | "cancelled"
progressPct: { type: "number", defaultValue: 0 },
lastLessonId: { type: "string", required: false },
certificateId: { type: "string", required: false },
paymentRef: { type: "string", required: false },
enrolledAt: { type: "date", defaultValue: () => new Date() },
completedAt: { type: "date", required: false },
},
},
lessonProgress: {
modelName: "lessonProgress",
fields: {
enrollmentId: { type: "string", required: true },
lessonId: { type: "string", required: true },
completed: { type: "boolean", defaultValue: false },
completedAt: { type: "date", required: false },
positionSecs: { type: "number", defaultValue: 0 }, // video resume position
},
},
quizAttempt: {
modelName: "quizAttempt",
fields: {
lessonId: { type: "string", required: true },
userId: { type: "string", required: true },
answers: { type: "string", required: true }, // JSON: [{ questionId, answer }]
score: { type: "number", required: true }, // 0-100
passed: { type: "boolean", required: true },
attemptedAt: { type: "date", defaultValue: () => new Date() },
},
},
})
```


Plugin Structure

```
src/plugins/school/
├── db.ts
├── types.ts
├── schemas.ts
├── query-keys.ts
├── client.css
├── style.css
├── api/
│ ├── plugin.ts # defineBackendPlugin — catalog, enrollment, progress, quiz endpoints
│ ├── getters.ts # listCourses, getCourse, listLessons, getLesson, getEnrollment, getProgress
│ ├── mutations.ts # enroll, completeLesson, submitQuiz, updateCourse, updateLesson
│ ├── query-key-defs.ts
│ ├── serializers.ts
│ └── index.ts
└── client/
├── plugin.tsx # defineClientPlugin — catalog, player, student portal, instructor dashboard
├── overrides.ts # SchoolPluginOverrides
├── index.ts
├── hooks/
│ ├── use-courses.tsx # useCourses, useCourse
│ ├── use-enrollment.tsx # useEnrollment, useEnroll
│ ├── use-progress.tsx # useLessonProgress, useCourseProgress
│ ├── use-quiz.tsx # useSubmitQuiz, useQuizAttempts
│ └── index.tsx
└── components/
└── pages/
├── course-list-page.tsx / .internal.tsx
├── course-detail-page.tsx / .internal.tsx
├── lesson-player-page.tsx / .internal.tsx
├── quiz-page.tsx / .internal.tsx
├── certificate-page.tsx / .internal.tsx
├── my-courses-page.tsx / .internal.tsx
├── instructor-dashboard-page.tsx / .internal.tsx
├── course-editor-page.tsx / .internal.tsx
└── lesson-editor-page.tsx / .internal.tsx
```


Routes

Route Path Description
catalog /learn Course catalog with filters
course /learn/:slug Course detail + enroll CTA
lesson /learn/:slug/lessons/:lessonId Lesson player (video / text / quiz)
quiz /learn/:slug/lessons/:lessonId/quiz Quiz attempt page
certificate /learn/certificates/:certificateId Public certificate verification
myCourses /learn/me Student portal — enrolled courses
instructorDashboard /learn/instructor Instructor course list
courseEditor /learn/instructor/courses/:id Course + curriculum editor
lessonEditor /learn/instructor/courses/:id/lessons/:lessonId Lesson editor

Lifecycle Hooks

```ts
export interface SchoolPluginHooks {
/** Called after a student successfully enrolls (free or paid) /
onAfterEnroll?: (enrollment: Enrollment, course: Course) => Promise
/
* Called when all lessons in a course are completed /
onCourseComplete?: (enrollment: Enrollment, course: Course) => Promise<{ certificateId: string }>
/
* Called when a quiz attempt is submitted /
onQuizSubmit?: (attempt: QuizAttempt, lesson: Lesson) => Promise
/
* Called before a paid enrollment — return false to abort */
onBeforeEnroll?: (userId: string, course: Course) => Promise
}
```


SSG Support

```ts
// prefetchForRoute route keys
export type SchoolRouteKey = "catalog" | "course"
// lesson / quiz / myCourses are user-specific — skipped

await myStack.api.school.prefetchForRoute("catalog", queryClient)
await myStack.api.school.prefetchForRoute("course", queryClient, { slug: "intro-to-typescript" })
```

Route key SSG? Notes
catalog Published courses list
course Per-course static detail page
lesson / quiz / myCourses / instructor routes Dynamic, user-specific

Consumer Setup

```ts
// lib/stack.ts
import { schoolBackendPlugin } from "@btst/stack/plugins/school/api"

school: schoolBackendPlugin({
hooks: {
onCourseComplete: async (enrollment, course) => {
const certId = crypto.randomUUID()
// generate PDF, email student, etc.
return { certificateId: certId }
},
},
})
```

```ts
// lib/stack-client.tsx
import { schoolClientPlugin } from "@btst/stack/plugins/school/client"

school: schoolClientPlugin({
apiBaseURL: "",
apiBasePath: "/api/data",
siteBaseURL: "https://example.com",
siteBasePath: "/pages",
queryClient,
})
```


Non-Goals (v1)

  • Live / cohort-based classes (scheduled sessions)
  • Discussion forums / community features
  • Assignment submissions and instructor grading
  • Built-in video hosting (accept any video URL)
  • Multi-instructor / co-author workflows
  • Subscription / membership pricing models
  • Mobile app or offline downloads
  • SCORM / xAPI compliance

Plugin Configuration Options

Option Type Description
hooks SchoolPluginHooks Enrollment, completion, quiz, and payment lifecycle hooks
quizPassMark number Global default pass percentage (default: 70)
certificatesEnabled boolean Toggle certificate generation (default: true)

Documentation

Add docs/content/docs/plugins/school.mdx covering:

  • Overview — course catalog + lesson player + progress tracking; v1 scope
  • SetupschoolBackendPlugin with hooks, schoolClientPlugin
  • Enrollment flow — free vs paid, onAfterEnroll hook
  • Quiz system — question schema, pass mark, attempt limits
  • CertificatesonCourseComplete hook, certificate verification page
  • SSGprefetchForRoute route key table + Next.js page.tsx examples
  • Schema referenceAutoTypeTable for all config + hooks
  • Routes — table of route keys, paths, descriptions

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions