Corporate Training Platform
Built custom LMS with video hosting, quiz system and certificate generation for internal corporate training

Corporate Training Platform
A project developing a custom Learning Management System (LMS) for enterprise, replacing manual training processes with an automated platform featuring progress tracking and certificate generation.
Project Details:
- Client: Enterprise Corp (Fortune 500 company)
- Industry: Business Services / Consulting
- Timeline: 2 months (June - July 2024)
- Team Size: 3 developers, 1 designer, 1 PM
- Role: Full-stack Developer / Technical Lead
Challenge
Enterprise Corp is a large company with over 1,000 employees, struggling with traditional internal training processes.
Background
The current training process faced multiple issues:
- Manual training: Organizing offline classes was time-consuming and costly
- No tracking: Unable to monitor employee learning progress
- Difficult to scale: Hard to organize training across multiple locations
- No certification: No automated certificate issuance system
- Outdated content: Difficult to update and maintain training materials
Specific Requirements
- Video-based Learning: Platform to host and stream training videos
- Progress Tracking: Monitor each employee's learning progress
- Quiz System: Assess knowledge after each module
- Certificate Generation: Automatically create certificates upon completion
- Admin Dashboard: Manage courses, users and reports
- Mobile Support: Learn on any device
Key Challenges
- Video hosting: Need cost-effective solution for hosting hundreds of videos
- Performance: Smooth streaming for many concurrent users
- User engagement: Design UX/UI to encourage course completion
- Reporting: Detailed analytics for HR department
- Time constraint: Must complete in 2 months to meet major training cycle
Solution
We developed a custom Learning Management System (LMS) with Next.js, optimized for video streaming and user engagement.
Approach & Methodology
- Methodology: Agile with 1-week sprints
- Development: Rapid prototyping and iterative development
- Testing: Continuous testing with real users
- Deployment: Vercel for fast deployment and scaling
System Architecture
Core Components:
-
Frontend: Next.js 15 with App Router
- Server-side rendering for better SEO
- Optimistic UI updates
- Progressive Web App (PWA) capabilities
-
Backend: Next.js API Routes + PostgreSQL
- RESTful API for course management
- User progress tracking
- Certificate generation logic
-
Video Storage: AWS S3 + CloudFront CDN
- Cost-effective video hosting
- Global content delivery
- Adaptive bitrate streaming
-
Database: PostgreSQL with Prisma ORM
- User management
- Course data and progress tracking
- Quiz results and analytics
-
File Processing: FFmpeg for video processing
- Video transcoding
- Thumbnail generation
- Multiple quality versions
-
Authentication: NextAuth.js with corporate SSO
- Single Sign-On with Azure AD
- Role-based access control (RBAC)
- Session management
Technology Decisions
Why Next.js:
- Fast development with App Router
- API routes for backend logic
- Excellent performance out-of-the-box
- Easy deployment with Vercel
Why AWS S3 + CloudFront:
- Cost-effective for video storage
- Global CDN for fast delivery
- Scalable to handle growth
- Secure with signed URLs
Why PostgreSQL:
- Reliable for critical business data
- Complex queries for reporting
- ACID compliance
- Good performance with proper indexing
Why FFmpeg:
- Industry-standard video processing
- Multiple format support
- Quality optimization
- Thumbnail generation
Key Features
-
Course Management
- Course creation and organization
- Module and lesson structure
- Rich text editor for descriptions
- File attachments (PDFs, documents)
-
Video Learning
- Adaptive bitrate streaming
- Resume playback functionality
- Video bookmarks
- Playback speed control
- Subtitle support
-
Quiz System
- Multiple choice questions
- True/false questions
- Passing score requirements
- Instant feedback
- Retry options
-
Progress Tracking
- Real-time progress updates
- Course completion percentage
- Time spent tracking
- Learning analytics
-
Certificate Generation
- Automatic certificate creation
- PDF download
- Digital signatures
- Verification system
-
Admin Dashboard
- User management
- Course analytics
- Completion reports
- Performance metrics
- Bulk user import
Implementation
Phase 1: Foundation (Weeks 1-2)
Goals:
- Setup project infrastructure
- Design database schema
- Implement authentication
- Basic course structure
Week 1: Project Setup
// Database schema design with Prisma
model User {
id String @id @default(uuid())
email String @unique
name String
role Role @default(EMPLOYEE)
enrollments Enrollment[]
quizAttempts QuizAttempt[]
certificates Certificate[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Course {
id String @id @default(uuid())
title String
description String @db.Text
thumbnail String?
modules Module[]
enrollments Enrollment[]
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Module {
id String @id @default(uuid())
courseId String
course Course @relation(fields: [courseId], references: [id])
title String
order Int
lessons Lesson[]
createdAt DateTime @default(now())
}
model Lesson {
id String @id @default(uuid())
moduleId String
module Module @relation(fields: [moduleId], references: [id])
title String
type LessonType // VIDEO, QUIZ, READING
videoUrl String?
content String? @db.Text
duration Int? // Duration in seconds
order Int
progresses LessonProgress[]
createdAt DateTime @default(now())
}
model Enrollment {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id])
courseId String
course Course @relation(fields: [courseId], references: [id])
progress Int @default(0) // Percentage
completedAt DateTime?
certificateId String?
lessonProgress LessonProgress[]
enrolledAt DateTime @default(now())
}
model LessonProgress {
id String @id @default(uuid())
enrollmentId String
enrollment Enrollment @relation(fields: [enrollmentId], references: [id])
lessonId String
lesson Lesson @relation(fields: [lessonId], references: [id])
completed Boolean @default(false)
timeSpent Int @default(0) // Seconds
lastPosition Int? // For video resume
completedAt DateTime?
updatedAt DateTime @updatedAt
}
Week 2: Authentication & Basic UI
Implement SSO authentication:
// NextAuth.js configuration with Azure AD
import NextAuth from 'next-auth';
import AzureADProvider from 'next-auth/providers/azure-ad';
export const authOptions = {
providers: [
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID!,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,
tenantId: process.env.AZURE_AD_TENANT_ID!,
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.role = token.role;
}
return session;
},
},
};
export default NextAuth(authOptions);
Phase 2: Core Features (Weeks 3-5)
Week 3: Video Upload & Processing
Implement video upload with AWS S3:
// API route for video upload
import { NextRequest, NextResponse } from 'next/server';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3Client = new S3Client({
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
export async function POST(req: NextRequest) {
try {
const { fileName, fileType } = await req.json();
const key = `videos/${Date.now()}-${fileName}`;
// Generate presigned URL for upload
const command = new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET!,
Key: key,
ContentType: fileType,
});
const uploadUrl = await getSignedUrl(s3Client, command, {
expiresIn: 3600, // 1 hour
});
return NextResponse.json({
uploadUrl,
key,
});
} catch (error) {
return NextResponse.json(
{ error: 'Upload failed' },
{ status: 500 }
);
}
}
Video player component with progress tracking:
// VideoPlayer component
'use client';
import { useRef, useEffect } from 'react';
import { useRouter } from 'next/navigation';
interface VideoPlayerProps {
videoUrl: string;
lessonId: string;
lastPosition?: number;
onProgress: (position: number) => void;
onComplete: () => void;
}
export default function VideoPlayer({
videoUrl,
lessonId,
lastPosition = 0,
onProgress,
onComplete,
}: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const progressInterval = useRef<NodeJS.Timeout>();
useEffect(() => {
// Resume from last position
if (videoRef.current && lastPosition > 0) {
videoRef.current.currentTime = lastPosition;
}
// Track progress every 5 seconds
progressInterval.current = setInterval(() => {
if (videoRef.current) {
onProgress(videoRef.current.currentTime);
}
}, 5000);
return () => {
if (progressInterval.current) {
clearInterval(progressInterval.current);
}
};
}, [lastPosition, onProgress]);
const handleEnded = () => {
onComplete();
};
return (
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden">
<video
ref={videoRef}
src={videoUrl}
controls
className="w-full h-full"
onEnded={handleEnded}
>
Your browser does not support video playback.
</video>
</div>
);
}
Week 4: Quiz System
Quiz component with instant feedback:
// Quiz component
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
interface QuizProps {
questions: Question[];
lessonId: string;
passingScore: number;
}
export default function Quiz({ questions, lessonId, passingScore }: QuizProps) {
const [currentQuestion, setCurrentQuestion] = useState(0);
const [answers, setAnswers] = useState<Record<string, string>>({});
const [showResults, setShowResults] = useState(false);
const router = useRouter();
const handleAnswer = (questionId: string, answerId: string) => {
setAnswers(prev => ({
...prev,
[questionId]: answerId,
}));
};
const handleSubmit = async () => {
// Calculate score
let correct = 0;
questions.forEach(q => {
if (answers[q.id] === q.correctAnswerId) {
correct++;
}
});
const score = (correct / questions.length) * 100;
const passed = score >= passingScore;
// Save attempt
await fetch('/api/quiz/attempt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
lessonId,
answers,
score,
passed,
}),
});
if (passed) {
// Mark lesson as complete
await fetch(`/api/lessons/${lessonId}/complete`, {
method: 'POST',
});
router.push('/dashboard');
} else {
setShowResults(true);
}
};
if (showResults) {
return (
<div className="text-center py-12">
<h2 className="text-2xl font-bold text-red-600 mb-4">
Score too low. Please try again.
</h2>
<button
onClick={() => {
setAnswers({});
setCurrentQuestion(0);
setShowResults(false);
}}
className="bg-blue-600 text-white px-6 py-2 rounded-lg"
>
Retry Quiz
</button>
</div>
);
}
const question = questions[currentQuestion];
return (
<div className="max-w-2xl mx-auto py-8">
<div className="mb-4">
<span className="text-sm text-gray-600">
Question {currentQuestion + 1} of {questions.length}
</span>
<div className="w-full bg-gray-200 h-2 rounded-full mt-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{
width: `${((currentQuestion + 1) / questions.length) * 100}%`,
}}
/>
</div>
</div>
<h3 className="text-xl font-semibold mb-6">{question.text}</h3>
<div className="space-y-3">
{question.options.map(option => (
<button
key={option.id}
onClick={() => handleAnswer(question.id, option.id)}
className={`w-full text-left p-4 border-2 rounded-lg transition-all ${
answers[question.id] === option.id
? 'border-blue-600 bg-blue-50'
: 'border-gray-300 hover:border-gray-400'
}`}
>
{option.text}
</button>
))}
</div>
<div className="mt-8 flex justify-between">
<button
onClick={() => setCurrentQuestion(prev => Math.max(0, prev - 1))}
disabled={currentQuestion === 0}
className="px-6 py-2 bg-gray-200 rounded-lg disabled:opacity-50"
>
Previous
</button>
{currentQuestion === questions.length - 1 ? (
<button
onClick={handleSubmit}
className="px-6 py-2 bg-blue-600 text-white rounded-lg"
>
Submit Quiz
</button>
) : (
<button
onClick={() => setCurrentQuestion(prev => prev + 1)}
className="px-6 py-2 bg-blue-600 text-white rounded-lg"
>
Next
</button>
)}
</div>
</div>
);
}
Week 5: Certificate Generation
Certificate generation with PDF:
// Certificate generation API
import { NextRequest, NextResponse } from 'next/server';
import PDFDocument from 'pdfkit';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
export async function POST(req: NextRequest) {
try {
const { userId, courseId, userName, courseName } = await req.json();
// Generate PDF certificate
const doc = new PDFDocument({
size: 'A4',
layout: 'landscape',
});
const chunks: Buffer[] = [];
doc.on('data', chunk => chunks.push(chunk));
// Design certificate
doc.fontSize(48)
.font('Helvetica-Bold')
.text('Certificate of Completion', 50, 100, { align: 'center' });
doc.fontSize(20)
.font('Helvetica')
.text('This is to certify that', 50, 200, { align: 'center' });
doc.fontSize(32)
.font('Helvetica-Bold')
.text(userName, 50, 250, { align: 'center' });
doc.fontSize(20)
.font('Helvetica')
.text('has successfully completed the course', 50, 320, { align: 'center' });
doc.fontSize(28)
.font('Helvetica-Bold')
.text(courseName, 50, 370, { align: 'center' });
doc.fontSize(16)
.font('Helvetica')
.text(`Date: ${new Date().toLocaleDateString()}`, 50, 450, { align: 'center' });
doc.end();
// Wait for PDF generation
await new Promise(resolve => doc.on('end', resolve));
const pdfBuffer = Buffer.concat(chunks);
// Upload to S3
const s3Client = new S3Client({ region: process.env.AWS_REGION! });
const key = `certificates/${userId}-${courseId}-${Date.now()}.pdf`;
await s3Client.send(new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET!,
Key: key,
Body: pdfBuffer,
ContentType: 'application/pdf',
}));
const certificateUrl = `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;
// Save certificate record
// ... database logic ...
return NextResponse.json({ certificateUrl });
} catch (error) {
return NextResponse.json(
{ error: 'Certificate generation failed' },
{ status: 500 }
);
}
}
Phase 3: Admin Dashboard & Analytics (Weeks 6-7)
Admin dashboard with reports:
// Admin dashboard with analytics
'use client';
import { useEffect, useState } from 'react';
import { Chart } from 'react-chartjs-2';
export default function AdminDashboard() {
const [stats, setStats] = useState({
totalUsers: 0,
activeUsers: 0,
completedCourses: 0,
averageCompletionRate: 0,
});
useEffect(() => {
fetch('/api/admin/stats')
.then(res => res.json())
.then(setStats);
}, []);
return (
<div className="p-8">
<h1 className="text-3xl font-bold mb-8">Admin Dashboard</h1>
{/* Stats cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<StatsCard
title="Total Users"
value={stats.totalUsers}
icon="👥"
/>
<StatsCard
title="Active Users"
value={stats.activeUsers}
icon="✅"
/>
<StatsCard
title="Completed Courses"
value={stats.completedCourses}
icon="🎓"
/>
<StatsCard
title="Avg Completion Rate"
value={`${stats.averageCompletionRate}%`}
icon="📊"
/>
</div>
{/* Charts and detailed analytics */}
{/* ... more components ... */}
</div>
);
}
Phase 4: Testing & Launch (Week 8)
Testing with real users:
- Internal beta testing with 50 employees
- Gather feedback and fix issues
- Performance optimization
- Final deployment
Outcome
After 2 months of development and 3 months of operation, the platform has delivered impressive results.
Key Metrics
| Metric | Target | Achieved | Status | |--------|--------|----------|--------| | Employees Trained | 300+ | 500+ | ✅ +67% | | Completion Rate | 80% | 95% | ✅ +19% | | Training Cost per Employee | - | -60% | ✅ Achieved | | Training Time | - | -40% | ✅ Achieved | | Platform Uptime | 99% | 99.8% | ✅ Exceeded | | User Satisfaction | 4.0/5 | 4.7/5 | ✅ Exceeded |
Business Impact
-
500+ employees trained: In the first 3 months
- Exceeded initial target by 67%
- All departments covered
- Training materials standardized
-
95% completion rate:
- Much higher than traditional training (60-70%)
- Engaging UX/UI helps retention
- Flexible learning schedule
-
60% cost savings:
- No venue rental needed
- No trainer costs
- No travel expenses
- Content reusable many times
-
40% reduction in training time:
- Self-paced learning
- On-demand access
- No scheduling conflicts
- Watch on any device
-
Automated reporting:
- Real-time analytics
- Export reports easily
- Track individual progress
- Identify struggling employees
Client Testimonial
"This platform has transformed how we train employees. From a manual, time-consuming process, we now have an automated system with complete tracking. ROI exceeded expectations by far."
— HR Director, Enterprise Corp
"Employees love the platform's flexibility. They can learn in their free time, on any device. The 95% completion rate is the clearest evidence."
— Learning & Development Manager, Enterprise Corp
Lessons Learned
Technical Learnings:
-
Video optimization is critical:
- Multiple quality levels improve experience
- Adaptive streaming reduces buffering
- CDN distribution is a must-have
-
Progress tracking must be real-time:
- Users appreciate seeing progress
- Gamification increases engagement
- Instant feedback motivates completion
-
Mobile-first design:
- 60% traffic from mobile devices
- Responsive design is not optional
- PWA capabilities add value
Process Learnings:
-
User testing early:
- Beta testing reveals critical UX issues
- Real user feedback is invaluable
- Iterate based on data
-
Keep it simple:
- Simple UI increases adoption
- Don't over-engineer
- Focus on core features first
-
Automated reporting saves time:
- HR appreciates easy exports
- Real-time data helps decisions
- Visual analytics tell story better
Future Enhancements
Phase 5: Advanced Features (Q4 2024)
- Discussion forums
- Peer-to-peer learning
- Live webinar integration
- Advanced analytics
Phase 6: Mobile Apps (Q1 2025)
- Native iOS and Android apps
- Offline viewing capability
- Push notifications
- Better mobile experience
Phase 7: AI Integration (Q2 2025)
- AI-powered course recommendations
- Automatic quiz generation
- Personalized learning paths
- Chatbot support
Technology Stack
Frontend
- Framework: Next.js 15.5.4 (App Router)
- Language: TypeScript 5.9
- Styling: Tailwind CSS 4.1
- UI Library: React 19.0
- State Management: Zustand
Backend
- Runtime: Node.js 20+
- API: Next.js API Routes
- Authentication: NextAuth.js with Azure AD SSO
- Video Processing: FFmpeg
Database & Storage
- Database: PostgreSQL 15 with Prisma ORM
- Video Storage: AWS S3
- CDN: AWS CloudFront
- File Storage: AWS S3
Integrations
- SSO: Azure Active Directory
- Email: Resend API
- Analytics: Custom analytics dashboard
- PDF Generation: PDFKit
DevOps
- Hosting: Vercel
- Database Hosting: AWS RDS
- CI/CD: GitHub Actions
- Monitoring: Sentry + Vercel Analytics
Testing
- Unit Tests: Jest
- E2E Tests: Playwright
- Code Quality: ESLint + Prettier
Interested in a similar training platform? Get in touch to discuss how we can help your organization.
Interested in a similar project?
Let's discuss your project. I'm ready to help you turn your ideas into reality.
Contact Me