supabase-policy-guardrails について
supabase-policy-guardrailsは、Supabaseを運用するチーム向けのガバナンスパターンを提供します。再利用可能なRLSポリシー、命名規則、マイグレーション検査、コスト監視、セキュリティ監査を扱います。
主な機能
- 01再利用可能なRLSポリシーテンプレート
- 02データベース命名規則の検証
- 03CIでのマイグレーションセキュリティ検査
- 04コスト閾値とSlackアラート
- 05セキュリティ監査スクリプト
活用シーン
- 01新しいテーブルに一貫したアクセス方針を適用
- 02CIでRLSのないマイグレーションを阻止
- 03Supabaseのコストを監視
- 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 ポリシーライブラリ(一般的なアクセスパターン向けの再利用可能なテンプレート)、命名規則(テーブル、列、関数、ポリシー)、マイグレーションレビュープロセス(RLS を確認し、破壊的な操作を防ぎ、命名規則を適用する CI チェック)、コストアラートの設定(請求のしきい値と使用状況の監視)、セキュリティ監査スクリプト(公開されたキー、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 を実行するか、マイグレーションを手動で適用します |
例
RLS テンプレートの適用、セキュリティ監査の実行、命名規則の確認などの詳しい例については、CI、コスト、セキュリティのリファレンスを参照してください。
リソース
- Supabase Row Level Security
- Supabase CLI Migrations
- Supabase Management API
- Supabase Pricing
- PostgreSQL Naming Conventions
次のステップ
さまざまな種類のアプリにおけるアーキテクチャパターンについては、supabase-architecture-variantsを参照してください。