场景代入:你前端发一个 supabase.from('articles').select(),背后到底发生了什么?请求怎么从浏览器穿过网关、落到 Postgres、又怎么被 RLS 拦一道?这一节把整条链路拆开,让你建立"请求从哪进、权限在哪判、数据从哪出"的空间感。这是理解后面所有组件的前提。
Supabase 的运行时可以切成四层,从外到内:
/rest 去 PostgREST,/auth 去 Auth 服务,/realtime 去 Realtime,/storage 去 Storage。下面 SVG 是这条链路的竖向剖切,注意 Kong 是统一入口,所有服务共享底部数据库:

我们拿"已登录用户查自己的资料"举例,把每层发生了什么写清楚。这是理解权限模型的关键——RLS 不是在应用里判的,是在 Postgres 里判的。
-- 这张表的策略我们前面建过:select 时 auth.uid() = id -- 当客户端带着 JWT 调用 GET /rest/v1/profiles?select=username -- PostgREST 把请求翻译成等价于下面的 SQL(省略连接细节): select username from public.profiles where auth.uid() = id; -- auth.uid() 由网关注入的 JWT 解析而来
旅程逐跳:
Authorization: Bearer <JWT> 发请求到 Kong。/rest,转给 PostgREST。sub(即用户 uuid),作为 auth.uid() 注入查询。auth.uid() = id 自动附加到 where,非本人行被过滤。关键点:即使有人直连 PostgREST 改了查询参数,RLS 仍在数据库层兜底,应用层漏写校验也不会泄数据。
几个工程动机:
用 CLI 看本地网关路由规则,能直观确认这种分流:
# 本地起服务后,查看 Kong 的配置(路由列表) supabase status # Storage: http://127.0.0.1:54321/storage/v1/
每个路径前缀对应一个后端服务,这就是"网关分流"的落地形态。
背景:某前端工程师图省事,在前端直接拼了 GET /rest/v1/orders?select=*,以为"反正只给当前用户看界面"。
操作:攻击者拿到匿名 JWT(或别人的 token),直接调这个接口。
过程与结果:
auth.uid() 为 null,RLS 策略 auth.uid() = user_id 不满足,返回空结果集,HTTP 200 但无数据。sub 过滤,只能看到该用户自己的订单,看不到别人的。解读:这个"自动兜底"来自架构设计——权限判点在数据层,而不依赖前端是否"善良"。如果当初把权限写在应用代码里,前端绕过就裸奔了。
变式:若你故意想让某些字段公开(比如订单状态给物流方查),就单独写一条 for select using (true) 的策略只暴露状态列,配合列级权限,而不是放开整表。
提醒:别以为"前端不显示"就等于"用户看不到"。任何经 PostgREST 的接口都能被直接调用,RLS 才是真正防线。前端隐藏字段只是体验,不是安全。
建议:我们建议把 Kong 的基地址和路径前缀记牢,排错时先看请求打到了哪个服务(看路径前缀),能省很多绕路时间。
很多新人以为"本地用 Docker、云端用 Supabase 云"是两套东西,其实它们的组件镜像完全一致——差别只在你有没有把 Kong 暴露到公网。下面这张表把四层在两种环境下的落点对齐,你能直观看到"本地能跑通,上云只是换了 IP":
| 层 | 本地(Docker) | 云端(Supabase 托管) | 关键差异 |
|---|---|---|---|
| 客户端层 | 你的前端 / 测试脚本 | 同左 | 无 |
| 网关层 | 本地 Kong(54321) | 云端 Kong(项目域名) | 仅域名不同 |
| 服务层 | 本地 PostgREST / Auth / Realtime | 云端同组件 | 镜像同版本 |
| 数据层 | 本地 Postgres + PgBouncer | 云端 Postgres + PgBouncer | 备份/扩缩由云端管 |
💡 关键直觉:所谓"环境不一致导致上线炸",在 Supabase 里几乎不会发生,因为组件同源。真正会出岔子的只有两处——一是云端开了而你本地没开的扩展(如 pg_cron),二是云端 RLS 策略因迁移没推全而缺失。所以上云前用 db push 把迁移推齐,比纠结"环境"重要得多。
背景:你担心"anon key 是公开的,别人拿去是不是能乱查"。我们用一次探测把这个边界划清楚。
操作:拿匿名 JWT 对本地的 profiles 表做几种动作(假设该表已开 RLS 且只有本人可读写):
-- 在 SQL Editor 用管理员上下文(service_role 绕过 RLS)看匿名实际能返回什么 -- 模拟:匿名对 profiles 做 select select count(*) from public.profiles; -- 管理员看到真实总数,比如 142
# 用匿名身份真实调一次接口,看返回 curl -H "apikey: <anon>" "http://127.0.0.1:54321/rest/v1/profiles?select=id" # 返回 [] 或 401/空,因为 RLS 把匿名挡在外面
结果:匿名 key 虽然公开,但在 RLS 收口下什么都捞不到——这就是"anon 可公开、权限靠 RLS"的设计底气。攻击者即便拿全库结构,也看不到一行受保护数据。
解读:很多团队因为"key 公开"就不敢用 BaaS,其实真正兜底的不是 key 保密,而是 RLS 这道数据库层的闸门。把这道闸门写对,anon key 贴满前端都无妨;闸门没写,service_role 再藏也救不了。
⚠️ 常见坑:有人本地测试时为了"方便"临时把表策略改成 using (true),上线忘了改回去。因为本地和云端结构要手动 push,这种"本地松、云端也松"的失误很隐蔽。我们建议本地也默认收紧,别给自己留"临时放开"的坏习惯。
下一节我们逐个拆开核心组件,看 Auth、Database、Storage、Realtime、Functions 各自的能力边界。