---
title: "Supabase RLSポリシー例：APIで検証済み"
description: "Supabase RLSポリシーのコピペ用の例。自分の行、公開プロフィール、チーム、MFAを、ゲスト、所有者、別のユーザーで実行した結果を示します。"
url: "https://madsalt.tech/ja/blog/supabase-rls-policy-examples/"
author: "Denys Zaichenko"
author_url: "https://madsalt.tech/ja/about/"
language: "ja"
published: 2026-10-27
---

# Supabase RLSポリシー例：APIで検証済み

TL;DR

以下は、ほとんどのSupabaseアプリに必要なRLSポリシーを、ゲスト、行の所有者、サインイン済みの別のユーザーとして、REST API経由で実行した結果です。コピーする前に知っておきたい結果が3つあります。他人の行へのupdateは、エラーではなく空のリストを添えた`200`を返すこと。一見無害なメンバーシップテーブルのinsertポリシーのせいで、ユーザーが別の会社に参加し、その会社のプロジェクトを読めたこと。そして、`auth.uid()`の代わりに`(select auth.uid())`と書くだけで、200,000行のクエリが156 msから7.4 msになったことです。

私はDenys、フルスタックかつAIエンジニアで、私が作る、または直すアプリの多くはSupabaseで動いています。ポリシーのスニペットはどこにでもありますが、実際のリクエストが当たったときに何が起きるかは、ほとんど示されません。そこで実行してみました。ここにあるのは、私がいつも最初に使うSupabase RLSポリシーと、それぞれの結果です。

**テスト方法。** ほとんどのリクエストは、ローカルのSupabaseスタック（CLI 2.109.1、Postgres 17.6、PostgREST 14.14、Auth 2.192.0）に、公開キーと、サインイン済みの実在のユーザー2人（AとB）で送りました。構文エラーや計測のようにHTTPが要らないケースは、Supabaseの`anon`ロールと`authenticated`ロールを置き、`auth.uid()`を作り直した素のローカルPostgres 17.10で実行しました。すべて2026年10月24日、架空の行で行っています。

## どのポリシーより先に：テーブルにgrantはある？

ポリシーは、ロールがどの行を見られるかを決めます。grantは、ロールがそもそもテーブルに届くかを決めます。現在のSupabaseプロジェクトでは、新しいテーブルにData API用のgrantが自動では付かなくなっています（2026年10月30日からはすべてのプロジェクトで）。私のスタックで、RLSがオンでgrantのないテーブルは次のように応答しました。

| リクエスト                | ステータス | ボディ                                                                         |
| ------------------------- | ---------- | ------------------------------------------------------------------------------ |
| ゲスト `GET /invoices`    | `401`      | `permission denied for table invoices`、`GRANT SELECT ... TO anon`のヒント付き |
| ユーザーA `GET /invoices` | `403`      | 同じ内容で、`authenticated`向けのヒント付き                                    |

そのため、アプリがブラウザから読むテーブルすべてで、最初にやることはこれです。

```sql
alter table public.todos enable row level security;
grant select, insert, update, delete on public.todos to authenticated;
```

ブラウザが必要とするコマンドだけを、ゲストに読ませる必要がない限り`authenticated`にだけ付与してください。

## ユーザーに自分の行だけを見せるには？

コマンドごとに1つのポリシーを、それぞれサインイン済みのユーザーに限定して作ります。

```sql
create policy "read own" on public.todos
  for select to authenticated using ((select auth.uid()) = user_id);
create policy "add own" on public.todos
  for insert to authenticated with check ((select auth.uid()) = user_id);
create policy "edit own" on public.todos
  for update to authenticated
  using ((select auth.uid()) = user_id) with check ((select auth.uid()) = user_id);
create policy "delete own" on public.todos
  for delete to authenticated using ((select auth.uid()) = user_id);
```

Aは2行、Bは1行を持っています。返ってきた結果です。

| リクエスト元 | 内容                      | ステータス | ボディ                                       |
| ------------ | ------------------------- | ---------- | -------------------------------------------- |
| ゲスト       | 全件読み取り              | `401`      | permission denied（`anon`にgrantなし）       |
| A            | 全件読み取り              | `200`      | Aの2行                                       |
| A            | Bの行をinsert             | `403`      | `new row violates row-level security policy` |
| A            | Bの行をupdate             | `200`      | `[]`                                         |
| A            | Bの行をdelete             | `200`      | `[]`                                         |
| A            | 自分の行の所有者をBに変更 | `403`      | `new row violates row-level security policy` |

覚えておくべきは、2つの`200 []`の行です。RLSは、見えない行へのupdateやdeleteを拒否しません。除外するだけで、変更された行がゼロでも成功として扱われます。アプリが「エラーなし」を「保存できた」とみなしていると、実際には何も起きていないのに、ユーザーに編集できたと伝えてしまいます。変更された行を返してもらい（supabase-jsでは`.select()`、HTTPでは`Prefer: return=representation`）、1行返ってきたことを確認してください。

## updateポリシーになぜWITH CHECKが必要？

`using`は、ユーザーが触れる既存の行を選びます。`with check`は、その行が変更後にどんな状態であってよいかを決めます。同じupdateポリシーの3つのバージョンを比べました。

| updateポリシー                                     | Aが自分の行をBに渡す                         |
| -------------------------------------------------- | -------------------------------------------- |
| `using (uid = user_id) with check (uid = user_id)` | `403`、拒否                                  |
| `using (uid = user_id)`、`with check`なし          | 拒否（Postgresは`using`をcheckとして再利用） |
| `using (uid = user_id) with check (true)`          | `200`、行はBのものに                         |

`with check`を省くのは見た目より安全です。Postgresが`using`の式にフォールバックするからです。ユーザーが行を他人のアカウントへ移せてしまうのは、一部のジェネレーターが「updateを許可する」ために書く`with check (true)`のほうです。checkは省略せずに書いてください。

## プロフィールを公開しつつ、所有者だけが編集できるようにするには？

```sql
create policy "public read" on public.profiles
  for select to anon, authenticated using (true);
create policy "own edit" on public.profiles
  for update to authenticated
  using ((select auth.uid()) = id) with check ((select auth.uid()) = id);
```

素のPostgresでの実行では、ゲストは2つのプロフィールを読め、ゲストのupdateは0行を変更し、Aがフィルターなしで実行した`update profiles set display_name = 'x'`は、Aの行だけのちょうど1行を変更しました。公開するつもりのデータへのselectなら、`using (true)`で問題ありません。ほかのテーブルでは、RLSなしと同じです。また、`to`句なしで書いたポリシーはゲストにも適用されます。私のゲストは、`to`のない`using (true)`ポリシーを通じて「非公開」の行を読めました。

## チーム内で行を共有するには？

よくある形は、`memberships`テーブルと、そこからユーザーのチームを引くデータ側のポリシーです。

```sql
create policy "members read" on public.projects
  for select to authenticated
  using (org_id in (select org_id from public.memberships
                    where user_id = (select auth.uid())));
```

このポリシー自体は問題ありません。漏洩はその隣にありました。`memberships`には、多くのチュートリアルが勧める「ユーザーは自分のメンバーシップをinsertできる」というポリシーを付けました。

```sql
create policy "join any org" on public.memberships
  for insert to authenticated with check (user_id = (select auth.uid()));
```

これは行が誰のものかを確認するだけで、どのチームかは確認しません。会社1のメンバーであるAが、リクエストを1つ送りました。

![membershipsのinsertポリシー。会社1のユーザーAが、REST API経由で実行。ポリシー: user_id = auth.uid()：ユーザーA → 会社2に参加 → 201 Created → Bの給与を閲覧、insertポリシーなし：ユーザーA → 会社2に参加 → 拒否 → 自社のみ。](https://madsalt.tech/charts/ja/supabase-rls-policy-examples-join.svg)

*図1. ローカルのSupabaseスタック（CLI 2.109.1）での私のテスト、2026年10月24日。*

| Aのリクエスト                                   | ステータス | ボディ                   |
| ----------------------------------------------- | ---------- | ------------------------ |
| プロジェクトを読む                              | `200`      | `A roadmap`              |
| メンバーシップ`{user_id: A, org_id: 2}`をinsert | `201`      | 作成された               |
| もう一度プロジェクトを読む                      | `200`      | `A roadmap`、`B payroll` |

対策は、`memberships`にinsertポリシーを一切付けず、ブラウザから書き込めなくすることです（素のPostgresの実行では、同じinsertは`42501`で拒否されました）。メンバーの追加は、サーバー側、または招待を確認する関数の中で動く招待フローで行います。ユーザーが2つの会社に所属する場合が次の落とし穴で、それは[2つの組織に属すユーザーで起きる漏洩](https://madsalt.tech/ja/notes/supabase-rls-user-in-two-organizations/)にまとめました。

## ポリシーが自分自身のテーブルを読む必要があるときは？

「メンバーは同じチームの仲間を見られる」は、`memberships`を読む`memberships`上のポリシーを意味します。Postgresはクエリの実行時に拒否します。

```text
ERROR 42P17: infinite recursion detected in policy for relation "members2"
```

検索を、所有者として動く関数に移します。APIに公開されていないスキーマに置き、`search_path`を固定します。

```sql
create schema private;
create function private.user_org_ids() returns setof int
  language sql stable security definer set search_path = ''
  as $$ select org_id from public.members2 where user_id = (select auth.uid()) $$;
revoke all on function private.user_org_ids() from public;
grant usage on schema private to authenticated;
grant execute on function private.user_org_ids() to authenticated;

create policy "see teammates" on public.members2
  for select to authenticated using (org_id in (select private.user_org_ids()));
```

その後、Aは会社1の2人のメンバーを見られ、Bは自分だけが見えました。関数は`public`に置かないでください。そこにあるものは何でもAPI経由で呼び出せます。

## 機密性の高い行にMFAを必須にするには？

通常のポリシーに加えて、restrictiveポリシーを追加します。restrictiveポリシーは`and`で結合されるので、両方を通過する必要があります。

```sql
create policy "mfa only" on public.payouts as restrictive
  for select to authenticated using ((select auth.jwt() ->> 'aal') = 'aal2');
```

同じユーザーで、`aal1`のセッションは支払いを0件、`aal2`のセッションは1件読みました。

## RLSをオンにしてポリシーを置かないテーブルは？

サーバーだけが触るテーブルです。RLSがオンでポリシーがなければ、ブラウザのロールにはすべて拒否されます。素のPostgresの実行では、ゲストもAも0行を読み、Aのinsertは`42501`で拒否されました。RLSをオフのままにするのは、その逆です。RLSがオフでgrantがデフォルトのテーブルを、公開キーのみ、サインインなしで読んだ結果です。

| ゲストのリクエスト      | ステータス | ボディ              |
| ----------------------- | ---------- | ------------------- |
| `GET /leads`            | `200`      | 2行とも             |
| `DELETE /leads?id=gt.0` | `200`      | 2行とも、削除された |

Advisorの警告「RLS disabled in public」が指しているのは、これです。

## 1つのポリシーで複数のコマンドをカバーできる？

リストでは書けません。`for update, delete`は構文エラー（`42601: syntax error at or near ","`）になりますが、公開されているスニペット集には今も載っています。`for all`は有効で、1つの`using`と1つの`with check`をすべてのコマンドに適用しますが、それが望みどおりになることはまれです。`using`付きのinsertポリシーも失敗します。`only WITH CHECK expression allowed for INSERT`。

## ポリシーでクエリは遅くなる？

遅くなることがあります。ポリシーの式は、Postgresが対象にする行ごとに実行されます。1,000ユーザーが持つ200,000行（Aは200行）で、ユーザーAとして`select * from big`を計測しました（7回の中央値、Postgres 17.10）。

![所有者専用ポリシー、4通りの書き方。200,000行のうち自分の200行を読むのにかかったミリ秒。7回の中央値。auth.uid() = user_id、インデックスなし：156.30、(select auth.uid())、インデックスなし：7.40、auth.uid() = user_id、インデックスあり：0.21、(select auth.uid())、インデックスあり：0.05。](https://madsalt.tech/charts/ja/supabase-rls-policy-examples.svg)

*図2. Supabaseのロールを作り直したローカルのPostgres 17.10での私のテスト、2026年10月24日。*

| ポリシーとインデックス                            | 中央値   |
| ------------------------------------------------- | -------- |
| `auth.uid() = user_id`、インデックスなし          | 156.3 ms |
| `(select auth.uid()) = user_id`、インデックスなし | 7.4 ms   |
| `auth.uid() = user_id`、`user_id`にインデックス   | 0.21 ms  |
| `(select auth.uid()) = user_id`、インデックスあり | 0.05 ms  |

呼び出しを`select`で包むと、Postgresは行ごとではなくクエリごとに1回だけ計算できます。残りはインデックスが担います。Supabaseの[RLSパフォーマンスガイド](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv)も、自社の100,000行のテーブルで同じ桁の変化（179 msから9 ms）を報告しています。多くのブログ記事がその数字を自分のものとして引用していますが、上の数字は私の実行によるものです。

## ロールはどこから取るべき？

`app_metadata`か自分のテーブルからです。`user_metadata`からは決して取らないでください。サインイン済みのユーザーは`supabase.auth.updateUser()`で自分の`user_metadata`を変更できるため、`auth.jwt() -> 'user_metadata' ->> 'role'`を確認するポリシーでは、自分でそのロールを付与できてしまいます。サインアップ時のメタデータからロールをコピーするサインアップトリガーも同じです。そのケースは[登録だけでOwnerに](https://madsalt.tech/ja/notes/supabase-raw-user-meta-data-role/)にあります。

## 自分のポリシーをどう確認する？

- 無料の[Supabaseセキュリティチェック](https://madsalt.tech/ja/tools/supabase-security-check/)は、プロジェクトを読み取り専用で読み、常に真になるポリシー、ゲスト向けの書き込みポリシー、ユーザーのメタデータを信用するポリシー、RLSがオフのテーブルを指摘し、それぞれに修正用のSQLを付けます。
- どの行が誰のものかを知っているスキャナーはありません。それには、この記事のように2人のユーザーとしてリクエストを実行します。ロールのあるアプリ向けの完全な方法は[Supabase RLSのテスト方法](https://madsalt.tech/ja/blog/how-to-test-supabase-rls/)にあります。
- Supabaseが守ってくれる範囲と、あなたに任される範囲は[Supabaseは安全？](https://madsalt.tech/ja/blog/is-supabase-secure/)にあります。
- 所有者とチームの同じテストをAppwriteでも実行した結果は、[Appwrite vs Supabaseの安全性：同じ攻撃で比較](https://madsalt.tech/ja/blog/appwrite-vs-supabase/)にあります。

**制限。** ローカルのスタック1つとノートPC1台、架空のデータ、2026年10月24日。タイミングはハードウェアとテーブルの形で変わります。ミリ秒より比率のほうが重要です。StorageのポリシーとRealtimeはここでは扱いません。

ポリシーが守れているか不安ですか？

スキーマを一緒に見ることができれば嬉しいです。売り込みはありません。

[お問い合わせ](https://madsalt.tech/ja/#contact)
