Training App LMS Features
Architecture for LMS capabilities - courses, lessons, progress tracking, assessments, and certifications using direct Supabase client
Implementation:
apps/command-center/src/features/training/progress/- Progress tracking hooksapps/command-center/src/features/training/content/- Course content systemapps/command-center/src/routes/training/- Training routespackages/ui/src/components/training/- Shared UI components
Technology Stack:
- Data Access: Direct Supabase client via
useSupabase()from @repo/ui - Auth: WorkOS AuthKit + useUserProfile() (simple - no org switching)
- Quizzes: TypeformSurvey from @repo/ui
- Content: React components with course registry
Overview
The LMS features extend the training app shell with Learning Management System capabilities: React component-based courses, progress tracking, assessments using TypeformSurvey, and auto-awarded certifications.
Key Design Decisions:
- Direct Supabase client instead of API layer (KISS principle)
useSupabase()from@repo/ui/supabasefor all database operations- Simple auth model (no organization switching layer)
- Content as React components (no CMS complexity)
Content Model
Course Structure (File-based)
apps/command-center/src/features/training/content/
├── courses/
│ ├── ai-orchestration-fundamentals/
│ │ ├── course.config.ts # Course metadata
│ │ ├── Lesson01BuildingBlocks.tsx # React component
│ │ ├── Lesson02Workflows.tsx
│ │ └── FinalAssessment.tsx # Uses LessonQuiz
│ └── sales-fundamentals/
│ └── ...
├── certifications/
│ └── certifications.config.ts # Certification definitions
└── registry.ts # Auto-discovers coursesConfiguration Types
// apps/command-center/src/features/training/content/types.ts
interface CourseConfig {
slug: string
title: string
description: string
audience: 'internal' | 'developer' | 'customer' | 'public'
estimatedDuration: string
lessons: LessonConfig[]
assessments?: AssessmentConfig[]
certification?: CertificationRequirement
}
interface LessonConfig {
slug: string
title: string
duration: string
order: number
component: React.ComponentType
}
interface AssessmentConfig {
slug: string
title: string
passingScore: number // 0-100
questions: TypeformQuestion[]
}
interface CertificationRequirement {
certificationSlug: string
requiredLessons: 'all' | string[]
requiredAssessments?: { slug: string; minScore: number }[]
}Database Schema
All tables use training_ prefix. Access control enforced via RLS policies.
-- Lesson completion tracking
CREATE TABLE training_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
organization_id UUID NOT NULL REFERENCES organizations(id),
course_slug TEXT NOT NULL,
lesson_slug TEXT NOT NULL,
completed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(user_id, course_slug, lesson_slug)
);
-- Assessment results
CREATE TABLE training_assessments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
organization_id UUID NOT NULL REFERENCES organizations(id),
course_slug TEXT NOT NULL,
assessment_slug TEXT NOT NULL,
score DECIMAL(5,2) NOT NULL,
passed BOOLEAN NOT NULL,
answers JSONB,
attempted_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Awarded certifications
CREATE TABLE training_certifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
organization_id UUID NOT NULL REFERENCES organizations(id),
certification_slug TEXT NOT NULL,
awarded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(user_id, certification_slug)
);RLS Policies
Each training table has 3 policies using OR logic:
- Platform admins - Full access to all training data
- Org admins - Full access to training data in their organization
- Regular users - Full access to their own training data
-- Example policy structure (applied to all 3 tables)
CREATE POLICY "Platform admins have full access"
ON training_progress TO authenticated
USING (current_user_is_platform_admin());
CREATE POLICY "Org admins can manage training"
ON training_progress TO authenticated
USING (is_org_admin(organization_id));
CREATE POLICY "Users have full access to own data"
ON training_progress TO authenticated
USING (user_id = current_user_supabase_id());Dual ID System
The platform uses WorkOS for authentication but Supabase for the database, requiring ID translation:
| System | ID Format | Example |
|---|---|---|
| WorkOS | String | user_01K423P4W2SJKNJABC17BC4M7X |
| Supabase | UUID | a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
The current_user_supabase_id() helper bridges this gap:
CREATE FUNCTION public.current_user_supabase_id() RETURNS uuid AS $$
SELECT id FROM users WHERE workos_user_id = (auth.jwt() ->> 'sub')::text
$$ LANGUAGE sql STABLE SECURITY DEFINER;Why this is needed: Standard auth.uid() returns the WorkOS ID string, which cannot directly compare to UUID columns. The helper function looks up the corresponding Supabase UUID from the users table.
RLS Helper Functions
| Function | Purpose |
|---|---|
current_user_supabase_id() | Maps WorkOS ID to Supabase UUID for user-scoped RLS |
current_user_is_platform_admin() | Checks if current user is platform admin |
is_org_admin(org_id) | Checks if current user is admin of specified org |
UI Components
New components in packages/ui/src/components/training/:
| Component | Purpose |
|---|---|
LessonContainer | Wrapper with header, navigation, mark complete |
LessonContent | Styled content area with proper spacing |
Callout | Info/warning/tip/danger boxes |
CodeBlock | Syntax-highlighted code with copy button |
LessonNavigation | Prev/Next buttons + progress indicator |
CourseCard | Card for course catalog display |
ProgressRing | Circular progress indicator |
Example Lesson Component
// apps/command-center/src/features/training/content/courses/ai-orchestration-fundamentals/Lesson01BuildingBlocks.tsx
import { LessonContainer, Callout, KeyTakeaways } from '@repo/ui'
import { Stack, Text } from '@mantine/core'
export default function Lesson01BuildingBlocks() {
return (
<LessonContainer
title="The Building Blocks"
subtitle="AI Orchestration Fundamentals"
duration="8 min"
>
<Stack gap="xl">
<Text>In this lesson, you'll learn the core building blocks...</Text>
<Callout type="info" title="What you'll learn">
<Text>- Core platform concepts</Text>
<Text>- How AI agents work</Text>
<Text>- Basic workflow patterns</Text>
</Callout>
<KeyTakeaways items={[
'Workflows orchestrate step-by-step processes',
'Agents make intelligent decisions',
'Integrations connect external services'
]} />
</Stack>
</LessonContainer>
)
}Route Structure
apps/command-center/src/routes/training/
├── index.tsx # Training dashboard
├── courses/
│ ├── index.tsx # Course catalog
│ └── $courseSlug/
│ ├── index.tsx # Course overview with SubshellSidebar
│ └── $lessonSlug.tsx # Lesson viewer
├── assessments/
│ └── $courseSlug/$assessmentSlug.tsx # Quiz page
└── certifications/
└── index.tsx # My certificationsAuthentication
Training uses the command-center authentication stack:
- WorkOS AuthKit for user authentication
useUserProfile()syncs WorkOS user to SupabaseuserstableuseSupabase()provides authenticated Supabase client for DB operations- Organization context from command-center (user's current org)
See Authentication Architecture for details.
Data Access Pattern
All database operations use direct Supabase client:
// Example: Progress tracking hook
import { useSupabase } from '@repo/ui/supabase'
import { useMutation, useQuery } from '@tanstack/react-query'
export function useMarkComplete() {
const { client } = useSupabase()
return useMutation({
mutationFn: async ({ courseSlug, lessonSlug }) => {
const { error } = await client
.from('training_progress')
.upsert({ course_slug: courseSlug, lesson_slug: lessonSlug })
if (error) throw error
}
})
}Multi-Tenancy
Simplified Model (No Org Switching):
- All tables include
organization_idcolumn (for future use) - User associated with one org based on WorkOS membership
- No org context switching in training app
Related Documentation
- Training Overview - Feature infrastructure and setup
- Content Development - How to create lessons, quizzes, and courses
- Course Subshell - Contextual sidebar navigation
- RLS Policies - Database security patterns
- Authentication - WorkOS integration and dual ID system
Last Updated: 2026-01-12