メインコンテンツへスキップ
友田 陽大
データベース・RLS
Supabase
RLS
PostgreSQL
セキュリティ
アーキテクチャ設計

Supabase RLS入門:最初のポリシーを書く——有効化・GRANT・anon/authenticatedの基礎を、つまずきポイント込みで

Supabase(PostgreSQL)の行レベルセキュリティ(RLS)を、ゼロから最初のポリシーが書けるまで丁寧に解説する入門ガイド。enable row level security、SELECT/INSERT/UPDATE/DELETEポリシー、USING/WITH CHECK、GRANTとRLSの2層モデル、anon/authenticated/service_role、初心者が必ずハマる5つの罠まで、公式準拠の実コードで。

公開日
読了時間
11分
著者
友田 陽大
シェア

「Supabaseでテーブルを作ったら、anon キーで誰でも全行読めてしまった」——RLSを知らずにSupabaseを使い始めると、ほぼ全員がこの事故を一度は踏みます。逆に、RLSを有効化した瞬間に「今度は自分でも1行も読めなくなった」と戸惑う。この2つの戸惑いの正体を解きほぐすのが、この入門記事のゴールです。

読み終えると、あなたは最初のRLSポリシーを自信を持って書け、なぜそれで安全になるのかを説明でき、初心者が必ずハマる5つの罠を最初から回避できるようになります。題材には、私が一人で構築した複数人同時編集のリアルタイム試合記録アプリ69テーブル全てにRLSを有効化し、約280本のポリシーを本番運用)の設計判断を交えます。内容はSupabase公式PostgreSQL公式(2026年6月時点)に忠実です。

この記事の位置づけ:これは「RLSの一番最初」を扱う入門です。書けるようになった後の道筋——マルチテナント本番設計パフォーマンス最適化pgTAPテスト——は各記事へ繋ぎます。まずはここで土台を固めましょう。


1. RLSとは何か:全クエリに自動で付く「暗黙のWHERE」

行レベルセキュリティ(Row-Level Security, RLS)を一言で言うと、テーブルへのあらゆるクエリに、データベースが自動で付け足す WHEREです。

たとえば profiles テーブルに「自分の行しか見えない」というポリシーを書くと、アプリが select * from profiles という無防備なSQLを投げても、PostgreSQLは内部でこう実行します。

-- アプリが投げたSQL
select * from profiles;

-- PostgreSQLが実際に実行するSQL(ポリシーが暗黙のWHEREとして付く)
select * from profiles where (auth.uid() = user_id);

ここが決定的に重要です。この where はアプリ側のコードではなく、DB側に存在します。 だから——

  • どんなクライアント(ブラウザ・モバイル・curl・他人のスクリプト)から来ても効く
  • アプリのコードに where user_id = ? を書き忘れても効く
  • 新しいエンドポイントを足しても、自動で効く

アプリの if (user.id === row.ownerId) のような出し分けは「書き忘れたら漏れる」のに対し、RLSは最終防衛線としてDBが弾く。これがRLSの価値であり、認可を「アプリの善意」ではなく「DBの構造」で守るということです。


2. 出発点:enableした瞬間に「デフォルト拒否」になる

最初のSQLはこれです。テーブルにRLSを有効化します。

alter table public.profiles enable row level security;

ここで初心者が必ず驚くことが起きます。有効化した直後、ポリシーを1本も書いていなければ、(テーブル所有者以外は)誰も1行も読めなくなります。 PostgreSQL公式の言葉では「ポリシーが存在しなければ、デフォルト拒否(default-deny)が適用され、行は見えず変更もできない」(PostgreSQL: Row Security Policies)。

これは機能であって、バグではありません。RLSの設計思想は「まず全部閉じる。許可は明示的に1本ずつ足す」というホワイトリスト(許可リスト)方式です。だから手順は必ずこの順番になります。

  1. enable row level security全部閉じる
  2. create policy ...必要な許可だけを開ける

逆をやってはいけません。「とりあえず全部開けておいて後で閉じる」は、閉じ忘れた瞬間に全公開です。閉じてから開ける——これがRLSの鉄則です。


3. つまずきの最大関門:GRANT と POLICY は別物(2層モデル)

入門者が最も混乱するのがここなので、最初に潰します。Supabaseでテーブルにアクセスできるには、2つの別々の関門を両方通る必要があります。

何を決めるか構文たとえると
GRANT(権限)そのテーブルを そもそも操作してよいか(テーブル単位)grant select on ... to authenticated建物に入る鍵
POLICY(RLS)どの行を 操作してよいか(行単位)create policy ... using (...)部屋ごとの入室許可

両方が true のときだけアクセスできます。 GRANTがなければRLS以前に弾かれ、GRANTがあってもポリシーが無ければデフォルト拒否で弾かれる。「ポリシーを書いたのにアクセスできない」「RLSを無効にしているのにアクセスできない」の多くは、この2層の取り違えです。

Supabaseでは、テーブルをダッシュボードで作ると anon / authenticated ロールへのGRANTが自動で付くことが多いため、GRANT層を意識しないまま動いてしまい、かえって混乱の種になります。公式が示す明示的なGRANTはこうです。

grant select on public.profiles to anon;
grant select, insert, update, delete on public.profiles to authenticated;
grant select, insert, update, delete on public.profiles to service_role;

覚え方:GRANTは「テーブルに触れるか(粗い)」、POLICYは「どの行に触れるか(細かい)」。RLSはGRANTをさらに絞るもので、置き換えるものではありません。


4. 誰として実行されるか:anon / authenticated / service_role

ポリシーを書く前に、Supabaseが「リクエストを誰として実行するか」を理解します。Supabaseは受け取ったリクエストを、JWT(トークン)の有無に応じて3つのPostgresロールにマップします(Supabase公式)。

ロールいつ使われるかRLS使う場所
anonトークン無し(未ログイン)効く公開データの読み取り
authenticated有効なユーザートークンあり(ログイン済み)効く通常のアプリ操作
service_roleサービスロールキー使用バイパスするサーバー側の管理処理のみ

ここで入門者にとって最重要の警告です。

service_role キーは RLS を完全にバイパスします。絶対にブラウザ・モバイルアプリ・公開リポジトリに置かないでください。 これはサーバー(信頼できる環境)専用です。クライアントには必ず anon(publishable)キーを使います。

service_role キーがクライアントに漏れる=RLSを全部書いた意味が消える、という事故は後を絶ちません。鍵の取り違えだけは、入門段階から型として避けてください(詳細:anon/service_roleキー露出)。

ポリシー内で「今のユーザーは誰か」を知るには、auth.uid()(ユーザーID)と auth.jwt()(トークン全体)を使います。未認証時、auth.uid()null を返します。


5. 最初のポリシーを書く:USING と WITH CHECK

ポリシーには条件式を書く場所が2つあります。役割がまったく違うので、ここを最初に正確に掴みます。

  • USING (...)既存の行に対する検査。true になった行だけが「見える/触れる」。=読み取り側のフィルタ
  • WITH CHECK (...)INSERT/UPDATE で生まれる新しい行に対する検査。false なら弾かれる。=書き込み側のフィルタ

どのコマンドでどちらが効くかが、ポリシー設計の地図です(PostgreSQL: CREATE POLICY)。

コマンドUSING(既存行)WITH CHECK(新しい行)
SELECT✅ 効く
INSERT✅ 効く
UPDATE✅ 効く✅ 効く
DELETE✅ 効く

「自分の profiles 行だけを読み書きできる」を、コマンドごとに4本書きます。(select auth.uid()) と副問い合わせで包むのは、最初からパフォーマンスの効く形で書く癖です(理由:性能最適化)。

-- 読む:自分の行だけ見える
create policy "Users can view own profile"
on public.profiles for select
to authenticated
using ( (select auth.uid()) = user_id );

-- 作る:自分のuser_idを持つ行しか作れない(他人になりすませない)
create policy "Users can insert own profile"
on public.profiles for insert
to authenticated
with check ( (select auth.uid()) = user_id );

-- 更新:自分の行だけ対象にでき(USING)、更新後も自分の行のまま(WITH CHECK)
create policy "Users can update own profile"
on public.profiles for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

-- 消す:自分の行だけ消せる
create policy "Users can delete own profile"
on public.profiles for delete
to authenticated
using ( (select auth.uid()) = user_id );

なぜ INSERT に WITH CHECK が要るのか

INSERTには「既存行」が無いので USING は使えません。代わりに WITH CHECK で「作ろうとしている新しい行」を検査します。with check ((select auth.uid()) = user_id) は「user_id 列に自分のIDが入っている行しか作れない」という意味。これを書かないと、認証済みユーザーが他人の user_id を持つ行を捏造できます。「自分の行しか読めない」テーブルに「他人の行を書ける」穴が同居する——これがWITH CHECK欠落による書き込みバイパスです。

公開データを読ませたいとき

「ログインユーザーなら全員読める」なら using (true) を、未ログインにも見せるなら to anon を足します。

-- 認証済みなら誰でも読める公開プロフィール一覧
create policy "Public profiles are viewable by signed-in users"
on public.profiles for select
to authenticated
using ( true );

using (true) は「行による制限なし(GRANTの範囲で全部読める)」という意味で、意図した公開です。問題は意図せず true にしてしまうこと。だから「なぜこの行は公開してよいのか」を常に説明できる状態にしておきます。


6. 一段進んだ基礎:FORCE・auth.jwt()・restrictive

入門の締めに、すぐ必要になる3つの概念を軽く触れておきます。深掘りは各リンク先へ。

テーブル所有者もRLSに従わせる:FORCE

PostgreSQLではテーブル所有者は通常RLSをバイパスします(PostgreSQL公式)。所有者として接続するバックエンド処理でもRLSを効かせたいなら、明示します。

alter table public.profiles force row level security;

JWTのクレームで判定する:auth.jwt()

ロールやチーム所属など、DBを引かずにトークンの中身だけで判定したいときは auth.jwt() を使います。ただし認可に使ってよいのは app_metadata(書き換え不可)だけで、ユーザーが書き換えられる user_metadata を認可に使うのは厳禁です(応用:RBAC設計)。

「絶対に外せない条件」をANDで固定する:restrictive

通常のポリシー(permissive)はORで結合され「許可を足す」ものです。対して as restrictiveANDで結合され「全体に制約をかける」。テナント境界やMFA要求のような外せない条件はrestrictiveで固定します。

-- 二要素認証(aal2)を通したセッションでなければ更新を一切許さない(全UPDATEにANDで効く)
create policy "Require MFA for updates"
on public.profiles as restrictive for update
to authenticated
using ( (select auth.jwt()->>'aal') = 'aal2' );

7. 入門者が必ずハマる5つの罠(チェックリスト)

最後に、私が現場で何度も見てきた「最初の事故」を、回避法とセットで。デプロイ前にこの5つを指差し確認してください。

#症状回避
1enable を忘れるRLSを書いたつもりが全行公開公開スキーマの全テーブルで enable。CIで未有効化を検査
2service_role をクライアントで使うRLSが全部バイパスされ無意味化クライアントは anon/publishableキーのみ。service_roleはサーバー限定
3WITH CHECK を忘れる読めないが他人の行を書けるINSERT/UPDATEに必ず WITH CHECK。SELECTのテストだけで安心しない
4GRANT と POLICY を混同「ポリシー書いたのに動かない/消したのに動く」2層モデルを思い出す。両方がtrueで初めて通る
5テストが「許可」だけポリシーを緩めた退行に気づけない「拒否されるべきが拒否される」もpgTAPでテスト

まとめ:RLSは「閉じてから、明示的に開ける」

  • RLSは全クエリに付く暗黙のWHERE。アプリの善意ではなくDBの構造で認可を守る最終防衛線。
  • enable row level securityデフォルト拒否になり、create policy で許可を1本ずつ開ける。
  • アクセスには GRANT(テーブル権限)と POLICY(行の許可)の2層が両方要る。
  • USING=見える既存行、WITH CHECK=書ける新しい行。INSERT/UPDATEのWITH CHECKを忘れない
  • service_role はRLSをバイパスする——クライアントに置かない

最初の一歩を踏めたら、次はマルチテナント設計Next.jsとの統合性能へ。RLSは「書いて終わり」ではなく、設計・統合・性能・テストの4点で本番に耐えます。

一次情報(必ず最新を確認してください)

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

お困りごとはありませんか?

設計から実装・運用まで、一人 × 生成AI で伴走します

この記事のような実装を、要件定義から本番運用まで一気通貫で。まずは30分の無料技術相談から、状況をお聞かせください。

プロジェクト単位(請負)・技術顧問のどちらにも対応可能です。まずは30分の無料技術相談から。

あわせて読みたい