TL;DR
SupabaseでRLSを有効にするには、テーブルごとに文を1つ実行します。alter table public.todos enable row level security;、またはダッシュボードの「Enable RLS」です。SQLエディタやマイグレーションで作ったテーブルは、デフォルトでは有効になりません。有効にした直後、テーブルはエラーを返すのではなく、静かになります。ローカルでの私のテストでは、public keyと、ログイン済みの2人のユーザーは、select、update、deleteで200 []を受け取り、失敗したのはinsertだけで42501でした。secret keyは3行すべてを見たままでした。テーブルの上に通常のビューを作ると、public keyに3行すべてが渡ったままでした。2026年10月30日からは、新しいテーブルは何かが届く前にgrantも必要になるため、grantと同じマイグレーションでRLSを有効にし、ポリシーを書いてください。
私は無料のSupabaseセキュリティチェッカーを運営しています。その中で「RLS disabled」は、最も説明が不十分だと感じる指摘です。多くのガイドは、RLSを有効にすると「テーブルにアクセスできなくなる」と書いています。読み取りについては正しく、書き込みについては誤解を招きます。そこで、ローカルのSupabaseスタックに1つのテーブルを作り、下の各ステップのあとに、public key、ログイン済みの2人のユーザー、secret keyとして、あらゆる種類のリクエストを送りました。
SupabaseのRLSとは?
RLS(row-level security、行レベルセキュリティ)は、リクエストごとに行を絞り込むPostgresの機能です。Supabaseはsupabase-jsからのすべてのリクエストを、ロールとして実行されるデータベースクエリに変換します。public(anon、publishable)keyならanon、ログイン済みのユーザーならauthenticatedです。RLSが有効だと、Postgresは行ごとにポリシーを確認し、ポリシーが許可した行だけを返し、変更し、削除します。RLSが無効だと、テーブルにgrantを持つロールは、すべての行に届きます。
secret key(service role)は別です。そのロールはBYPASSRLSを持つため、RLSは決して適用されません。サーバー上にだけ置くべき理由です。2つのキーの違いは、Supabase anon keyとservice role keyの違い:何が開くかにあります。
SupabaseでRLSを有効にするには?
テーブルの作り方によって、3つの方法があります。
- Table Editorでテーブルを作るときは、「Enable Row Level Security (RLS)」にチェックを入れたままにします。Supabaseのダッシュボードのソースでは「Recommended」と表示され、外すと、テーブルが誰にでも読み書きできる状態になるという警告が出ます。
- 既存のテーブルについては、SupabaseのAPIセキュリティのガイドに、Database、Policiesの順に進み、テーブルの「Enable RLS」を選ぶとあります。
- SQLで、SQLエディタまたはマイグレーションに書きます。
alter table public.todos enable row level security;
Supabaseのガイドは明確に述べています。ダッシュボードで作ったテーブルはデフォルトでRLSが有効で、SQLエディタや別のツールで作ったテーブルはそうではありません。マイグレーションやAIツールでスキーマを作っているなら、すべてのテーブルを確認してください。
RLSを有効にした直後はどうなる?
insert以外はエラーになりません。所有者の列(user_id uuid default auth.uid())を持つpublic.todosを作り、古いプロジェクトと同じようにAPIのロールにgrantして、3行を追加しました。ユーザーAの2行とユーザーBの1行です。そのあと、ポリシーなしでRLSを有効にしました。
| リクエスト | public key | ユーザーA | ユーザーB | secret key |
|---|---|---|---|---|
| Select | 200、0行 | 200、0行 | 200、0行 | 200、3行 |
| 自分の行のInsert | 401、エラー 42501 | 403、エラー 42501 | 403、エラー 42501 | 201、行を挿入 |
| 行のUpdate | 200、0行 | 200、0行 | 200、0行 | 200、1行 |
| 行のDelete | 200、0行 | 200、0行 | 200、0行 | 200、1行 |
insertのエラーはnew row violates row-level security policyです。updateとdeleteは空の配列を付けて200を返すため、返された行を確認しないアプリは、何も保存されていないのに「保存しました」と報告します。RLSを有効にしたあとは、書き込んだ内容を読み戻すか、Prefer: count=exactで件数を取得してください。
RLSの前は、同じgrantのままで、public keyは3行すべてを読み、insertでき、任意の行をupdateやdeleteできました。ユーザーAはユーザーBの行を編集・削除でき、自分の行をBに渡すこともできました。
テーブルをまた使えるようにするには?
操作ごとにポリシーを書きます。自分の行用の一般的なセットは次のとおりです。
create policy "own select" on public.todos for select to authenticated
using ((select auth.uid()) = user_id);
create policy "own insert" on public.todos for insert to authenticated
with check ((select auth.uid()) = user_id);
create policy "own update" on public.todos for update to authenticated
using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
create policy "own delete" on public.todos for delete to authenticated
using ((select auth.uid()) = user_id);
これで、ユーザーAは自分の2行、ユーザーBは自分の1行が見えました。public keyは0行のままです。AがBの所有する行をinsertしようとしたとき、または自分の行をBに移そうとしたときの応答は403, 42501でした。この拒否は、updateのwith checkによるものです。AがBの行をupdateまたはdeleteしようとしたときの応答は200, 0 rowsでした。ほかのパターン(公開プロフィール、チーム、MFA)は、Supabase RLSポリシー例:APIで検証済みにあります。
secret keyについて1つ細かな点があります。secret keyが挿入した行はuser_id = nullになりました。そのキーの背後にはユーザーがおらず、auth.uid()が何も返さないためです。RLSは迂回されましたが、デフォルト値は列名が示唆する動きをしませんでした。
各ステップでpublic keyは何を読めた?
すべてが漏れるステップが2つあります。RLSのないgrantと、正しいポリシーを持つテーブルの上の通常のビューです。次の2つのセクションで、その両方を説明します。
10月30日のgrant変更のあとも、RLSは必要?
必要です。2026年10月30日から、Supabaseはpublicの新しいテーブルに、APIのロールへの自動grantを付けなくなりました。新規プロジェクトも既存プロジェクトも同じです。私のローカルスタックは、すでにその動作でした。RLSが無効の状態で作ったばかりのテーブルは、secret keyを含むすべてのロールに42501を返したため、何も漏れませんでした。詳細はSupabase 10月30日のgrant変更:壊れる点と確認方法にあります。
ただし、アプリがテーブルを必要とした時点でgrantは戻ってきます。そこからは、public keyとすべての行の間にあるのはRLSだけです。そこで、すべてを1つのマイグレーションに、この順番で入れてください。
create table public.todos (...);
alter table public.todos enable row level security;
create policy "own select" on public.todos ...;
grant select, insert, update, delete on table public.todos to authenticated;
grantは、そのテーブルが必要なロールだけに与えます。anonからすべてを取り消すと、public keyのselectは200 []から401, 42501に変わりました。grantが1つ目の鍵、RLSが2つ目の鍵です。
ビューではなぜ行が漏れる?
ビューは所有者の権限で実行され、Supabaseでは所有者がpostgresで、RLSを迂回するからです。todosに自分の行のポリシーを設定したうえで、create view public.todos_view as select * from public.todosという通常のビューを作ると、public keyとユーザーBに3行すべてが返りました。
with (security_invoker = true)で作り直すと、同じビューがテーブルとまったく同じ結果になりました。public keyには0行、Aには2行、Bには1行です。
create view public.todos_view with (security_invoker = true) as
select * from public.todos;
SupabaseのAdvisorは、通常の種類をsecurity_definer_viewとして警告します。その意味と直し方は、Supabase security definer viewの解説ノートにあります。
新しいテーブルすべてでRLSを自動的に有効にするには?
イベントトリガーを使います。Supabaseのイベントトリガーのガイドには、関数rls_auto_enable()と、publicでのcreate table、create table as、select intoのあとに実行されるトリガーensure_rlsが載っています。公開されているとおりに入れたところ、次の結果になりました。
- そのあとに
create tableで作ったテーブルは、alterなしでRLSが有効でした。public keyはselectで200 []、insertで401でした。 create table as selectで作ったテーブルも、RLSが有効でした。- トリガーより前に作ったテーブルは、無効のままでした。トリガーはさかのぼって適用されません。
トリガーがないと、保護されたテーブルの隣に作った2つ目のテーブルはRLSが無効で、grantすればpublic keyが自由に読み書きできました。また、対象はpublicだけです。公開している他のスキーマには、関数に専用の行が必要です。
FORCE ROW LEVEL SECURITYは役に立つ?
心配なロールには効きません。alter table ... force row level securityは、RLSをテーブルの所有者にも適用させます。Supabaseではpostgresもservice_roleもBYPASSRLSを持つため、forceのあとも所有者は3行を数え、secret keyはすべてを見たままで、postgresが所有するビューは漏れたままでした。変化があったのは、BYPASSRLSなしで私が作った所有者ロールだけで、以前は2行、以後は0行でした。
自分のプロジェクトをどう確認する?
- SQLエディタで、RLSが無効な
publicのテーブルを一覧します。
select c.relname as table_name
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relkind in ('r', 'p')
and not c.relrowsecurity
order by 1;
- ダッシュボードのAdvisorは、同じ内容を
rls_disabled_in_publicとして報告します。このエラーの意味はRLS disabled in publicの解説ノートにあります。 - 無料のSupabaseセキュリティチェックは、プロジェクトを読み取り専用で調べ、APIが届くRLS無効のテーブル、RLSを迂回するビュー、全員に許可するポリシーを、それぞれの修正SQL付きで一覧します。
- スキャナーは、どの行が誰のものかを知ることができません。それを証明するには、上のように2人のユーザーとしてリクエストを送ります。ロールのあるアプリの完全な方法は、Supabase RLSのテスト:87権限中30が無効だったにあります。
テスト方法
- ノートPC上のSupabase CLI 2.109.1:Postgres 17.6、PostgREST 14.14、Auth 2.192.0。パスワードでログインした2人のユーザーと、ローカルのpublishable keyおよびsecret key。
- テーブルは
public.todosの1つで、リクエストのたびに同じ3行に戻しました。各ステップはSQLとして適用し、各リクエストはREST API経由で送り、ステータスコードと本文を記録しました。 - ステップ2〜5のgrantは、2026年10月30日より前に作られたプロジェクトに合わせて、手作業で追加しました。
限界。 ローカルスタック1つ、架空のデータ、2026年10月28日です。ダッシュボードの手順はSupabaseのドキュメントからの引用で、実際にクリックして確認したものではありません。ダッシュボード自身のロールで作ったテーブルは、別のデフォルトgrantになる可能性があります。それも、Storage、Realtime、GraphQLも、テストしていません。