# 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つの罠まで、公式準拠の実コードで。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: Supabase, RLS, PostgreSQL, セキュリティ, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/supabase-rls-getting-started-enable-first-policy-guide
- カテゴリ: データベース・RLS
- 総合ガイド: https://tomodahinata.com/blog/supabase-production-guide-nextjs-rls-realtime-edge-functions

## 要点

- RLSは『テーブルへの全クエリに自動で付く暗黙のWHERE』。enableした瞬間にデフォルト拒否（ポリシーが無ければ誰も読めない）になるのが出発点
- アクセスには2層が要る。GRANT（そのテーブルを操作する権限）とPOLICY（どの行を操作してよいか）。片方だけでは動かず、これが入門最大の混乱点
- USINGは『見える/触れる既存行』、WITH CHECKは『書いてよい新しい行』。SELECT/DELETEはUSINGのみ、INSERTはWITH CHECKのみ、UPDATEは両方が要る
- Supabaseはリクエストをanon（未認証）/authenticated（認証済み）/service_role（RLSバイパス・サーバー専用）にマップする。TO authenticatedとauth.uid()で『自分の行だけ』を表現する
- 初心者の事故は『enable忘れ＝全公開』『service_roleをクライアントで使用＝全バイパス』『WITH CHECK忘れ＝書き込みバイパス』に集中する。最初から型として避ける

---

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

読み終えると、あなたは**最初のRLSポリシーを自信を持って書け、なぜそれで安全になるのかを説明でき、初心者が必ずハマる5つの罠を最初から回避できる**ようになります。題材には、私が一人で構築した[複数人同時編集のリアルタイム試合記録アプリ](/case-studies/realtime-sports-scoring-app)（**69テーブル全てにRLSを有効化し、約280本のポリシー**を本番運用）の設計判断を交えます。内容は[Supabase公式](https://supabase.com/docs/guides/database/postgres/row-level-security)・[PostgreSQL公式](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)（2026年6月時点）に忠実です。

> **この記事の位置づけ**：これは「RLSの一番最初」を扱う入門です。書けるようになった後の道筋——[マルチテナント本番設計](/blog/supabase-rls-production-multi-tenancy-patterns)、[パフォーマンス最適化](/blog/supabase-rls-performance-optimization-select-wrap-index-guide)、[pgTAPテスト](/blog/supabase-rls-testing-pgtap-policy-regression-guide)——は各記事へ繋ぎます。まずはここで土台を固めましょう。

---

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

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

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

```sql
-- アプリが投げた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を**有効化**します。

```sql
alter table public.profiles enable row level security;
```

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

これは**機能であって、バグではありません**。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はこうです。

```sql
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公式](https://supabase.com/docs/guides/database/postgres/row-level-security)）。

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

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

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

`service_role` キーがクライアントに漏れる＝RLSを全部書いた意味が消える、という事故は後を絶ちません。鍵の取り違えだけは、入門段階から型として避けてください（[詳細：anon/service_roleキー露出](/blog/supabase-anon-key-service-role-key-exposure-guide)）。

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

---

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

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

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

どのコマンドでどちらが効くかが、ポリシー設計の地図です（[PostgreSQL: CREATE POLICY](https://www.postgresql.org/docs/current/sql-createpolicy.html)）。

| コマンド | USING（既存行） | WITH CHECK（新しい行） |
| --- | --- | --- |
| `SELECT` | ✅ 効く | — |
| `INSERT` | — | ✅ 効く |
| `UPDATE` | ✅ 効く | ✅ 効く |
| `DELETE` | ✅ 効く | — |

「自分の `profiles` 行だけを読み書きできる」を、コマンドごとに4本書きます。`(select auth.uid())` と副問い合わせで包むのは、最初から**パフォーマンスの効く形**で書く癖です（[理由：性能最適化](/blog/supabase-rls-performance-optimization-select-wrap-index-guide)）。

```sql
-- 読む：自分の行だけ見える
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`欠落による[書き込みバイパス](/blog/supabase-rls-with-check-using-write-bypass-guide)です。

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

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

```sql
-- 認証済みなら誰でも読める公開プロフィール一覧
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公式](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)）。所有者として接続するバックエンド処理でもRLSを効かせたいなら、明示します。

```sql
alter table public.profiles force row level security;
```

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

ロールやチーム所属など、DBを引かずに**トークンの中身だけ**で判定したいときは `auth.jwt()` を使います。ただし**認可に使ってよいのは `app_metadata`（書き換え不可）だけ**で、ユーザーが書き換えられる `user_metadata` を認可に使うのは厳禁です（[応用：RBAC設計](/blog/supabase-rls-rbac-custom-claims-app-metadata-authorize-guide)）。

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

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

```sql
-- 二要素認証(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つを指差し確認してください。

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

---

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

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

最初の一歩を踏めたら、次は[マルチテナント設計](/blog/supabase-rls-production-multi-tenancy-patterns)・[Next.jsとの統合](/blog/nextjs-app-router-supabase-rls-ssr-server-client-auth-guide)・[性能](/blog/supabase-rls-performance-optimization-select-wrap-index-guide)へ。RLSは「書いて終わり」ではなく、**設計・統合・性能・テスト**の4点で本番に耐えます。

### 一次情報（必ず最新を確認してください）

- [Supabase: Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security)
- [PostgreSQL: Row Security Policies](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
- [PostgreSQL: CREATE POLICY](https://www.postgresql.org/docs/current/sql-createpolicy.html)
