上一节我们把链路打通了,这一节把五个核心组件单独拎出来看:Auth、Database、Storage、Realtime、Edge Functions。每个都讲它解决什么、怎么用、以及边界在哪——也就是"该用别用"的判断。理解组件边界,才能避免把所有逻辑都塞进数据库或都塞进函数。
Auth 服务(源自 GoTrue)负责注册、登录、签发 JWT。登录成功后返回的 JWT 里带 sub(用户 uuid)、role 等声明,后续所有请求靠它做 RLS 判断。
import { createClient } from '@supabase/supabase-js' const supabase = createClient(URL, ANON_KEY) // 邮箱密码注册 const { data, error } = await supabase.auth.signUp({ email: 'dev@example.com', password: 'a-strong-password', }) // data.user.id 就是后续 RLS 里的 auth.uid() // 登录后拿 session const { data: session } = await supabase.auth.signInWithPassword({ email: 'dev@example.com', password: 'a-strong-password', }) // session.access_token 即为 Bearer token,supabase-js 自动附带
边界:Auth 管"你是谁",不管"你能看哪些行"——那是 RLS 的事。常见误区是把权限逻辑写在 Auth 的 metadata 里,结果是应用层到处读 metadata 判断,既慢又易漏。正确做法:身份交给 JWT,权限交给 RLS。
Database 就是标准 Postgres,Supabase 在它之上加了:自动 REST/GraphQL API(PostgREST)、RLS、触发器、扩展(pgvector、PostGIS 等)。你写的每一条 SQL 策略,同时约束了 API 和未来的函数调用。
-- 开启一个扩展,比如向量检索 create extension if not exists vector; -- 建表时直接享受 JSONB、数组、范围类型 create table public.docs ( id bigint generated always as identity primary key, title text, embedding vector(1536), tags text[] );
边界:Database 强在事务与关系,但不适合做长时间运行的计算任务(比如转码视频)。那种活交给 Edge Functions 或外部服务。
Storage 存文件(图片、视频、文档),元数据在 Postgres,对象本身在 S3 兼容层。权限同样由 RLS 通过策略表控制。下面的代码上传一张头像并限定只有本人能覆盖:
-- 权限策略:用户只能写自己 userId 命名的目录 create policy "头像归属" on storage.objects for all using (auth.uid()::text = (storage.foldername(name))[1]) with check (auth.uid()::text = (storage.foldername(name))[1]);
// 前端上传,路径以用户 id 开头 await supabase.storage .from('avatars') .upload(`${userId}/me.png`, file, { upsert: true }) // 上传成功后可通过 getPublicUrl 拿访问地址
边界:Storage 不适合当数据库用。有人把结构化数据存成 JSON 文件再读,既慢又难查询。该建表建表。
Realtime 订阅 Postgres 的 WAL,把 INSERT/UPDATE/DELETE 推到 WebSocket 客户端。支持表监听、行级过滤、broadcast(自定义事件)、presence(在线状态)。
const channel = supabase .channel('room:42') .on('broadcast', { event: 'cursor' }, (e) => { console.log('别人光标位置', e.payload) }) .subscribe() // 自己移动光标时广播 await supabase.channel('room:42').send({ type: 'broadcast', event: 'cursor', payload: { x: 120, y: 80 }, })
边界:Realtime 解决"数据变了通知你",不解决"历史消息存储"。聊天记录还得落库(见 3.4 节),Realtime 只管推送增量。
Edge Functions 是基于 Deno 的 TypeScript 运行时,跑在离用户近的边缘节点,适合无状态、短耗时的逻辑(webhook 处理、第三方 API 代理、轻计算)。它能用服务角色密钥直连数据库,绕过 RLS 做管理员操作——所以要小心。
// supabase/functions/hello/index.ts import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' Deno.serve(async (req) => { const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // 注意:绕过 RLS ) const { data } = await supabase.from('articles').select('count') return new Response(JSON.stringify(data)) })
部署:
supabase functions deploy hello
边界:函数无状态、有冷启动、执行有时间上限,不适合跑批处理或长事务。重计算放到专用 worker。
下面 SVG 把这五个组件的能力边界和"不该做什么"画成一张对照矩阵:

背景:做一个带实时刷新的评论区,要求新评论立刻出现在所有看客屏幕上。
操作与排布:
comments 表,RLS 让登录用户可插、所有人可读(前面 1.1 写过)。on('postgres_changes', {event:'INSERT', table:'comments'}) 监听,新评论推送即渲染。结果:新评论由某人写入表 → Realtime 推给所有订阅者 → 各自前端追加 DOM。全程无后端服务。
解读:组件各司其职,没有"为了用而用"。很多新手会硬塞一个 Edge Function 去做"写入后通知",其实 Realtime 已经把这件事包了。
变式:若评论要过敏感词过滤再上屏,那就在插入前加一个 Edge Function 做校验,再决定写不写表——这时函数才该出场。
⚠️ Edge Functions 用 service_role 密钥会绕过 RLS,等于数据库管理员权限。千万别在前端暴露这个密钥,也别在函数里信任客户端传来的"我是管理员"字段。
💡 我们给的组件选择口诀:要存关系数据→Database;要传文件→Storage;要即时通知→Realtime;要无状态小逻辑→Edge Func;要登录→Auth。按这个顺序对号入座,基本不会错配。
下一节看 Dashboard 怎么把这些组件可视化,以及它在日常开发里的真实用处。