你在 Supabase Dashboard 的 Table Editor 里能看到表 posts 里有 100 条数据。但前端 await supabase.from('posts').select('*') 返回空数组 [],没有报错。或者更怪:登录的用户能看见自己的数据,但别人的看不见——明明你写的是 public posts。
这是 Supabase 的 RLS(Row Level Security)在拦你。RLS 默认行为是 enabled + 没 policy = deny all,不报错,只是返回 0 行。所以”silent empty result” 是 RLS 的典型症状。
常见原因
按命中率从高到低:
1. 启用了 RLS 但没写 SELECT policy
最高频。你点了 “Enable RLS” 但没加任何 policy → 任何 query 返回 0 行。Supabase Dashboard 会在表名旁显示警告,但很多人没注意。
如何判断:Dashboard → Table → 看表名旁是否有”RLS enabled, 0 policies”小红色警告。
2. policy 期望 auth.uid() 但请求是匿名
CREATE POLICY "users see own posts" ON posts
FOR SELECT USING (auth.uid() = author_id);
未登录用户调用 → auth.uid() 是 null → 永远 false → 看不到任何。
如何判断:登录用户能看到 / 未登录看不到。
3. 客户端用的 anon key 不是 service_role
前端用 SUPABASE_ANON_KEY 调用 → 受 RLS 限制。你可能误以为 supabase-js 自动 bypass。
如何判断:用 service_role key 在 SQL editor 里跑相同 query,如果有结果 → RLS 在拦。
4. 没区分 SELECT 和 INSERT/UPDATE 的 policy
-- 只加了 SELECT policy
CREATE POLICY "see own" ON posts FOR SELECT USING (...);
INSERT 还是 deny。或者反过来:能 INSERT 但 SELECT 自己刚插的也看不到(因为没 SELECT policy)。
如何判断:能写入但读不到自己刚写的。
5. policy 里 join 的字段不对
CREATE POLICY "team members see post" ON posts
USING (
EXISTS (
SELECT 1 FROM team_members
WHERE team_id = posts.team_id -- ❌ 应该是 user_id = auth.uid()
AND user_id = posts.author_id -- 逻辑错
)
);
逻辑错的 policy 永远 false。
如何判断:policy 看似合理但所有用户都看不到任何。
6. JWT 没传 / 过期
frontend 没把 session token 传到 Supabase client,或 token 过期了。auth.uid() 解不出来 = null。
如何判断:console.log(supabase.auth.getSession()) 看 session 是否存在且未过期。
最短修复路径
Step 1:确认数据真的存在(用 service_role 验证)
-- Supabase Dashboard → SQL Editor
-- SQL editor 默认用 postgres role,bypass RLS
SELECT count(*) FROM posts; -- 100
SELECT * FROM posts LIMIT 5; -- 应该有数据
如果这里能查到 → 确认是 RLS 拦的,不是数据问题。
Step 2:看表上的 policies
-- 列出 posts 表的所有 policies
SELECT * FROM pg_policies WHERE tablename = 'posts';
或 Dashboard → Database → Tables → posts → Policies。
Step 3:加 SELECT policy
最简单的”所有人可读”:
CREATE POLICY "anyone can read posts" ON posts
FOR SELECT
TO anon, authenticated
USING (true);
或更安全的”登录用户读 published 的”:
CREATE POLICY "authenticated read published" ON posts
FOR SELECT
TO authenticated
USING (status = 'published');
或”用户读自己的 + 公开的”:
CREATE POLICY "own or public" ON posts
FOR SELECT
TO authenticated
USING (author_id = auth.uid() OR is_public = true);
部署:直接在 SQL Editor 运行,或写进 supabase/migrations/:
supabase migration new add_posts_select_policy
# 编辑生成的 SQL 文件
supabase db push
Step 4:分 operation 加 policy
-- 读:所有登录用户
CREATE POLICY "auth read" ON posts FOR SELECT
TO authenticated USING (true);
-- 写:只本人能改
CREATE POLICY "own insert" ON posts FOR INSERT
TO authenticated WITH CHECK (author_id = auth.uid());
CREATE POLICY "own update" ON posts FOR UPDATE
TO authenticated USING (author_id = auth.uid());
CREATE POLICY "own delete" ON posts FOR DELETE
TO authenticated USING (author_id = auth.uid());
Step 5:客户端确认带 token
// 前端
const { data: session } = await supabase.auth.getSession();
console.log('session:', session); // 应该有 access_token
// 调用时 supabase-js 自动用 session,但确认下
const { data, error } = await supabase.from('posts').select('*');
console.log(data, error);
session 是 null → 用户没登录,按 anon role 查。
Step 6:debug policy 真实命中
-- 在 SQL Editor,模拟登录用户跑
SET LOCAL ROLE authenticated;
SET LOCAL "request.jwt.claims" TO '{"sub": "your-user-uuid"}';
SELECT * FROM posts;
RESET ROLE;
看哪条 policy 让 row 通过。
Step 7:server 端用 service_role bypass
某些 server-side 场景(如 cron job、admin endpoint)确实需要看全部:
import { createClient } from '@supabase/supabase-js';
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // ⚠️ 仅 server 端用
{ auth: { autoRefreshToken: false, persistSession: false } }
);
const { data } = await supabaseAdmin.from('posts').select('*'); // bypass RLS
⚠️ service_role key 绝对不能露给 client。
预防建议
- 建表时一起写 RLS policy,policies 走 git migration 不在 dashboard 手改
- 永远不要在生产关闭 RLS——表数据通过 anon key 全开是灾难
- 每个 table 默认至少四条 policy(SELECT / INSERT / UPDATE / DELETE),按需放宽
- policy 用
TO authenticated或TO anon显式指定 role,别 default 全 role - 复杂 policy 写完用
SET LOCAL ROLE自测 - service_role key 只在 server-side env,前端永远 anon key
- 在测试环境定期 audit:“以 anon role 跑能看到哪些表的数据”,发现意外暴露立刻封
- 文档化每个 table 的”谁能读 / 谁能写”,新人加表对照写 policy