Best Boilerplates for EdTech Platforms 2026
·StarterPick Team
edtechonline-courseslmssaas-boilerplatenextjsmux2026
EdTech Platforms Have Unique Technical Requirements
Building an education technology product — a course platform, a learning management system, a coding school, a tutoring marketplace — requires features that general SaaS boilerplates do not include:
- Video hosting and streaming (course videos, lectures, recorded sessions)
- Course structure (modules, lessons, quizzes, progress tracking)
- Progress tracking (completion percentages, streaks, certificates)
- Drip content (releasing content over time)
- Cohort-based learning (enrollments with start/end dates)
- Interactive exercises (code editors, quizzes, assignments)
- Certificates of completion (PDF generation)
The base SaaS infrastructure (auth, payments, user management) comes from a boilerplate. The EdTech layer is custom.
TL;DR
Best starting points for EdTech platforms in 2026:
- Payload CMS + Next.js — Best for content-heavy course platforms. Payload manages courses, modules, lessons.
- ShipFast + Mux — Fastest to launch a course platform. ShipFast handles auth/billing; Mux handles video.
- OpenSaaS + custom LMS layer — Free complete SaaS base with background jobs for course progress.
- Teachable/Thinkific (hosted) — Not boilerplates, but faster than building for standard courses.
- Moodle (open source, fork) — For LMS-heavy products; PHP-based, heavy but feature-complete.
Key Takeaways
- Mux is the standard for video in EdTech — upload, transcode, adaptive streaming, and analytics
- UploadThing or S3 handle course materials (PDFs, code files, slides)
- Progress tracking requires a
user_lesson_completionstable — simple to model, critical for UX - Drip content is implemented with
unlocks_attimestamps per lesson relative to enrollment - Certificates of completion use PDF generation (react-pdf or Puppeteer)
- Most EdTech startups build on a SaaS boilerplate + Mux for video + custom LMS features
Course Data Model
// Prisma schema for a course platform:
model Course {
id String @id @default(cuid())
instructorId String
instructor User @relation(fields: [instructorId], references: [id])
title String
description String
slug String @unique
thumbnailUrl String?
price Int // In cents (0 = free)
status CourseStatus @default(DRAFT)
createdAt DateTime @default(now())
modules Module[]
enrollments Enrollment[]
}
model Module {
id String @id @default(cuid())
courseId String
course Course @relation(fields: [courseId], references: [id])
title String
order Int // Display order
createdAt DateTime @default(now())
lessons Lesson[]
}
model Lesson {
id String @id @default(cuid())
moduleId String
module Module @relation(fields: [moduleId], references: [id])
title String
description String?
videoId String? // Mux playback ID
duration Int? // In seconds
order Int
isPreview Boolean @default(false) // Free preview without enrollment
dripsAfter Int? // Days after enrollment before unlock
completions UserLessonCompletion[]
}
model Enrollment {
id String @id @default(cuid())
userId String
courseId String
user User @relation(fields: [userId], references: [id])
course Course @relation(fields: [courseId], references: [id])
enrolledAt DateTime @default(now())
completedAt DateTime?
@@unique([userId, courseId])
}
model UserLessonCompletion {
id String @id @default(cuid())
userId String
lessonId String
completedAt DateTime @default(now())
@@unique([userId, lessonId])
}
Video Streaming with Mux
Mux is the standard video infrastructure for EdTech platforms:
npm install @mux/mux-node @mux/mux-player-react
Uploading Course Videos
import Mux from '@mux/mux-node';
const mux = new Mux({
tokenId: process.env.MUX_TOKEN_ID!,
tokenSecret: process.env.MUX_TOKEN_SECRET!,
});
// Create an upload URL for the instructor:
export async function createVideoUploadUrl() {
const upload = await mux.video.uploads.create({
cors_origin: process.env.NEXT_PUBLIC_URL!,
new_asset_settings: {
playback_policy: ['signed'], // Require signed URLs for premium content
},
});
return { uploadUrl: upload.url, uploadId: upload.id };
}
// Webhook: Mux calls this when the video is ready:
export async function POST(req: Request) {
const event = await req.json();
if (event.type === 'video.asset.ready') {
const assetId = event.data.id;
const playbackId = event.data.playback_ids[0].id;
await db.lesson.update({
where: { muxUploadId: event.data.upload_id },
data: {
muxAssetId: assetId,
videoId: playbackId, // Use for playback
duration: Math.floor(event.data.duration),
},
});
}
}
Signed Video Playback (Premium Content)
// Generate signed playback token (expires in 1 hour):
export async function getSignedVideoToken(playbackId: string) {
const jwt = await Mux.JWT.signPlaybackId(playbackId, {
type: 'video',
expiration: '1h',
keyId: process.env.MUX_SIGNING_KEY_ID!,
keySecret: process.env.MUX_SIGNING_PRIVATE_KEY!,
});
return jwt;
}
// In the lesson page server component:
export default async function LessonPage({ params }: { params: { lessonId: string } }) {
const session = await getServerSession();
const lesson = await db.lesson.findUnique({ where: { id: params.lessonId } });
// Check enrollment:
const enrollment = await db.enrollment.findUnique({
where: { userId_courseId: { userId: session.user.id, courseId: lesson.module.courseId } },
});
if (!enrollment && !lesson.isPreview) redirect('/courses');
const token = lesson.videoId ? await getSignedVideoToken(lesson.videoId) : null;
return (
<div>
<h1>{lesson.title}</h1>
{lesson.videoId && token && (
<MuxPlayer
playbackId={lesson.videoId}
tokens={{ playback: token }}
streamType="on-demand"
/>
)}
</div>
);
}
Progress Tracking
// Mark a lesson complete:
export async function POST(
req: Request,
{ params }: { params: { lessonId: string } }
) {
const session = await getServerSession();
await db.userLessonCompletion.upsert({
where: {
userId_lessonId: { userId: session.user.id, lessonId: params.lessonId },
},
create: {
userId: session.user.id,
lessonId: params.lessonId,
},
update: { completedAt: new Date() },
});
// Check if course is complete:
const lesson = await db.lesson.findUnique({
where: { id: params.lessonId },
include: { module: { include: { course: { include: { modules: { include: { lessons: true } } } } } } },
});
const allLessons = lesson.module.course.modules.flatMap((m) => m.lessons);
const completedCount = await db.userLessonCompletion.count({
where: {
userId: session.user.id,
lessonId: { in: allLessons.map((l) => l.id) },
},
});
if (completedCount === allLessons.length) {
await db.enrollment.update({
where: {
userId_courseId: {
userId: session.user.id,
courseId: lesson.module.course.id,
},
},
data: { completedAt: new Date() },
});
// Trigger certificate generation...
}
return Response.json({ success: true });
}
// Get course progress for a user:
export async function getCourseProgress(userId: string, courseId: string) {
const course = await db.course.findUnique({
where: { id: courseId },
include: { modules: { include: { lessons: true } } },
});
const totalLessons = course.modules.flatMap((m) => m.lessons).length;
const completedLessons = await db.userLessonCompletion.count({
where: {
userId,
lesson: { module: { courseId } },
},
});
return {
percentage: Math.floor((completedLessons / totalLessons) * 100),
completedLessons,
totalLessons,
};
}
Certificate Generation
// Generate a PDF certificate using react-pdf:
import { renderToBuffer } from '@react-pdf/renderer';
import { CertificateTemplate } from '@/components/CertificateTemplate';
export async function generateCertificate(
userId: string,
courseId: string
): Promise<Buffer> {
const [user, course] = await Promise.all([
db.user.findUnique({ where: { id: userId } }),
db.course.findUnique({ where: { id: courseId } }),
]);
const pdf = await renderToBuffer(
<CertificateTemplate
userName={user.name}
courseName={course.title}
completionDate={new Date().toLocaleDateString()}
certificateId={`CERT-${userId.slice(0, 8)}-${courseId.slice(0, 8)}`}
/>
);
// Upload to S3/R2:
const key = `certificates/${userId}/${courseId}.pdf`;
await uploadToStorage(key, pdf);
return pdf;
}
Drip Content
// Check if a lesson is unlocked for a user:
export async function isLessonUnlocked(userId: string, lessonId: string): Promise<boolean> {
const lesson = await db.lesson.findUnique({
where: { id: lessonId },
include: { module: true },
});
if (!lesson.dripsAfter) return true; // No drip = always unlocked
const enrollment = await db.enrollment.findUnique({
where: {
userId_courseId: {
userId,
courseId: lesson.module.courseId,
},
},
});
if (!enrollment) return false;
const unlockDate = new Date(enrollment.enrolledAt);
unlockDate.setDate(unlockDate.getDate() + lesson.dripsAfter);
return new Date() >= unlockDate;
}
Recommended Stack by EdTech Type
| EdTech Product | Base Boilerplate | Add |
|---|---|---|
| Online course platform | ShipFast + Payload CMS | Mux video |
| Coding bootcamp | T3 Stack | Code runner (Piston API), Mux |
| Live tutoring marketplace | OpenSaaS | Stripe Connect, Daily.co |
| Corporate LMS | Makerkit (team billing) | Custom LMS layer, SCORM |
| AI tutoring app | OpenSaaS | OpenAI SDK, adaptive content |
Methodology
Based on publicly available information from Mux documentation, react-pdf documentation, and EdTech builder community resources as of March 2026.
Building an EdTech platform? StarterPick helps you find the right SaaS boilerplate foundation to build your learning platform on top of.