Supabase 全栈开发实战:Auth、数据库、Edge Functions 与实时订阅完整指南

深入实战 Supabase 开源 Firebase 替代方案,涵盖 PostgreSQL 数据库、Row Level Security、Auth 认证、Edge Functions、实时订阅与对象存储,附完整 Next.js 代码示例与生产部署避坑指南。

开发者效率 2026-05-29 18 分钟

根据 Supabase 官方 2026 年 Q1 数据,其注册开发者已突破 200 万,GitHub Star 超过 75K,月活跃项目数同比增长 180%。与 Firebase 的黑盒架构不同,Supabase 基于开源的 PostgreSQL 构建,意味着你拥有完整的数据所有权、可本地开发、可自行部署。如果你正在寻找一个既能快速原型验证、又能支撑生产级应用的全栈平台,Supabase 是 2026 年最值得关注的选择。

Supabase 不是一个单一产品,而是一组开源工具的组合。它的核心架构由五个模块组成:Database(PostgreSQL + PostgREST 自动 REST API)、Auth(基于 GoTrue 的认证服务)、Storage(基于 S3 协议的文件存储)、Edge Functions(基于 Deno 的服务端函数)、Realtime(基于 PostgreSQL WAL 的实时推送)。这些模块各自独立运行,通过 Kong 网关统一暴露 API,形成了一个完整的后端即服务平台。

与传统的「先搭后端、再接数据库、再加认证」的开发流程相比,Supabase 让你可以在 5 分钟内拥有一个带认证、数据库、文件存储和实时功能的完整后端。这不是偷懒,而是把精力集中在业务逻辑上,而不是基础设施搭建上

🏗️ 一、Supabase 架构核心与本地开发环境

1.1 为什么选 Supabase 而非 Firebase?

很多开发者在选择 BaaS(Backend as a Service)时会在 Firebase 和 Supabase 之间纠结。两者的核心差异不在于功能覆盖度,而在于架构哲学

维度 Supabase Firebase
底层数据库 PostgreSQL(关系型) Firestore(文档型)
数据所有权 ✅ 完全控制,可自托管 ❌ 数据锁死在 Google
SQL 查询 ✅ 原生 SQL + PostgREST ❌ 专有查询语法
实时订阅 ✅ 基于 PostgreSQL WAL ✅ 原生支持
本地开发 ✅ Docker 完整模拟 ⚠️ 模拟器功能有限
Edge Functions ✅ Deno 运行时 ✅ Cloud Functions
免费额度 500MB 数据库 + 1GB 存储 1GB Firestore + 5GB 存储
开源 ✅ Apache 2.0 ❌ 闭源

⚡ **关键结论:**如果你的项目需要复杂查询(JOIN、聚合、全文搜索)、数据可移植性、或已有的 SQL 经验,Supabase 是更自然的选择。如果你需要极简的 key-value 存储和最快速的原型,Firebase 仍有优势。

1.2 本地开发环境搭建

Supabase 的最大优势之一是完整的本地开发支持。整个平台通过 Docker 运行,你可以在断网环境下开发。

# 安装 Supabase CLI
npm install -g supabase

# 在项目根目录初始化
cd my-project
supabase init

# 启动本地 Supabase(需要 Docker)
supabase start

启动后你会看到类似输出:

         API URL: http://127.0.0.1:54321
          DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
      Studio URL: http://127.0.0.1:54323
    Inbucket URL: http://127.0.0.1:54324
      JWT secret: super-secret-jwt-token-with-at-least-32-characters
        anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

💡 提示:anon key 是前端使用的公开密钥,配合 RLS(Row Level Security)保证安全。service_role key 是后端管理员密钥,永远不要暴露到前端代码中

1.3 数据库迁移工作流

Supabase 使用迁移文件管理数据库 Schema 变更,这比直接在控制台操作要可靠得多。

# 创建新的迁移文件
supabase migration new create_users_table

# 编辑 supabase/migrations/xxxx_create_users_table.sql
-- 创建用户资料表
CREATE TABLE public.profiles (
  id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
  username TEXT UNIQUE NOT NULL,
  display_name TEXT,
  avatar_url TEXT,
  bio TEXT DEFAULT '',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- 自动在用户注册时创建 profile
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.profiles (id, username)
  VALUES (NEW.id, NEW.raw_user_meta_data ->> 'username');
  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();

-- 启用 RLS
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
# 应用迁移到本地数据库
supabase db reset

# 推送到远程 Supabase 项目
supabase db push

📌 **记住:**永远使用迁移文件管理 Schema,而不是直接在 Dashboard 操作。迁移文件可以版本控制、可以回滚、可以在多个环境(开发/测试/生产)一致地应用。

🔐 二、Row Level Security:数据库层面的权限控制

2.1 RLS 的核心理念

Row Level Security(RLS)是 Supabase 安全模型的基石。它的核心思想是:每一行数据都有自己的访问规则,直接在数据库层面执行,绕过应用层代码

这意味着即使有人拿到了你的 anon key,也无法通过 API 访问到不属于他的数据——因为 RLS 策略会在 PostgreSQL 层面拦截非法查询。

-- 策略 1:所有人可以查看公开的 profile
CREATE POLICY "Profiles are viewable by everyone"
  ON public.profiles
  FOR SELECT
  USING (true);

-- 策略 2:用户只能更新自己的 profile
CREATE POLICY "Users can update own profile"
  ON public.profiles
  FOR UPDATE
  USING (auth.uid() = id)
  WITH CHECK (auth.uid() = id);

-- 策略 3:用户只能删除自己的 profile
CREATE POLICY "Users can delete own profile"
  ON public.profiles
  FOR DELETE
  USING (auth.uid() = id);

2.2 复杂 RLS 策略实战

实际项目中的 RLS 策略往往比简单的 auth.uid() = id 复杂得多。以下是一个多角色、多租户的博客系统示例:

-- 文章表
CREATE TABLE public.posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  author_id UUID REFERENCES public.profiles(id) NOT NULL,
  org_id UUID REFERENCES public.organizations(id) NOT NULL,
  title TEXT NOT NULL,
  content TEXT,
  status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published')),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;

-- 查看策略:已发布文章所有人可见,草稿和审核中只有作者和同组织成员可见
CREATE POLICY "Published posts are public"
  ON public.posts FOR SELECT
  USING (
    status = 'published'
    OR author_id = auth.uid()
    OR org_id IN (
      SELECT org_id FROM public.org_members
      WHERE user_id = auth.uid()
    )
  );

-- 创建策略:只有组织成员可以发布文章
CREATE POLICY "Org members can create posts"
  ON public.posts FOR INSERT
  WITH CHECK (
    org_id IN (
      SELECT org_id FROM public.org_members
      WHERE user_id = auth.uid()
      AND role IN ('author', 'admin')
    )
  );

-- 更新策略:作者可以编辑草稿,管理员可以编辑任何状态
CREATE POLICY "Authors can update own drafts"
  ON public.posts FOR UPDATE
  USING (
    (author_id = auth.uid() AND status = 'draft')
    OR org_id IN (
      SELECT org_id FROM public.org_members
      WHERE user_id = auth.uid() AND role = 'admin'
    )
  );

⚠️ **警告:**RLS 默认拒绝所有访问。如果你启用了 RLS 但忘记添加策略,所有查询都会返回空结果——这是新手最常踩的坑。调试时先用 SELECT * FROM pg_policies WHERE tablename = 'your_table' 检查策略是否正确。

2.3 RLS 性能优化

RLS 策略中的子查询会在每次查询时执行,如果不注意优化,会成为性能瓶颈。

-- ❌ 避免:每次查询都做全表扫描的 RLS 策略
CREATE POLICY "Bad policy" ON public.posts FOR SELECT
  USING (
    org_id IN (
      SELECT org_id FROM public.org_members  -- 每行都执行一次
      WHERE user_id = auth.uid()
    )
  );

-- ✅ 推荐:使用函数缓存 + 索引优化
CREATE OR REPLACE FUNCTION public.get_user_org_ids()
RETURNS SETOF UUID AS $$
  SELECT org_id FROM public.org_members WHERE user_id = auth.uid();
$$ LANGUAGE sql SECURITY DEFINER STABLE;

-- 确保有索引
CREATE INDEX idx_org_members_user_id ON public.org_members(user_id);

-- 使用函数的优化策略
CREATE POLICY "Optimized policy" ON public.posts FOR SELECT
  USING (org_id IN (SELECT public.get_user_org_ids()));

🚀 三、Auth 认证系统与 Next.js 集成

3.1 多种认证方式

Supabase Auth 支持邮箱密码、魔法链接、社交登录、手机验证码等多种方式。以下是在 Next.js 中的完整集成:

// lib/supabase/client.ts — 客户端 Supabase 实例
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}
// lib/supabase/server.ts — 服务端 Supabase 实例
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) {
          cookiesToSet.forEach(({ name, value, options }) => {
            cookieStore.set(name, value, options)
          })
        },
      },
    }
  )
}
// app/login/actions.ts — 登录 Server Action
'use server'

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

export async function login(formData: FormData) {
  const supabase = await createClient()

  const data = {
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  }

  const { error } = await supabase.auth.signInWithPassword(data)

  if (error) {
    redirect('/login?error=' + encodeURIComponent(error.message))
  }

  revalidatePath('/', 'layout')
  redirect('/dashboard')
}

export async function signup(formData: FormData) {
  const supabase = await createClient()

  const { error } = await supabase.auth.signUp({
    email: formData.get('email') as string,
    password: formData.get('password') as string,
    options: {
      data: {
        username: formData.get('username') as string,
      }
    }
  })

  if (error) {
    redirect('/signup?error=' + encodeURIComponent(error.message))
  }

  redirect('/signup?success=1')
}

3.2 社交登录(GitHub OAuth)

// app/login/actions.ts — 新增 GitHub 登录
export async function loginWithGitHub() {
  const supabase = await createClient()

  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'github',
    options: {
      redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
    },
  })

  if (data.url) {
    redirect(data.url)
  }
}
// app/auth/callback/route.ts — OAuth 回调处理
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')

  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      return NextResponse.redirect(`${origin}/dashboard`)
    }
  }

  return NextResponse.redirect(`${origin}/login?error=auth_failed`)
}

💡 **提示:**Supabase 的 OAuth 回调需要在 Dashboard → Authentication → URL Configuration 中配置 Redirect URLs。本地开发时需要添加 http://localhost:3000/auth/callback

📡 四、Edge Functions 与实时订阅

4.1 Edge Functions 实战

Supabase Edge Functions 基于 Deno 运行时,部署在全球边缘节点,适合处理 Webhook、定时任务、AI 调用等场景。

// supabase/functions/process-payment/index.ts
import { serve } from 'https://deno.land/std@0.208.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  // CORS 处理
  if (req.method === 'OPTIONS') {
    return new Response('ok', {
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'POST',
      },
    })
  }

  try {
    const { order_id } = await req.json()

    // 使用 service_role key 绕过 RLS
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL')!,
      Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
    )

    // 查询订单
    const { data: order, error } = await supabase
      .from('orders')
      .select('*, profiles(*)')
      .eq('id', order_id)
      .single()

    if (error || !order) {
      return new Response(
        JSON.stringify({ error: 'Order not found' }),
        { status: 404, headers: { 'Content-Type': 'application/json' } }
      )
    }

    // 模拟支付处理
    const paymentResult = {
      success: true,
      transaction_id: crypto.randomUUID(),
      amount: order.total_amount,
    }

    // 更新订单状态
    await supabase
      .from('orders')
      .update({
        status: 'paid',
        paid_at: new Date().toISOString(),
        transaction_id: paymentResult.transaction_id,
      })
      .eq('id', order_id)

    return new Response(
      JSON.stringify(paymentResult),
      { headers: { 'Content-Type': 'application/json' } }
    )
  } catch (err) {
    return new Response(
      JSON.stringify({ error: 'Internal server error' }),
      { status: 500, headers: { 'Content-Type': 'application/json' } }
    )
  }
})
# 部署 Edge Function
supabase functions deploy process-payment

# 设置环境变量
supabase secrets set STRIPE_SECRET_KEY=sk_test_xxx

4.2 实时订阅

Supabase 的实时功能基于 PostgreSQL 的 WAL(Write-Ahead Log),可以在数据变更时即时推送通知。

// 实时订阅订单状态变更
import { createClient } from '@/lib/supabase/client'

const supabase = createClient()

// 监听特定用户的订单变更
const channel = supabase
  .channel('order-updates')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'orders',
      filter: `user_id=eq.${userId}`,
    },
    (payload) => {
      console.log('订单状态变更:', payload.new)
      // payload.new 包含更新后的完整行数据
      if (payload.new.status === 'paid') {
        showSuccessNotification(payload.new)
      }
    }
  )
  .subscribe()

// 组件卸载时取消订阅
// supabase.removeChannel(channel)
// 实时广播:协作编辑场景
const channel = supabase
  .channel('document-collab')
  .on('broadcast', { event: 'cursor-move' }, (payload) => {
    // 收到其他用户的光标位置
    updateRemoteCursor(payload.payload)
  })
  .on('presence', { event: 'sync' }, () => {
    // 获取当前在线用户列表
    const state = channel.presenceState()
    updateOnlineUsers(state)
  })
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      // 注册自己的在线状态
      await channel.track({
        user_id: currentUser.id,
        username: currentUser.name,
        online_at: new Date().toISOString(),
      })
    }
  })

⚠️ **警告:**实时订阅会占用 Supabase 的连接数。免费计划限制 200 个并发连接,Pro 计划 500 个。务必在组件卸载时调用 removeChannel(),否则会导致连接泄漏。

📊 五、存储与 AI 集成

5.1 对象存储

// 上传用户头像
async function uploadAvatar(file: File): Promise<string> {
  const supabase = createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) throw new Error('Not authenticated')

  const filePath = `${user.id}/${crypto.randomUUID()}.${file.name.split('.').pop()}`

  const { error } = await supabase.storage
    .from('avatars')
    .upload(filePath, file, {
      cacheControl: '3600',
      upsert: false,
    })

  if (error) throw error

  // 获取公开 URL
  const { data: { publicUrl } } = supabase.storage
    .from('avatars')
    .getPublicUrl(filePath)

  // 更新 profile
  await supabase
    .from('profiles')
    .update({ avatar_url: publicUrl })
    .eq('id', user.id)

  return publicUrl
}

5.2 存储桶安全策略

-- 存储桶的 RLS 策略
-- 只有头像拥有者可以删除
CREATE POLICY "Avatar owners can delete"
  ON storage.objects
  FOR DELETE
  USING (
    bucket_id = 'avatars'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

-- 头像公开可读
CREATE POLICY "Avatars are publicly accessible"
  ON storage.objects
  FOR SELECT
  USING (bucket_id = 'avatars');

5.3 pgvector 向量搜索集成

Supabase 内置了 pgvector 扩展,可以直接在 PostgreSQL 中进行向量相似性搜索,无需额外的向量数据库。

-- 启用 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;

-- 创建文档嵌入表
CREATE TABLE public.documents (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  content TEXT NOT NULL,
  metadata JSONB DEFAULT '{}',
  embedding VECTOR(1536),  -- OpenAI text-embedding-3-small 维度
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 创建 HNSW 索引(比 IVFFlat 更适合小到中型数据集)
CREATE INDEX ON public.documents
  USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 200);

-- 相似性搜索函数
CREATE OR REPLACE FUNCTION match_documents(
  query_embedding VECTOR(1536),
  match_count INT DEFAULT 10,
  filter JSONB DEFAULT '{}'
)
RETURNS TABLE (
  id UUID,
  content TEXT,
  metadata JSONB,
  similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
  RETURN QUERY
  SELECT
    d.id,
    d.content,
    d.metadata,
    1 - (d.embedding <=> query_embedding) AS similarity
  FROM public.documents d
  WHERE d.metadata @> filter
  ORDER BY d.embedding <=> query_embedding
  LIMIT match_count;
END;
$$;
// 在 Next.js 中调用向量搜索
import OpenAI from 'openai'
import { createClient } from '@/lib/supabase/server'

const openai = new OpenAI()

async function searchDocuments(query: string, filter = {}) {
  const supabase = await createClient()

  // 生成查询向量
  const embedding = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: query,
  })

  // 搜索相似文档
  const { data, error } = await supabase
    .rpc('match_documents', {
      query_embedding: embedding.data[0].embedding,
      match_count: 5,
      filter: filter,
    })

  return data
}

💡 **提示:**Supabase 的 pgvector 与 RLS 完全兼容。你可以为不同用户设置不同的文档访问权限,向量搜索会自动过滤无权访问的数据。

⚠️ 六、生产环境避坑指南

在实际项目中使用 Supabase,以下几个坑点需要特别注意:

6.1 连接池管理

Supabase 的直接连接(Port 5432)每个请求创建一个 PostgreSQL 连接,在 Serverless 环境中会迅速耗尽连接数。

// ❌ 避免:Serverless 函数使用直接连接
const supabase = createClient(
  'https://xxx.supabase.co',
  'key',
  { db: { host: 'db.xxx.supabase.co', port: 5432 } }  // 直接连接
)

// ✅ 推荐:使用 Supavisor 连接池(Port 6543)
const supabase = createClient(
  'https://xxx.supabase.co',
  'key',
  {
    db: {
      host: 'aws-0-ap-northeast-1.pooler.supabase.com',
      port: 6543,
    }
  }
)

6.2 N+1 查询陷阱

Supabase 的 select() 嵌套查询很方便,但不注意会产生 N+1 问题。

// ❌ 避免:循环中单独查询
for (const post of posts) {
  const { data: author } = await supabase
    .from('profiles')
    .select('username, avatar_url')
    .eq('id', post.author_id)
    .single()
  post.author = author
}

// ✅ 推荐:使用嵌套 select 一次查询
const { data: posts } = await supabase
  .from('posts')
  .select(`
    *,
    author:profiles(username, avatar_url),
    comments(count)
  `)
  .eq('status', 'published')
  .order('created_at', { ascending: false })
  .limit(20)

6.3 RLS 调试技巧

-- 检查当前用户的 JWT claims
SELECT auth.uid(), auth.role(), auth.jwt();

-- 测试 RLS 策略(以特定用户身份执行)
SET request.jwt.claims = '{"sub": "user-uuid-here", "role": "authenticated"}';
SET role = 'authenticated';
SELECT * FROM public.posts;  -- 查看该用户能看到什么
RESET role;

6.4 TypeScript 类型自动生成

Supabase CLI 可以从数据库 Schema 自动生成 TypeScript 类型定义,这意味着你的前端代码和数据库结构永远保持同步。

# 从远程数据库生成类型
supabase gen types typescript --project-id your-project-id > lib/database.types.ts

# 从本地数据库生成(开发时使用)
supabase gen types typescript --local > lib/database.types.ts

生成的类型文件可以直接传递给 Supabase 客户端,获得完整的类型提示:

// lib/supabase/client.ts — 带类型的 Supabase 客户端
import { createBrowserClient } from '@supabase/ssr'
import { Database } from '@/lib/database.types'

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

// 使用时获得完整的类型推断
const supabase = createClient()
const { data } = await supabase
  .from('posts')  // IDE 自动补全表名
  .select('title, status, author:profiles(username)')  // 自动补全字段名

// data 的类型自动推断为:
// { title: string; status: "draft" | "review" | "published"; author: { username: string } }[]

📌 **记住:**每次运行 supabase db push 或修改 Schema 后,都应该重新生成类型文件。可以在 package.json 中添加 "db:types": "supabase gen types typescript --local > lib/database.types.ts" 作为快捷命令。

6.5 数据库函数(RPC)调用

对于复杂的业务逻辑,直接在 PostgreSQL 中编写函数比在应用层多次查询更高效。

-- 创建一个带权限检查的原子操作函数
CREATE OR REPLACE FUNCTION public.transfer_credits(
  from_user UUID,
  to_user UUID,
  amount INT
)
RETURNS JSONB AS $$
DECLARE
  from_balance INT;
  result JSONB;
BEGIN
  -- 检查发送方余额(带行锁)
  SELECT credits INTO from_balance
  FROM public.profiles
  WHERE id = from_user
  FOR UPDATE;

  IF from_balance < amount THEN
    RETURN jsonb_build_object('success', false, 'error', '余额不足');
  END IF;

  -- 原子转账
  UPDATE public.profiles SET credits = credits - amount WHERE id = from_user;
  UPDATE public.profiles SET credits = credits + amount WHERE id = to_user;

  -- 记录交易
  INSERT INTO public.credit_transactions (from_user, to_user, amount)
  VALUES (from_user, to_user, amount);

  RETURN jsonb_build_object('success', true, 'new_balance', from_balance - amount);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
// 在前端调用 RPC 函数
const { data, error } = await supabase
  .rpc('transfer_credits', {
    from_user: currentUser.id,
    to_user: targetUserId,
    amount: 100,
  })

if (data.success) {
  console.log('转账成功,新余额:', data.new_balance)
} else {
  console.error('转账失败:', data.error)
}

⚠️ **警告:**使用 SECURITY DEFINER 的函数会以函数创建者的权限执行,绕过 RLS。务必在函数内部手动实现权限检查,否则可能造成安全漏洞。只在确实需要跨表原子操作时使用 SECURITY DEFINER

🎯 总结与最佳实践

Supabase 在 2026 年已经成为全栈开发的主流选择之一。它的核心优势在于:用开源的 PostgreSQL 打底,用 RLS 做安全,用 Edge Functions 做逻辑,用实时订阅做交互——形成了一个完整的全栈能力闭环。

使用 Supabase 的关键建议:

  • 从迁移文件开始:用 supabase migration 管理所有 Schema 变更,而不是直接在 Dashboard 操作
  • RLS 优先设计:在写应用代码之前先设计 RLS 策略,确保数据安全是数据库层面的
  • 善用嵌套查询:Supabase 的 select() 支持关系嵌套,可以一次查询获取关联数据
  • 使用连接池:在 Serverless 环境中必须使用 Supavisor(Port 6543),不要用直接连接
  • 自动生成类型:用 supabase gen types typescript 保持前后端类型同步
  • 善用 RPC 函数:复杂业务逻辑下沉到 PostgreSQL 函数,减少网络往返
  • 不要把 service_role key 暴露到前端:这个密钥可以绕过 RLS,泄露等于裸奔
  • 不要忽视实时连接数:每次订阅都占用一个连接,必须在卸载时清理
  • 不要在 RLS 策略中写复杂子查询:用 SECURITY DEFINER 函数封装,避免性能问题

相关工具和资源:

工具 说明 链接
Supabase CLI 本地开发与迁移管理 npm: supabase
Supabase Dashboard 在线管理控制台 app.supabase.com
@supabase/ssr Next.js/Nuxt SSR 集成 npm: @supabase/ssr
pgvector 向量相似性搜索 supabase.com/docs/guides/database/extensions/pgvector
Supabase Realtime 实时订阅文档 supabase.com/docs/guides/realtime
PostgREST 自动 REST API 引擎 postgrest.org
supabase-ui 官方 UI 组件库 github.com/supabase/ui

📚 相关文章