Custom LMS Integration Guide
Build SCORM capabilities into your custom Learning Management System.
Table of Contents
- Overview
- Architecture
- Implementation Steps
- Code Examples
- UI Components
- Data Synchronization
- Best Practices
Overview
This guide helps you integrate SCORM functionality into your custom LMS, enabling:
- SCORM package management
- Learner session tracking
- Progress and completion reporting
- Grade book integration
Architecture
Recommended Architecture
┌─────────────────┐
│ Custom LMS │
│ │
│ ┌───────────┐ │
│ │ Frontend │ │
│ │ (React/ │ │
│ │ Vue) │ │
│ └─────┬─────┘ │
│ │ │
│ ┌─────▼─────┐ │
│ │ Backend │ │
│ │ (Node/ │ │
│ │ Python) │ │
│ └─────┬─────┘ │
└────────┼────────┘
│
│ API Calls
│
┌────────▼────────┐
│ SCORM API │
│ (AllureLMS) │
└─────────────────┘
Data Flow
- Upload: LMS → SCORM API → Storage
- Launch: LMS → SCORM API → Launch URL
- Tracking: SCORM Player → SCORM API → LMS (via polling/webhooks)
- Reporting: LMS → SCORM API → Analytics
Implementation Steps
Step 1: Database Schema
Add SCORM-related tables to your LMS:
-- Packages table
CREATE TABLE scorm_packages (
id UUID PRIMARY KEY,
scorm_package_id VARCHAR(255) UNIQUE NOT NULL,
title VARCHAR(255) NOT NULL,
version VARCHAR(50),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Sessions table
CREATE TABLE scorm_sessions (
id UUID PRIMARY KEY,
scorm_session_id VARCHAR(255) UNIQUE NOT NULL,
package_id UUID REFERENCES scorm_packages(id),
user_id UUID REFERENCES users(id),
completion_status VARCHAR(50),
success_status VARCHAR(50),
score DECIMAL(5,2),
time_spent_seconds INTEGER,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
Step 2: API Client
Create a SCORM API client:
// lib/scorm-client.ts
export class ScormClient {
constructor(
private apiKey: string,
private baseUrl: string,
private tenantId: string
) {}
/** Mint presigned URL → PUT ZIP → process (run server-side when possible). */
async uploadPackage(file: File, uploadedBy: string) {
const mint = await fetch(`${this.baseUrl}/api/v1/packages/upload-url`, {
method: 'POST',
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenant_id: this.tenantId,
uploaded_by: uploadedBy,
filename: file.name,
file_size: file.size,
}),
});
if (!mint.ok) throw new Error(`Mint failed: ${mint.statusText}`);
const ticket = await mint.json();
const put = await fetch(ticket.presigned_url, {
method: 'PUT',
headers: ticket.required_put_headers,
body: file,
});
if (!put.ok) throw new Error(`Storage upload failed: ${put.status}`);
const proc = await fetch(`${this.baseUrl}/api/v1/packages/process`, {
method: 'POST',
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenant_id: this.tenantId,
uploaded_by: uploadedBy,
storage_path: ticket.storage_path,
original_filename: file.name,
}),
});
if (!proc.ok) throw new Error(`Process failed: ${proc.statusText}`);
return proc.json();
}
async launchSession(packageId: string, userId: string) {
const response = await fetch(
`${this.baseUrl}/api/v1/packages/${packageId}/launch`,
{
method: 'POST',
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: userId,
session_id: crypto.randomUUID(),
}),
}
);
return response.json();
}
async getSession(sessionId: string) {
const response = await fetch(
`${this.baseUrl}/api/v1/sessions/${sessionId}`,
{
headers: { 'X-API-Key': this.apiKey },
}
);
return response.json();
}
}
Step 3: Backend Service
Create service layer:
// services/scorm-service.ts
import { ScormClient } from '@/lib/scorm-client';
import { db } from '@/lib/database';
export class ScormService {
private client: ScormClient;
constructor() {
this.client = new ScormClient(
process.env.CONNECT_API_KEY!,
process.env.CONNECT_API_BASE_URL!,
process.env.CONNECT_TENANT_ID!
);
}
async uploadPackage(file: File, userId: string) {
// Upload to SCORM API
const pkg = await this.client.uploadPackage(file, userId);
// Store in local database
const localPackage = await db.scormPackages.create({
data: {
scorm_package_id: pkg.id,
title: pkg.title,
version: pkg.version,
},
});
return localPackage;
}
async launchCourse(packageId: string, userId: string) {
// Get SCORM package ID from local database
const localPackage = await db.scormPackages.findUnique({
where: { id: packageId },
});
if (!localPackage) {
throw new Error('Package not found');
}
// Launch session
const launch = await this.client.launchSession(
localPackage.scorm_package_id,
userId
);
// Store session in local database
const session = await db.scormSessions.create({
data: {
scorm_session_id: launch.session_id,
package_id: packageId,
user_id: userId,
completion_status: 'not_attempted',
},
});
return {
sessionId: session.id,
playerUrl: launch.launch_url,
};
}
async syncSessionProgress(sessionId: string) {
// Get local session
const localSession = await db.scormSessions.findUnique({
where: { id: sessionId },
});
if (!localSession) {
throw new Error('Session not found');
}
// Get latest from SCORM API
const scormSession = await this.client.getSession(
localSession.scorm_session_id
);
// Update local database
const updated = await db.scormSessions.update({
where: { id: sessionId },
data: {
completion_status: scormSession.completion_status,
success_status: scormSession.success_status,
score: scormSession.score?.scaled,
time_spent_seconds: scormSession.time_spent_seconds,
},
});
return updated;
}
}
Code Examples
Upload Endpoint
// app/api/courses/upload/route.ts
import { ScormService } from '@/services/scorm-service';
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get('file') as File;
const userId = formData.get('userId') as string;
const scormService = new ScormService();
const package = await scormService.uploadPackage(file, userId);
return Response.json({ package });
}
Launch Endpoint
// app/api/courses/[id]/launch/route.ts
import { ScormService } from '@/services/scorm-service';
export async function POST(
request: Request,
{ params }: { params: { id: string } }
) {
const { userId } = await request.json();
const packageId = params.id;
const scormService = new ScormService();
const launch = await scormService.launchCourse(packageId, userId);
return Response.json(launch);
}
Progress Sync Endpoint
// app/api/sessions/[id]/sync/route.ts
import { ScormService } from '@/services/scorm-service';
export async function POST(
request: Request,
{ params }: { params: { id: string } }
) {
const sessionId = params.id;
const scormService = new ScormService();
const session = await scormService.syncSessionProgress(sessionId);
return Response.json(session);
}
UI Components
Package Upload Component
// components/ScormUpload.tsx
'use client';
import { useState } from 'react';
export function ScormUpload({ onUpload }: { onUpload: (pkg: any) => void }) {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
async function handleUpload(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append('file', file);
formData.append('userId', getCurrentUserId());
try {
const response = await fetch('/api/courses/upload', {
method: 'POST',
body: formData,
});
const package = await response.json();
onUpload(package);
} catch (error) {
console.error('Upload failed:', error);
} finally {
setUploading(false);
}
}
return (
<div>
<input
type="file"
accept=".zip"
onChange={handleUpload}
disabled={uploading}
/>
{uploading && <div>Uploading... {progress}%</div>}
</div>
);
}
Player Component
// components/ScormPlayer.tsx
'use client';
import { useEffect, useState } from 'react';
export function ScormPlayer({
packageId,
userId,
}: {
packageId: string;
userId: string;
}) {
const [playerUrl, setPlayerUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function launch() {
try {
const response = await fetch(`/api/courses/${packageId}/launch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
});
const { playerUrl } = await response.json();
setPlayerUrl(playerUrl);
} catch (error) {
console.error('Launch failed:', error);
} finally {
setLoading(false);
}
}
launch();
}, [packageId, userId]);
if (loading) return <div>Loading course...</div>;
if (!playerUrl) return <div>Failed to load course</div>;
return (
<iframe
src={playerUrl}
width="100%"
height="800px"
frameBorder="0"
allow="fullscreen"
title="SCORM Course"
/>
);
}
Data Synchronization
Polling Strategy
// hooks/useScormProgress.ts
import { useEffect, useState } from 'react';
export function useScormProgress(sessionId: string) {
const [progress, setProgress] = useState<any>(null);
useEffect(() => {
const interval = setInterval(async () => {
try {
const response = await fetch(`/api/sessions/${sessionId}/sync`, {
method: 'POST',
});
const session = await response.json();
setProgress(session);
if (session.completion_status === 'completed') {
clearInterval(interval);
}
} catch (error) {
console.error('Sync failed:', error);
}
}, 5000); // Poll every 5 seconds
return () => clearInterval(interval);
}, [sessionId]);
return progress;
}
Webhook Integration
// app/api/webhooks/scorm/route.ts
export async function POST(request: Request) {
// Verify webhook signature
const signature = request.headers.get('X-Allure-Signature');
const body = await request.text();
// Verify signature (implement HMAC verification)
if (!verifySignature(signature, body)) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
switch (event.event) {
case 'package.processing.completed':
await handlePackageProcessed(event.data);
break;
case 'session.completed':
await handleSessionCompleted(event.data);
break;
}
return new Response('OK');
}
Best Practices
- Cache Package Metadata: Store package info locally to reduce API calls
- Async Processing: Handle package processing asynchronously
- Error Recovery: Implement retry logic for transient failures
- Progress Tracking: Use webhooks when possible, polling as fallback
- Grade Book Integration: Sync scores to your LMS grade book
- User Experience: Show loading states and progress indicators
Related Documentation
Last Updated: 2025-01-15