Skip to main content

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:

  1. Payload CMS + Next.js — Best for content-heavy course platforms. Payload manages courses, modules, lessons.
  2. ShipFast + Mux — Fastest to launch a course platform. ShipFast handles auth/billing; Mux handles video.
  3. OpenSaaS + custom LMS layer — Free complete SaaS base with background jobs for course progress.
  4. Teachable/Thinkific (hosted) — Not boilerplates, but faster than building for standard courses.
  5. 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_completions table — simple to model, critical for UX
  • Drip content is implemented with unlocks_at timestamps 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;
}

EdTech ProductBase BoilerplateAdd
Online course platformShipFast + Payload CMSMux video
Coding bootcampT3 StackCode runner (Piston API), Mux
Live tutoring marketplaceOpenSaaSStripe Connect, Daily.co
Corporate LMSMakerkit (team billing)Custom LMS layer, SCORM
AI tutoring appOpenSaaSOpenAI 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.

Comments