关于 supabase-policy-guardrails
supabase-policy-guardrails 为团队提供 Supabase 治理模式,涵盖可复用的 RLS 策略、命名规范、迁移检查、成本监控和安全审计。适用于需要在 CI/CD 中统一数据库访问控制与安全检查的项目。
主要功能
- 01可复用的 RLS 策略模板
- 02数据库对象命名规范验证
- 03CI 迁移安全检查
- 04成本阈值与 Slack 告警
- 05安全审计脚本
适用场景
- 01为新表应用一致的访问策略
- 02在 CI 中拦截缺少 RLS 的迁移
- 03监控 Supabase 使用成本
- 04定期检查宽松策略与暴露密钥
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ jeremylongshore/ tons-of-skills-marketplace --skill supabase-policy-guardrails安装后在 Claude Code 中输入/supabase-policy-guardrails确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/ci-cost-security.mderrors.mdeslint-rules.mdexamples.md详细介绍
Supabase 策略护栏
概述
大规模使用 Supabase 时的组织治理:共享 RLS 策略库(适用于常见访问模式的可复用模板)、命名规范(表、列、函数、策略)、迁移审查流程(通过 CI 检查确保启用 RLS、防止破坏性操作并强制执行命名规范)、成本告警配置(账单阈值和用量监控),以及安全审计脚本(扫描暴露的密钥、缺少 RLS 的表和权限过于宽松的策略)。所有模式均使用来自 @supabase/supabase-js 的真实 createClient 和 Supabase CLI 命令。
前置条件
- 已安装并关联
supabaseCLI 的 Supabase 项目 - 已安装
@supabase/supabase-jsv2+ - CI/CD 流水线(推荐使用 GitHub Actions)
- 通过
psql或 Supabase SQL Editor 访问数据库 - 成本告警和用量 API 推荐使用 Pro 计划
操作说明
步骤 1 — 共享 RLS 策略库和命名规范
RLS 策略模板
创建可复用的 RLS 策略模板,供团队应用于新表。这样可以避免每位开发者临时编写策略,并确保访问控制一致。
-- supabase/migrations/00000000000000_rls_policy_library.sql
-- Shared RLS policy library — apply these templates to new tables
-- ============================================================
-- Template 1: Owner-only access (user owns the row)
-- Usage: tables with a user_id column (todos, profiles, settings)
-- ============================================================
CREATE OR REPLACE FUNCTION public.rls_owner_only(table_name text, user_column text DEFAULT 'user_id')
RETURNS void AS $$
BEGIN
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', table_name);
EXECUTE format(
'CREATE POLICY "owner_select" ON public.%I FOR SELECT USING (%I = auth.uid())',
table_name, user_column
);
EXECUTE format(
'CREATE POLICY "owner_insert" ON public.%I FOR INSERT WITH CHECK (%I = auth.uid())',
table_name, user_column
);
EXECUTE format(
'CREATE POLICY "owner_update" ON public.%I FOR UPDATE USING (%I = auth.uid())',
table_name, user_column
);
EXECUTE format(
'CREATE POLICY "owner_delete" ON public.%I FOR DELETE USING (%I = auth.uid())',
table_name, user_column
);
END;
$$ LANGUAGE plpgsql;
-- ============================================================
-- Template 2: Organization-scoped access (user is member of org)
-- Usage: tables with org_id referencing org_members
-- ============================================================
CREATE OR REPLACE FUNCTION public.rls_org_scoped(
table_name text,
org_column text DEFAULT 'org_id',
allow_delete boolean DEFAULT false
)
RETURNS void AS $$
BEGIN
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', table_name);
EXECUTE format(
'CREATE POLICY "org_select" ON public.%I FOR SELECT USING (
%I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid())
)', table_name, org_column
);
EXECUTE format(
'CREATE POLICY "org_insert" ON public.%I FOR INSERT WITH CHECK (
%I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid())
)', table_name, org_column
);
EXECUTE format(
'CREATE POLICY "org_update" ON public.%I FOR UPDATE USING (
%I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid() AND role IN (''admin'', ''editor''))
)', table_name, org_column
);
IF allow_delete THEN
EXECUTE format(
'CREATE POLICY "org_delete" ON public.%I FOR DELETE USING (
%I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid() AND role = ''admin'')
)', table_name, org_column
);
END IF;
END;
$$ LANGUAGE plpgsql;
-- ============================================================
-- Template 3: Public read, authenticated write
-- Usage: blog posts, product listings, public content
-- ============================================================
CREATE OR REPLACE FUNCTION public.rls_public_read_auth_write(
table_name text,
owner_column text DEFAULT 'created_by'
)
RETURNS void AS $$
BEGIN
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', table_name);
EXECUTE format(
'CREATE POLICY "public_select" ON public.%I FOR SELECT USING (true)',
table_name
);
EXECUTE format(
'CREATE POLICY "auth_insert" ON public.%I FOR INSERT WITH CHECK (auth.uid() IS NOT NULL)',
table_name
);
EXECUTE format(
'CREATE POLICY "owner_update" ON public.%I FOR UPDATE USING (%I = auth.uid())',
table_name, owner_column
);
EXECUTE format(
'CREATE POLICY "owner_delete" ON public.%I FOR DELETE USING (%I = auth.uid())',
table_name, owner_column
);
END;
$$ LANGUAGE plpgsql;
-- Apply templates to tables:
-- SELECT public.rls_owner_only('todos');
-- SELECT public.rls_org_scoped('projects', 'org_id', true);
-- SELECT public.rls_public_read_auth_write('blog_posts', 'author_id');
命名规范
-- supabase/migrations/00000000000001_naming_convention_check.sql
-- Validation function that checks naming conventions at migration time
CREATE OR REPLACE FUNCTION public.validate_naming_conventions()
RETURNS TABLE(issue text, object_name text, suggestion text) AS $$
BEGIN
-- Tables must be snake_case, plural
RETURN QUERY
SELECT
'Table name should be plural snake_case'::text,
t.tablename::text,
regexp_replace(t.tablename, '([A-Z])', '_\1', 'g')::text
FROM pg_tables t
WHERE t.schemaname = 'public'
AND (
t.tablename ~ '[A-Z]' -- contains uppercase
OR t.tablename ~ '-' -- contains hyphens
OR t.tablename !~ 's$' -- not plural (heuristic)
)
AND t.tablename NOT LIKE '\_%'; -- skip internal tables
-- Columns must be snake_case
RETURN QUERY
SELECT
'Column name should be snake_case'::text,
(c.table_name || '.' || c.column_name)::text,
regexp_replace(c.column_name, '([A-Z])', '_\1', 'g')::text
FROM information_schema.columns c
WHERE c.table_schema = 'public'
AND (c.column_name ~ '[A-Z]' OR c.column_name ~ '-');
-- Foreign key columns should end with _id
RETURN QUERY
SELECT
'Foreign key column should end with _id'::text,
(tc.table_name || '.' || kcu.column_name)::text,
(kcu.column_name || '_id')::text
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public'
AND kcu.column_name NOT LIKE '%_id';
-- Boolean columns should start with is_ or has_
RETURN QUERY
SELECT
'Boolean column should start with is_ or has_'::text,
(c.table_name || '.' || c.column_name)::text,
('is_' || c.column_name)::text
FROM information_schema.columns c
WHERE c.table_schema = 'public'
AND c.data_type = 'boolean'
AND c.column_name NOT LIKE 'is_%'
AND c.column_name NOT LIKE 'has_%';
END;
$$ LANGUAGE plpgsql;
-- Run: SELECT * FROM public.validate_naming_conventions();
命名规范参考
| 对象 | 规范 | 示例 |
|---|---|---|
| 表 | 复数形式的 snake_case | user_profiles、order_items |
| 列 | snake_case | created_at、full_name |
| 外键 | {referenced_table_singular}_id | user_id、order_id |
| 布尔值 | 使用 is_ 或 has_ 前缀 | is_active、has_verified_email |
| 时间戳 | 使用 _at 后缀 | created_at、updated_at、deleted_at |
| RLS 策略 | {scope}_{operation} | owner_select、org_insert |
| 函数 | verb_noun | create_user、get_dashboard_metrics |
| 索引 | idx_{table}_{columns} | idx_orders_user_id_created_at |
| 迁移 | {timestamp}_{verb}_{description} | 20250322000000_create_orders_table.sql |
步骤 2 — 通过 CI 检查执行迁移审查
有关 GitHub Actions 迁移护栏(强制启用 RLS、检查命名、拦截破坏性操作)、提交前钩子、通过 Slack 告警进行成本监控、安全审计脚本以及定期 Edge Function 审计,请参阅CI 检查、成本告警和安全审计。
输出
- 共享 RLS 策略库,包含仅限所有者、按组织限定范围和公开读取的模板
- 命名规范验证函数,用于检查表、列、外键和布尔值
- 强制执行 RLS、命名规范和破坏性操作控制的 CI 流水线
- 阻止硬编码密钥和未启用 RLS 的表的提交前钩子
- 可配置阈值并通过 Slack 发送告警的成本监控脚本
- 检测缺少 RLS、权限过于宽松的策略和缺失索引的安全审计脚本
- 用于持续安全监控的定期 Edge Function
错误处理
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 新表的 CI RLS 检查失败 | 迁移中缺少 ENABLE ROW LEVEL SECURITY | 在同一迁移中,于 CREATE TABLE 后添加 ALTER TABLE |
| 命名规范检查误报 | 表有意使用单数名称(例如 config) | 在验证函数的排除列表中添加该表 |
| 成本告警未触发 | 缺少 SUPABASE_ACCESS_TOKEN | 在 supabase.com/dashboard/account/tokens 生成令牌 |
| 安全审计超时 | 要扫描的表过多 | 仅审计指定架构,或对结果进行分页 |
| 提交前检查误拦截测试中的合法 JWT | 测试夹具包含类似 JWT 的字符串 | 将测试文件路径添加到排除模式中 |
| 找不到 RLS 模板函数 | 尚未应用迁移 | 运行 supabase db reset,或手动应用迁移 |
示例
完整示例请参阅 CI、成本和安全参考,其中包括应用 RLS 模板、运行安全审计和检查命名规范。
资源
后续步骤
如需了解不同应用类型的架构模式,请参阅 supabase-architecture-variants。
评论
支持 Markdown