使用 Supabase 和 Next.js 在 2026 年建構無頭式 LMS
在過去兩年中,我建立了三個學習管理系統平台。其中兩個使用了現成的解決方案,我們最終都把它們移除了。第三個——實際上運作良好的那個——是一個基於 Next.js 和 Supabase 的無頭架構。這就是我要帶你一起建立的。
無頭學習管理系統的宣傳很簡單:商業平台如 Teachable、Thinkific,甚至 Moodle 都提供了一種單一的體驗,其中內容模型、前端、驗證系統和支付流程全部焊接在一起。一旦你需要自訂功能——基於角色的內容控制、AI 驅動的測驗生成、白標多租戶部署——你就會被平台所困,而不是在開發功能。
透過 Supabase 處理你的資料庫、驗證、儲存和實時訂閱,以及 Next.js 處理你的渲染和 API 層,你可以獲得一個真正屬於你自己的系統。內容端沒有供應商鎖定。對學習者體驗擁有完全控制。以及一個可以直接查詢的 Postgres 資料庫,無需從伺服器元件進行 HTTP 往返。
讓我們來建立它。
目錄
- 為什麼為學習管理系統選擇無頭架構
- 架構概述
- 資料庫架構設計
- 設定 Supabase 驗證與基於角色的存取
- 行級安全性用於多租戶內容
- 建立 Next.js App Router 前端
- 課程內容渲染和進度跟蹤
- 實時功能:討論和通知
- Supabase 儲存的影片和檔案儲存
- 部署和效能考量
- 常見問題

為什麼為學習管理系統選擇無頭架構
傳統的學習管理系統平台並非為現代網路而建立。它們是為想要簽署合規性複選框的 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。 拖放重新排序對任何課程建立者都是必須的。整數排序使這變得微不足道。 - 課程上的
metadataJSONB。 這是你的逃脫艙。SEO 欄位、自訂品牌、功能旗標——把所有東西都扔進去,而無需架構遷移。

設定 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 策略是累加的。如果任何策略通過,該行是可見的。對於 INSERT、UPDATE 和 DELETE,所有適用的策略都必須通過。在除錯存取問題時請記住這一點。
建立 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 整合構建到內容平台中。