第二章: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 让数据库权限控制从’应用层的约定’变成了’数据库层的强制’。这是架构层面的安全提升,不是功能层面的便利。”