在過去兩年中,我建立了三個學習管理系統平台。其中兩個使用了現成的解決方案,我們最終都把它們移除了。第三個——實際上運作良好的那個——是一個基於 Next.js 和 Supabase 的無頭架構。這就是我要帶你一起建立的。

無頭學習管理系統的宣傳很簡單:商業平台如 Teachable、Thinkific,甚至 Moodle 都提供了一種單一的體驗,其中內容模型、前端、驗證系統和支付流程全部焊接在一起。一旦你需要自訂功能——基於角色的內容控制、AI 驅動的測驗生成、白標多租戶部署——你就會被平台所困,而不是在開發功能。

透過 Supabase 處理你的資料庫、驗證、儲存和實時訂閱,以及 Next.js 處理你的渲染和 API 層,你可以獲得一個真正屬於你自己的系統。內容端沒有供應商鎖定。對學習者體驗擁有完全控制。以及一個可以直接查詢的 Postgres 資料庫,無需從伺服器元件進行 HTTP 往返。

讓我們來建立它。

目錄

使用 Supabase 和 Next.js 在 2026 年建立無頭學習管理系統

為什麼為學習管理系統選擇無頭架構

傳統的學習管理系統平台並非為現代網路而建立。它們是為想要簽署合規性複選框的 IT 部門而建立的。使用者體驗反映了這一點。

無頭學習管理系統將你的內容管理和業務邏輯與表現層分離。這意味著:

  • 你的前端可以是任何東西。 一個 Next.js 網路應用、一個 React Native 行動應用、一個提供微課程的 Slack 機器人。同一個 API 為所有這些提供服務。
  • 你的內容模型是你的。 你定義「課程」是什麼。也許這是一系列 markdown 課程。也許這是一個帶有嵌入式互動練習的視頻模組集合。你不受 Moodle 認為課程應該是什麼的限制。
  • 你控制資料。 學習者進度、評估結果、參與度指標——它們全部存在於你的 Postgres 資料庫中。不需要從供應商儀表板匯出 CSV。

權衡很明顯:你必須自己建立更多。但透過 Supabase 處理驗證、儲存、實時和資料庫層,你實際編寫的自訂程式碼量令人驚訝地少。

架構概述

以下是我們正在建立的高層架構:

┌─────────────────────────────────────────────┐
│                 Next.js 15                   │
│          (App Router + RSC)                  │
│                                              │
│  ┌──────────┐  ┌──────────┐  ┌───────────┐  │
│  │ Server   │  │ Server   │  │ Client    │  │
│  │Components│  │ Actions  │  │Components │  │
│  └────┬─────┘  └────┬─────┘  └─────┬─────┘  │
│       │              │              │        │
│       └──────────────┼──────────────┘        │
│                      │                       │
│              Supabase SSR Client             │
└──────────────────────┬───────────────────────┘
                       │
         ┌─────────────┼─────────────┐
         │             │             │
    ┌────▼────┐  ┌─────▼────┐  ┌────▼─────┐
    │Supabase │  │Supabase  │  │Supabase  │
    │  Auth   │  │ Database │  │ Storage  │
    │         │  │(Postgres)│  │ (S3)     │
    └─────────┘  └──────────┘  └──────────┘
                       │
                 ┌─────▼─────┐
                 │  Realtime  │
                 │ (WebSocket)│
                 └───────────┘

關鍵的架構決策:

決策 選擇 基本原理
渲染 伺服器元件 + ISR 課程目錄頁面大多是靜態的;學習者儀表板需要新鮮資料
驗證 Supabase 驗證與 PKCE 內建 RLS 整合,無需獨立驗證服務
資料庫 Supabase Postgres 直接從伺服器元件查詢,無需 ORM
檔案儲存 Supabase 儲存 用於視頻內容的簽署 URL、RLS 控制的存取
實時 Supabase Realtime 討論論壇、實時進度更新
付款 Stripe(透過 webhooks) Supabase 邊緣函式處理 webhook 處理
內容格式 儲存在 DB 中的 MDX 在 React 元件中渲染的結構化內容

Next.js 15 中的伺服器元件可以直接使用伺服器客戶端查詢 Supabase——不需要 API 路由、沒有序列化邊界。對於學習管理系統來說,這是一個巨大的勝利,因為你在每次頁面加載時都在取得課程資料、使用者進度和註冊狀態。

資料庫架構設計

架構是任何學習管理系統的骨幹。我已經經歷了足夠多的迭代,知道早期把這件事做對會節省你數周的時間。以下是一個處理多租戶課程、結構化模組、進度跟蹤和評估的架構。

-- 組織(用於多租戶 / 白標)
CREATE TABLE organizations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  slug TEXT UNIQUE NOT NULL,
  settings JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 擴展 Supabase 驗證的使用者設定檔
CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  email TEXT NOT NULL,
  full_name TEXT,
  avatar_url TEXT,
  role TEXT NOT NULL DEFAULT 'learner' CHECK (role IN ('admin', 'instructor', 'learner')),
  organization_id UUID REFERENCES organizations(id),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 課程
CREATE TABLE courses (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
  instructor_id UUID REFERENCES profiles(id),
  title TEXT NOT NULL,
  slug TEXT NOT NULL,
  description TEXT,
  thumbnail_url TEXT,
  status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
  price_cents INTEGER DEFAULT 0,
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(organization_id, slug)
);

-- 模組(課程內的部分)
CREATE TABLE modules (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  course_id UUID REFERENCES courses(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  sort_order INTEGER NOT NULL DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 課程(個別內容單位)
CREATE TABLE lessons (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  module_id UUID REFERENCES modules(id) ON DELETE CASCADE,
  title TEXT NOT NULL,
  content_type TEXT NOT NULL CHECK (content_type IN ('video', 'text', 'quiz', 'assignment')),
  content JSONB NOT NULL DEFAULT '{}',
  sort_order INTEGER NOT NULL DEFAULT 0,
  duration_minutes INTEGER,
  is_free_preview BOOLEAN DEFAULT false,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 註冊
CREATE TABLE enrollments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,
  course_id UUID REFERENCES courses(id) ON DELETE CASCADE,
  enrolled_at TIMESTAMPTZ DEFAULT NOW(),
  completed_at TIMESTAMPTZ,
  stripe_payment_id TEXT,
  UNIQUE(user_id, course_id)
);

-- 進度跟蹤
CREATE TABLE lesson_progress (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,
  lesson_id UUID REFERENCES lessons(id) ON DELETE CASCADE,
  status TEXT DEFAULT 'not_started' CHECK (status IN ('not_started', 'in_progress', 'completed')),
  progress_pct SMALLINT DEFAULT 0 CHECK (progress_pct BETWEEN 0 AND 100),
  completed_at TIMESTAMPTZ,
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(user_id, lesson_id)
);

-- 測驗嘗試
CREATE TABLE quiz_attempts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,
  lesson_id UUID REFERENCES lessons(id) ON DELETE CASCADE,
  answers JSONB NOT NULL,
  score SMALLINT,
  passed BOOLEAN,
  attempted_at TIMESTAMPTZ DEFAULT NOW()
);

一些設計注記:

  • 課程上的 content 欄是 JSONB。 這是有意的。一堂視頻課程儲存 {"video_url": "...", "transcript": "..."}。一堂文字課程儲存 {"mdx": "..."} 。一個測驗儲存 {"questions": [...]}。這為你提供結構化資料,而無需為每種內容類型建立獨立的表格。
  • 模組和課程上的 sort_order 拖放重新排序對任何課程建立者都是必須的。整數排序使這變得微不足道。
  • 課程上的 metadata JSONB。 這是你的逃脫艙。SEO 欄位、自訂品牌、功能旗標——把所有東西都扔進去,而無需架構遷移。

使用 Supabase 和 Next.js 在 2026 年建立無頭學習管理系統 - 架構

設定 Supabase 驗證與基於角色的存取

Supabase 驗證提供電子郵件/密碼、OAuth 和魔法連結現成功能。對於學習管理系統,你通常需要三個角色:管理員、講師和學習者。技巧是將 Supabase 的 auth.users 與你的 profiles 表同步。

首先,設定一個資料庫觸發器,每當新使用者註冊時建立一個設定檔:

CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.profiles (id, email, full_name, avatar_url)
  VALUES (
    NEW.id,
    NEW.email,
    NEW.raw_user_meta_data->>'full_name',
    NEW.raw_user_meta_data->>'avatar_url'
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

現在,在 Next.js 端,設定 Supabase SSR 客戶端。這是使用 @supabase/ssr 的 2026 模式:

// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // 從伺服器元件調用 -- 忽略
          }
        },
      },
    }
  )
}
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

伺服器客戶端自動從 cookie 讀取驗證令牌。在伺服器元件中,你可以這樣做:

// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) redirect('/login')

  const { data: profile } = await supabase
    .from('profiles')
    .select('*, organization:organizations(*)')
    .eq('id', user.id)
    .single()

  const { data: enrollments } = await supabase
    .from('enrollments')
    .select('*, course:courses(*)')
    .eq('user_id', user.id)

  return (
    <div>
      <h1>歡迎回來,{profile?.full_name}</h1>
      {/* 渲染已註冊的課程 */}
    </div>
  )
}

沒有 API 路由。沒有 fetch 呼叫。從伺服器元件直接資料庫查詢。這就是 Supabase + Next.js App Router 的架構優勢。

行級安全性用於多租戶內容

RLS 使此架構實際上安全。沒有它,你的匿名金鑰會給任何人訪問所有內容的權限。有了它,資料庫本身會強制執行誰可以看到和修改什麼。

以下是核心表的策略:

-- 課程:任何人都可以讀取已發佈課程,講師可以管理他們自己的課程
ALTER TABLE courses ENABLE ROW LEVEL SECURITY;

CREATE POLICY "已發佈課程對所有人可見" ON courses
  FOR SELECT USING (status = 'published');

CREATE POLICY "講師管理他們的課程" ON courses
  FOR ALL USING (
    auth.uid() = instructor_id
  );

CREATE POLICY "管理員管理組織課程" ON courses
  FOR ALL USING (
    EXISTS (
      SELECT 1 FROM profiles
      WHERE id = auth.uid()
      AND role = 'admin'
      AND organization_id = courses.organization_id
    )
  );

-- 課程:已註冊使用者可以讀取,免費預覽對所有人可見
ALTER TABLE lessons ENABLE ROW LEVEL SECURITY;

CREATE POLICY "免費預覽課程對所有人可見" ON lessons
  FOR SELECT USING (is_free_preview = true);

CREATE POLICY "已註冊使用者可以讀取課程" ON lessons
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM enrollments e
      JOIN modules m ON m.course_id = e.course_id
      WHERE e.user_id = auth.uid()
      AND m.id = lessons.module_id
    )
  );

-- 進度:使用者只能看到和更新自己的進度
ALTER TABLE lesson_progress ENABLE ROW LEVEL SECURITY;

CREATE POLICY "使用者管理自己的進度" ON lesson_progress
  FOR ALL USING (auth.uid() = user_id);

-- 註冊:使用者看到他們自己的,講師看到他們課程的註冊
ALTER TABLE enrollments ENABLE ROW LEVEL SECURITY;

CREATE POLICY "使用者看到自己的註冊" ON enrollments
  FOR SELECT USING (auth.uid() = user_id);

CREATE POLICY "講師看到課程註冊" ON enrollments
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM courses
      WHERE courses.id = enrollments.course_id
      AND courses.instructor_id = auth.uid()
    )
  );

一件令人困擾的事情:對於 SELECT,RLS 策略是累加的。如果任何策略通過,該行是可見的。對於 INSERTUPDATEDELETE,所有適用的策略都必須通過。在除錯存取問題時請記住這一點。

建立 Next.js App Router 前端

我發現對學習管理系統有效的路由結構如下:

app/
├── (marketing)/
│   ├── page.tsx              # 登陸頁面
│   ├── courses/
│   │   ├── page.tsx           # 課程目錄
│   │   └── [slug]/
│   │       └── page.tsx       # 課程詳情 / 銷售頁面
├── (app)/
│   ├── layout.tsx             # 帶邊欄的已驗證佈局
│   ├── dashboard/
│   │   └── page.tsx           # 學習者儀表板
│   ├── learn/
│   │   └── [courseSlug]/
│   │       └── [lessonId]/
│   │           └── page.tsx   # 課程檢視器
│   ├── instructor/
│   │   ├── courses/
│   │   │   ├── page.tsx       # 講師課程清單
│   │   │   ├── new/
│   │   │   │   └── page.tsx   # 建立課程
│   │   │   └── [id]/
│   │   │       └── edit/
│   │   │           └── page.tsx  # 課程編輯器
├── api/
│   └── webhooks/
│       └── stripe/
│           └── route.ts       # Stripe webhook 處理程序

路由群組 (marketing)(app) 可以讓你使用不同的佈局,而不影響 URL 結構。行銷頁面獲得一個最小的頁頭和頁尾。應用程式部分獲得一個完整的邊欄,帶有課程導航。

以下是用於註冊免費課程的伺服器操作:

// app/actions/enroll.ts
'use server'

import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'

export async function enrollInCourse(courseId: string) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    return { error: '你必須登入才能註冊' }
  }

  // 檢查課程是否免費
  const { data: course } = await supabase
    .from('courses')
    .select('price_cents')
    .eq('id', courseId)
    .single()

  if (course?.price_cents && course.price_cents > 0) {
    return { error: '此課程需要付款' }
  }

  const { error } = await supabase
    .from('enrollments')
    .insert({ user_id: user.id, course_id: courseId })

  if (error) {
    if (error.code === '23505') {
      return { error: '你已經註冊了此課程' }
    }
    return { error: error.message }
  }

  revalidatePath('/dashboard')
  return { success: true }
}

Next.js 15 中的伺服器操作是處理突變的最簡潔的方式。沒有 API 路由,沒有手動 fetch 呼叫。表單只是調用該函式。

課程內容渲染和進度跟蹤

對於課程檢視器,你需要同時發生兩件事:渲染內容和跟蹤進度。以下是我的方法:

// app/(app)/learn/[courseSlug]/[lessonId]/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { LessonViewer } from '@/components/lesson-viewer'
import { ProgressTracker } from '@/components/progress-tracker'

export default async function LessonPage({
  params,
}: {
  params: Promise<{ courseSlug: string; lessonId: string }>
}) {
  const { courseSlug, lessonId } = await params
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) redirect('/login')

  const { data: lesson } = await supabase
    .from('lessons')
    .select(`
      *,
      module:modules(
        *,
        course:courses(*)
      )
    `)
    .eq('id', lessonId)
    .single()

  if (!lesson) redirect('/dashboard')

  // 獲取此課程中的所有課程,以供導航
  const { data: allLessons } = await supabase
    .from('lessons')
    .select('id, title, sort_order, module_id, content_type')
    .eq('module_id', lesson.module_id)
    .order('sort_order')

  // 獲取目前進度
  const { data: progress } = await supabase
    .from('lesson_progress')
    .select('*')
    .eq('user_id', user.id)
    .eq('lesson_id', lessonId)
    .maybeSingle()

  return (
    <div className="flex h-screen">
      <aside className="w-72 border-r overflow-y-auto">
        {/* 帶課程清單的課程邊欄 */}
      </aside>
      <main className="flex-1 overflow-y-auto">
        <LessonViewer
          lesson={lesson}
          progress={progress}
        />
        <ProgressTracker
          lessonId={lessonId}
          initialProgress={progress?.progress_pct ?? 0}
        />
      </main>
    </div>
  )
}

ProgressTracker 是一個客戶端元件,在使用者滾動瀏覽文字內容或觀看視頻時更新進度。它使用瀏覽器 Supabase 客戶端在即時進度中寫入:

// components/progress-tracker.tsx
'use client'

import { useEffect, useCallback } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useDebounce } from '@/hooks/use-debounce'

export function ProgressTracker({
  lessonId,
  initialProgress,
}: {
  lessonId: string
  initialProgress: number
}) {
  const supabase = createClient()

  const updateProgress = useCallback(
    async (pct: number) => {
      await supabase
        .from('lesson_progress')
        .upsert(
          {
            user_id: (await supabase.auth.getUser()).data.user?.id,
            lesson_id: lessonId,
            progress_pct: pct,
            status: pct >= 100 ? 'completed' : 'in_progress',
            completed_at: pct >= 100 ? new Date().toISOString() : null,
          },
          { onConflict: 'user_id,lesson_id' }
        )
    },
    [lessonId, supabase]
  )

  const debouncedUpdate = useDebounce(updateProgress, 2000)

  useEffect(() => {
    const handleScroll = () => {
      const scrollPct = Math.round(
        (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
      )
      if (scrollPct > initialProgress) {
        debouncedUpdate(scrollPct)
      }
    }
    window.addEventListener('scroll', handleScroll)
    return () => window.removeEventListener('scroll', handleScroll)
  }, [debouncedUpdate, initialProgress])

  return null // 此元件沒有 UI
}

將進度更新延遲是至關重要的。你不希望在每次滾動事件時都點擊資料庫。

實時功能:討論和通知

Supabase Realtime 將你的學習管理系統從靜態內容檢視器變成感覺活躍的東西。我總是首先建立的兩個功能:課程討論和講師的進度通知。

// components/lesson-discussion.tsx
'use client'

import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'

export function LessonDiscussion({ lessonId }: { lessonId: string }) {
  const supabase = createClient()
  const [messages, setMessages] = useState<any[]>([])

  useEffect(() => {
    // 取得現有訊息
    supabase
      .from('discussions')
      .select('*, profile:profiles(full_name, avatar_url)')
      .eq('lesson_id', lessonId)
      .order('created_at')
      .then(({ data }) => setMessages(data ?? []))

    // 訂閱新訊息
    const channel = supabase
      .channel(`lesson-${lessonId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'discussions',
          filter: `lesson_id=eq.${lessonId}`,
        },
        async (payload) => {
          // 使用設定檔取得完整訊息
          const { data } = await supabase
            .from('discussions')
            .select('*, profile:profiles(full_name, avatar_url)')
            .eq('id', payload.new.id)
            .single()
          if (data) setMessages((prev) => [...prev, data])
        }
      )
      .subscribe()

    return () => {
      supabase.removeChannel(channel)
    }
  }, [lessonId, supabase])

  return (
    <div>
      {/* 渲染訊息和輸入表單 */}
    </div>
  )
}

Supabase 儲存的影片和檔案儲存

對於視頻內容,Supabase 儲存與簽署 URL 是最好的方式。為課程視頻建立一個私人儲存桶,並在儲存層上使用類似於 RLS 的策略:

-- 儲存桶策略:只有已註冊使用者可以存取課程視頻
CREATE POLICY "已註冊使用者可以檢視課程視頻"
ON storage.objects FOR SELECT
USING (
  bucket_id = 'course-videos'
  AND EXISTS (
    SELECT 1 FROM enrollments e
    JOIN courses c ON c.id = e.course_id
    WHERE e.user_id = auth.uid()
    AND storage.filename(name) LIKE c.id || '/%'
  )
);

在伺服器端,生成短期簽署的 URL:

const { data } = await supabase.storage
  .from('course-videos')
  .createSignedUrl(`${courseId}/${lesson.video_filename}`, 3600) // 1 小時

對於生產環境,你會想在此前面放一個 CDN。Supabase 的內建 CDN 處理大多數情況,但對於高流量視頻交付,請考慮使用 Mux 或 Cloudflare Stream 之類的服務進行轉碼,並只在資料庫中儲存播放 ID。

部署和效能考量

Supabase 免費級別提供 500MB 資料庫、1GB 儲存和 50,000 個月活躍使用者。這足以啟動。Pro 計畫每月 $25,提供 8GB 資料庫、100GB 儲存和無 MAU 限制——這可以處理一個有數千名學習者的嚴肅的學習管理系統。

Supabase 計畫 DB 儲存 檔案儲存 MAU 限制 價格
免費 500MB 1GB 50,000 $0
Pro 8GB 100GB 無限 $25/月
團隊 8GB 100GB 無限 $599/月
企業 自訂 自訂 無限 自訂

對於 Next.js 部署,Vercel 是顯而易見的選擇——你獲得自動 ISR、邊緣中間件和圖像最佳化。但如果你對成本敏感,你可以在 Coolify 或 Railway 上部署花費明顯少得多。

生產中的效能提示:

  • 為課程目錄頁面使用 ISR。 revalidate: 3600 表示你的目錄最多每小時重新構建一次。對大多數用例而言足夠快。
  • 為課程檢視器使用伺服器元件。 初始頁面加載使用 SSR,所有內容都已加載。沒有加載旋轉器。
  • 新增資料庫索引。 最少:enrollments(user_id)lesson_progress(user_id, lesson_id)lessons(module_id, sort_order)courses(organization_id, slug)
  • 連線池。 使用 Supabase 的內建 PgBouncer(池連線字串)用於伺服器端查詢。用於遷移的直接連線。

如果你正在建立像這樣的東西,並且想要幫助架構或實現,我們的團隊定期進行 Next.js 開發無頭 CMS 構建。我們已經推出了幾個 Supabase 支援的應用程式,並且知道尖銳邊緣在哪裡。

常見問題

Supabase 能否處理生產學習管理系統的規模? 是的。Supabase 在受管 Postgres 上運行,可以輕鬆處理數百萬行。Pro 計畫包括通過 PgBouncer 的連線池,你可以獨立地擴展運算。作為參考,Supabase 報告其企業級層上的客戶運行 100k+ 並發連線。對於大多數學習管理系統用例——即使有數萬活躍學習者——每月 $25 的 Pro 計畫也綽綽有餘。

你如何在基於 Supabase 的學習管理系統中處理視頻託管? Supabase 儲存適用於使用簽署 URL 儲存和提供視頻檔案,但它不是視頻平台。對於生產學習管理系統視頻,我建議使用專門的視頻服務,如 Mux($0.007/分鐘用於編碼,$0.00015/秒用於交付)或 Cloudflare Stream($1/1000 分鐘儲存,$5/1000 分鐘交付)。在 Supabase 資料庫中儲存播放 ID,並在前端使用他們的播放器 SDK。這為你提供自適應位元率、分析和 DRM,而無需自己構建任何內容。

到 2026 年,Next.js App Router 對生產學習管理系統的穩定性夠嗎? 絕對地。App Router 自 Next.js 14 以來一直很穩定,Next.js 15 解決了伺服器操作和快取的剩餘粗糙邊緣。伺服器元件對於數據密集型應用程式(如學習管理系統)是真正的架構優勢。Next.js 15 中的 after() API 對於觸發分析事件和進度更新而不阻止響應特別有用。

你如何實現課程完成證書? 在課程級別對完成進行追蹤,其中 lesson_progress 然後將課程完成計算為所有模組中已完成課程的百分比。當使用者達到 100% 時,觸發 Supabase 邊緣函式(或 Next.js 伺服器操作),使用 @react-pdf/renderer 之類的庫生成 PDF 證書,並將其儲存在 Supabase 儲存中。將一行新增到帶有儲存路徑和唯一驗證代碼的 certificates 表。

SCORM 合規性呢? SCORM 是大多數現代學習管理系統平台都在遠離的遺留標準。也就是說,如果你需要 SCORM 支援——通常用於企業合規性培訓——你可以剖析 SCORM 套件並將提取的內容儲存在 Supabase 資料庫中。像 pipwerks-scorm-api-wrapper 這樣的庫處理執行時 API。這是可行的,但增加了重大複雜性。除非客戶特別要求,否則我會避免它。

你如何為付費課程處理付款? Stripe Checkout 是最簡單的方式。從伺服器操作建立 Stripe Checkout 會話,將使用者重定向到 Stripe,並在 Next.js API 路由或 Supabase 邊緣函式中處理 checkout.session.completed webhook。webhook 建立註冊記錄。這保持你的支付邏輯簡單且 PCI 合規,而無需自己處理信用卡資料。

此架構也能支援行動應用程式嗎? 這是採用無頭方法的最大優勢之一。你的 Supabase 資料庫和驗證透過 @supabase/supabase-js 從 React Native 應用程式的工作方式相同。相同的 RLS 策略保護資料。你只是建立不同的 UI 層。我們已經看到團隊從同一 Supabase 後端推出網路和行動版本,無需任何 API 更改。

你如何添加測驗生成或內容總結等 AI 功能? 將你的課程內容儲存為結構化 JSONB(而不是 HTML blob)。然後建立一個 Supabase 邊緣函式,將課程內容發送到 LLM(OpenAI、Anthropic 或自託管模型)以生成測驗問題、摘要或學習指南。將生成的內容儲存回 JSONB 欄位。我們早期設計的結構化資料模型使這變得簡單——AI 系統可以在沒有標記解析的情況下使用特定欄位。如果你有興趣這種類型的工作,請與我們的團隊聯絡——我們全年一直在將 AI 整合構建到內容平台中。