Elevasis LogoElevasis Docs

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 hooks
  • apps/command-center/src/features/training/content/ - Course content system
  • apps/command-center/src/routes/training/ - Training routes
  • packages/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/supabase for 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 courses

Configuration 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:

  1. Platform admins - Full access to all training data
  2. Org admins - Full access to training data in their organization
  3. 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:

SystemID FormatExample
WorkOSStringuser_01K423P4W2SJKNJABC17BC4M7X
SupabaseUUIDa1b2c3d4-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

FunctionPurpose
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/:

ComponentPurpose
LessonContainerWrapper with header, navigation, mark complete
LessonContentStyled content area with proper spacing
CalloutInfo/warning/tip/danger boxes
CodeBlockSyntax-highlighted code with copy button
LessonNavigationPrev/Next buttons + progress indicator
CourseCardCard for course catalog display
ProgressRingCircular 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 certifications

Authentication

Training uses the command-center authentication stack:

  • WorkOS AuthKit for user authentication
  • useUserProfile() syncs WorkOS user to Supabase users table
  • useSupabase() 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_id column (for future use)
  • User associated with one org based on WorkOS membership
  • No org context switching in training app


Last Updated: 2026-01-12