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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions