Best Boilerplates for EdTech Platforms 2026
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 |
Course Pricing Models: One-Time, Subscription, and Cohort
The pricing model you choose shapes your entire billing and enrollment architecture. Most EdTech boilerplates handle generic Stripe subscriptions, but EdTech products often need more nuanced payment flows.
One-time purchase (most common for course platforms) works well with Stripe's payment intents and a checkout session that creates an enrollment record on success. The Stripe webhook flow looks the same as any SaaS product: checkout.session.completed event creates an enrollment. ShipFast's pre-built Stripe integration handles this without modification — just trigger enrollment creation in the existing webhook handler.
Subscription access (used by Skillshare-style platforms) requires tracking active subscription status and gating all course content behind a subscription check. This matches standard SaaS billing more closely. OpenSaaS and Makerkit both include subscription management that maps directly to this model.
Cohort-based enrollment with fixed start dates requires additional fields: cohortStartDate, cohortEndDate, enrollment caps, and waitlists. No standard boilerplate handles this. Teams building cohort-based learning (like boot camps or live cohort courses) model cohorts as a separate entity related to a course, with enrollments linked to specific cohort instances rather than the course directly.
The practical advice: start with one-time purchase (simplest) unless your core model specifically requires subscriptions or cohorts. Over-engineering the payment model early delays everything else.
Multi-Tenant LMS vs Single-Tenant Course Platform
The architecture decision with the largest long-term implications is whether your product serves one audience or many organizations. These are fundamentally different products.
A single-tenant course platform (Podia, Gumroad-style) is a creator selling courses to their own students. One instructor (or team), one set of courses, one stripe account. A standard SaaS boilerplate handles this cleanly. Makerkit or ShipFast plus a course data model gets you there.
A multi-tenant LMS (like Teachable, where many instructors sell under one platform, or a B2B LMS where companies deploy learning paths for their employees) requires an Organization model above the User model and Row Level Security that scopes courses and enrollments to organizations. Every query in your application must include an organization filter. This is where Supabase's RLS becomes invaluable — a single SQL policy can enforce tenant isolation at the database layer, eliminating entire categories of authorization bugs.
For multi-tenant LMS builds, T3 Stack with a manual Supabase integration or Supabase's own Next.js starter is the more appropriate foundation than ShipFast (which doesn't include organization-scoped data patterns). Makerkit includes a team/organization model that covers most multi-tenant LMS requirements without custom implementation.
The cost of getting this wrong is high: retrofitting multi-tenant data isolation into a single-tenant codebase typically means touching every database query in the application. Make the decision upfront.
ShipFast + Mux: The Fastest Launch Path
For solo founders and small teams launching a course platform quickly, the ShipFast + Mux combination provides the most complete starting point with the least custom implementation.
ShipFast ($299) delivers auth, Stripe checkout and webhooks, landing page components, and Next.js App Router structure. Mux handles video uploading, transcoding, adaptive streaming, signed playback tokens, and analytics — replacing a significant amount of custom work that would otherwise go into video infrastructure. The integration adds roughly two days of implementation work: adding Mux SDK, building the upload endpoint, implementing the webhook for asset status, and wiring the signed token generation to the lesson page.
What you still build yourself: the course and lesson data model, the enrollment flow, progress tracking, drip content logic, and certificate generation. But the foundation is solid. A developer following ShipFast's patterns can implement the EdTech layer on top without fighting the underlying infrastructure. The landing page components ShipFast ships (Hero, Pricing, FAQ, Testimonials) are directly applicable to a course platform's marketing page — instructors want social proof and pricing tables, exactly what ShipFast provides out of the box.
The weakness of ShipFast + Mux for EdTech is content management. If instructors need to author courses through a UI (rather than through direct database access), you need to add an admin interface or integrate a headless CMS. Payload CMS combined with ShipFast's billing layer solves this, though the integration requires more work than either alone.
OpenSaaS as the Free EdTech Foundation
OpenSaaS (Wasp-based) earns its place in the EdTech stack because of one feature: PgBoss-based background jobs. Course completion workflows often involve asynchronous processing — generating a PDF certificate, sending a completion email, triggering a webhook to an external LMS API, unlocking the next drip batch. These require reliable background job execution that survives server restarts.
Most boilerplates handle this by suggesting you add a queue system (BullMQ with Redis, Inngest, Trigger.dev) manually. OpenSaaS ships with PgBoss queued jobs already configured in the Wasp DSL. For EdTech products with complex completion workflows, this translates directly to less infrastructure setup.
The tradeoff is Wasp's DSL learning curve and the less polished visual design compared to ShipFast. OpenSaaS's landing page is functional but not optimized for the kind of social proof and conversion optimization that course platforms need. Teams that choose OpenSaaS typically invest time in the marketing page earlier than ShipFast teams.
For developers building an AI tutoring platform — where LLM API calls run in the background, feedback is generated asynchronously, and course content is dynamic — OpenSaaS's job infrastructure is worth the DSL investment.
What No Boilerplate Handles for You
Understanding the EdTech gap in standard boilerplates prevents false expectations. Even the best-fit choices (ShipFast, OpenSaaS, Makerkit) leave these for custom implementation:
Video infrastructure — every team integrates Mux, Cloudflare Stream, or Bunny.net manually. There is no boilerplate that ships with video upload and playback preconfigured. Mux is the standard choice because its signed playback tokens and webhook events map cleanly to EdTech content protection patterns.
Content authoring UI — instructors need a way to create courses, add lessons, upload videos, and structure curriculum without touching code or the database directly. This typically means building a custom admin panel or integrating Payload CMS with a custom course content type. The four to six days required for a functional instructor dashboard is frequently underestimated.
Adaptive content and assessments — quizzes, assignments, code submission, and grading logic are entirely custom. Interactive code execution requires a sandboxed runtime (Piston API is a common choice for multi-language execution). These are advanced features that go well beyond what any boilerplate addresses.
SCORM/xAPI compliance — corporate LMS buyers frequently require SCORM compliance for integration with HR systems. This is a significant custom integration not addressed by any SaaS boilerplate on the market.
The Build vs Buy vs Hosted Decision
Before committing to building on a boilerplate, EdTech founders should honestly evaluate the hosted platform option. Teachable and Thinkific take a percentage of revenue (typically 0-5% depending on plan) but provide complete course platforms — video hosting, student dashboards, affiliates, and analytics — without engineering investment.
The hosted vs build calculation depends on two variables: monthly revenue and technical requirements. At $5,000 MRR, the percentage fees on Teachable Pro ($99/month + 0%) are much lower than the cost of three months of engineering to build an equivalent platform. At $50,000 MRR with custom requirements — cohort scheduling, corporate single sign-on, white-labeling, custom completion workflows — the build decision becomes financially and functionally justified.
Most EdTech founders who choose to build have specific requirements that hosted platforms don't support: custom completion certificates with specific designs, integration with external tools, multi-instructor platforms, or learning experiences that don't map to the standard video-lesson model. If your requirements fit a standard hosted platform, use one. Boilerplate-based development makes sense when the requirements don't fit.
For teams that do build, the comparison of all StarterPick boilerplate options helps narrow the choice. See also the SaaS boilerplate database options comparison for how each backend choice affects the EdTech data model. Multi-tenant EdTech builds should also review the best SaaS boilerplates for 2026 for organization-aware options.
Payments and Enrollment: Matching Billing Model to Architecture
The payment flow for EdTech products differs from standard SaaS in a critical way: the enrollment event is the purchase event. When a student pays for a course, you don't just update their subscription status — you create an enrollment record that grants access to specific course content. This two-step pattern (payment → enrollment) requires careful webhook handler design.
For one-time purchase courses, the flow is: Stripe checkout.session.completed → create Enrollment record for the purchased course → send welcome email with course access link. The common mistake is updating the user's role or a generic "access" flag rather than creating a course-specific enrollment. A user who buys Course A should not have access to Course B just because they've purchased something.
For subscription-based platforms (Skillshare-style), the enrollment pattern changes: all active subscribers are enrolled in all courses, and enrollment is gated on active subscription status checked at request time rather than pre-created records. The building Stripe from scratch vs boilerplate comparison covers subscription lifecycle edge cases — past_due grace periods, cancellation timing, and trial conversions — that affect when course access should be revoked.
Cohort-based programs add another layer: enrollment is tied to a cohort instance with specific start and end dates, and the payment happens during an enrollment window that closes before the cohort starts. This requires a separate Cohort model and enrollment status tied to cohort timing, not just payment status.
Analytics and Progress Visibility for Instructors
Instructors need to see how students are engaging with their content. Standard SaaS analytics tools (PostHog, Mixpanel) track page views and clicks but don't surface EdTech-specific metrics like video completion rates, lesson replay rates, and module drop-off points.
The data model above captures all the raw data needed for instructor analytics: UserLessonCompletion records, enrollment timestamps, and video watch events (if captured via Mux's data API). The instrumentation for instructor-facing analytics typically requires a custom analytics page that queries this data directly — there's no off-the-shelf tool that connects to your course completion data.
Mux's data API provides video-specific analytics: watch time, re-watch rates, error rates, and viewer counts per video. These are available via the Mux dashboard and API without any custom instrumentation, making video engagement data the easiest EdTech metric to surface to instructors.
For broader platform analytics — student activation rates, completion rates by course, and revenue attribution — a combination of your database queries and PostHog events provides sufficient coverage for products below 10,000 students. At higher scale, dedicated analytics infrastructure (ClickHouse or BigQuery) becomes appropriate.
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.