第二章:Supabase——全功能 BaaS 平台
第二章:Supabase——全功能 BaaS 平台
Supabase 是"开源的 Firebase 替代品"——但这个定位低估了它。它是一个完整的后端平台:PostgreSQL 数据库 + 认证 + 文件存储 + 实时订阅 + Edge Functions。对于 SaaS 开发者,Supabase 可以取代 90% 的后端工作。
一、Supabase 项目初始化
# 安装 Supabase 客户端
npm install @supabase/supabase-js @supabase/ssr
# 创建 .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci... # 服务端专用,永不暴露给客户端!
// lib/supabase/client.ts - 浏览器端(用于 Client Components)
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/types/database'
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// lib/supabase/server.ts - 服务器端(用于 Server Components + Server Actions)
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import type { Database } from '@/types/database'
export function createClient() {
const cookieStore = cookies()
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: any) {
try {
cookieStore.set({ name, value, ...options })
} catch (error) {
// Server Component 中无法 set cookie,忽略
}
},
remove(name: string, options: any) {
try {
cookieStore.set({ name, value: '', ...options })
} catch (error) {}
},
},
}
)
}
二、数据库 Schema 设计——SaaS 基础表
-- 基础 Schema(在 Supabase SQL Editor 中执行)
-- 扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "vector"; -- 向量搜索(AI 功能用)
-- 用户配置(扩展 Supabase Auth 的 users 表)
CREATE TABLE profiles (
id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
email TEXT NOT NULL,
full_name TEXT,
avatar_url TEXT,
stripe_customer_id TEXT UNIQUE, -- Stripe 客户 ID
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 订阅
CREATE TABLE subscriptions (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
stripe_subscription_id TEXT UNIQUE NOT NULL,
stripe_price_id TEXT NOT NULL,
status TEXT NOT NULL, -- active, canceled, past_due, trialing
current_period_end TIMESTAMPTZ NOT NULL,
cancel_at_period_end BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 组织(多租户)
CREATE TABLE organizations (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL, -- URL 友好的唯一标识
owner_id UUID REFERENCES profiles(id) NOT NULL,
plan TEXT DEFAULT 'free', -- free, pro, enterprise
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 组织成员
CREATE TABLE organization_members (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
role TEXT DEFAULT 'member', -- owner, admin, member
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(org_id, user_id)
);
-- 自动创建 Profile(Auth 触发器)
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO 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 handle_new_user();
三、Row Level Security(RLS)——数据权限核心
RLS 是 Supabase/PostgreSQL 的杀手级功能:在数据库层面控制哪个用户能看到/修改哪些数据。
-- 启用 RLS(必须!否则所有人能看到所有数据)
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
-- profiles 策略:只能看到/修改自己的数据
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id); -- auth.uid() 是当前登录用户的 ID
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
-- organizations 策略:组织成员才能看到
CREATE POLICY "Org members can view"
ON organizations FOR SELECT
USING (
id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
)
);
-- 只有 owner 可以删除组织
CREATE POLICY "Only owner can delete org"
ON organizations FOR DELETE
USING (owner_id = auth.uid());
-- organization_members:只能看到同组织的成员
CREATE POLICY "Members can view org members"
ON organization_members FOR SELECT
USING (
org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
)
);
// RLS 的实际效果:
// 即使你写了 supabase.from('profiles').select('*')
// 也只会返回当前用户自己的 Profile!
// 数据库层面的安全,不依赖应用层逻辑
async function getUserProfile() {
const supabase = createClient() // 服务端
// 自动过滤:只返回当前用户的数据(RLS 生效)
const { data, error } = await supabase
.from('profiles')
.select('*')
.single()
return data
}
四、Supabase Auth——认证系统
// lib/auth.ts - 常用认证操作
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
// 获取当前用户(Server Component 中用)
export async function getCurrentUser() {
const supabase = createClient()
const { data: { user }, error } = await supabase.auth.getUser()
if (error || !user) return null
return user
}
// 要求登录(未登录跳转到 /login)
export async function requireAuth() {
const user = await getCurrentUser()
if (!user) redirect('/login')
return user
}
// app/(auth)/login/page.tsx
"use client"
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
export default function LoginPage() {
const supabase = createClient()
const router = useRouter()
// 邮箱密码登录
const handleLogin = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const { error } = await supabase.auth.signInWithPassword({
email: formData.get('email') as string,
password: formData.get('password') as string,
})
if (error) {
alert(error.message)
return
}
router.push('/dashboard')
}
// Google OAuth 登录
const handleGoogleLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
}
})
}
// Magic Link(无密码)
const handleMagicLink = async (email: string) => {
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
}
})
if (!error) alert('检查你的邮箱!')
}
return (
<form onSubmit={handleLogin}>
<input name="email" type="email" placeholder="邮箱" required />
<input name="password" type="password" placeholder="密码" required />
<button type="submit">登录</button>
<button type="button" onClick={handleGoogleLogin}>Google 登录</button>
</form>
)
}
// app/auth/callback/route.ts - OAuth 回调处理
import { createClient } from '@/lib/supabase/server'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
if (code) {
const supabase = createClient()
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(new URL('/dashboard', request.url))
}
五、Supabase Storage——文件上传
// 上传用户头像
async function uploadAvatar(file: File, userId: string) {
const supabase = createClient()
const fileExt = file.name.split('.').pop()
const fileName = `${userId}/avatar.${fileExt}`
const { data, error } = await supabase.storage
.from('avatars') // bucket 名称
.upload(fileName, file, {
cacheControl: '3600',
upsert: true, // 覆盖已有文件
})
if (error) throw error
// 获取公开访问 URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(fileName)
// 更新用户 Profile
await supabase
.from('profiles')
.update({ avatar_url: publicUrl })
.eq('id', userId)
return publicUrl
}
六、实时订阅——Realtime 功能
// 监听实时数据变化(用于通知、协作功能)
"use client"
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
export function useRealtimeNotifications(userId: string) {
const [notifications, setNotifications] = useState<any[]>([])
const supabase = createClient()
useEffect(() => {
// 订阅当前用户的通知
const channel = supabase
.channel('notifications')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'notifications',
filter: `user_id=eq.${userId}`,
},
(payload) => {
setNotifications(prev => [payload.new, ...prev])
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [userId])
return notifications
}
七、自动生成 TypeScript 类型
# 从 Supabase 项目生成 TypeScript 类型(类型安全!)
npx supabase gen types typescript \
--project-id your-project-id \
> types/database.ts
# 之后所有查询都有完整类型提示
// types/database.ts(自动生成,不要手动修改)
export type Database = {
public: {
Tables: {
profiles: {
Row: { // SELECT 返回类型
id: string
email: string
full_name: string | null
avatar_url: string | null
stripe_customer_id: string | null
created_at: string
}
Insert: { // INSERT 类型
id: string
email: string
full_name?: string | null
// ...
}
Update: { // UPDATE 类型
email?: string
full_name?: string | null
// ...
}
}
// ...其他表
}
}
}
关键认知
Supabase vs 自建后端的取舍:
| 功能 | Supabase | 自建 |
|---|---|---|
| 认证系统 | 开箱即用,0 代码 | 1-2 周实现 |
| 数据权限 | RLS(声明式) | 中间件/服务层 |
| 实时功能 | 一行代码订阅 | WebSocket 服务 |
| 文件存储 | 5 分钟配置 | S3 + 签名 URL |
| 数据库迁移 | Supabase CLI | 手动脚本 |
| 可控性 | 较低(依赖平台) | 完全可控 |
| 月费 | Free: $0, Pro: $25 | EC2 $30+ |
RLS 是最重要的安全实践:没有 RLS,任何 API 泄露都可能导致全量数据泄露。有了 RLS,即使代码有 bug(如忘记过滤 user_id),数据库也会自动拒绝越权访问。
“Supabase 让数据库权限控制从’应用层的约定’变成了’数据库层的强制’。这是架构层面的安全提升,不是功能层面的便利。”