---
title: "Supabase RLSの設定手順とAuth連携ポリシー設計｜有効化漏れの検知まで"
url: "https://www.issoh.co.jp/tech/details/17056/"
published: 2026-08-27
updated: 2026-09-04
categories: ["データベース"]
publisher: "株式会社一創"
---

# Supabase RLSの設定手順とAuth連携ポリシー設計｜有効化漏れの検知まで

Supabaseはブラウザやモバイルアプリから PostgreSQL へ直接リクエストが飛ぶ構成のため、テーブルに行レベルセキュリティを掛け忘れると、その時点で全行が読み書き可能になります。この記事では、`alter table` による有効化と権限の絞り込み、`auth.uid()` を使ったポリシーの書き方、SELECT と INSERT で必要な句が変わる理由、anon・authenticated・service\_role の権限境界、JWT クレームでロール判定を行う手順、pgTAP でポリシーを自動テストする流れ、そして有効化漏れを機械的に見つける方法までを実装順に並べました。各節にはそのまま貼って動かせる SQL と、根拠になる公式ドキュメントの該当ページを添えています。DB 製品ごとの一般的な RLS 設定は別記事に委ね、Supabase Auth と結びつく部分だけを、2026年9月時点の公式ドキュメントに沿って扱います。

## まとめ：Supabase RLS設定の要点とポリシー設計の判断基準

Supabase での RLS 導入は、テーブルごとに3つの操作を順に済ませれば形になります。`enable row level security` で有効化し、`revoke` と `grant` でロールに渡す操作を絞り、操作別に `create policy` を書く。ポリシー本体は `(select auth.uid()) = user_id` という比較が出発点で、ここに `to authenticated` を必ず添えます。この3段は[Supabase公式のRow Level Securityガイド](https://supabase.com/docs/guides/database/postgres/row-level-security)がそのまま並べて提示している最小構成です。

設計の分かれ目は、認可の材料がどこにあるかです。所有者 ID の一致だけで判定できるならポリシーはDBに置いたほうが安全側に倒れます。ロールや権限表を引く必要があるなら JWT のカスタムクレームに載せ、それでも足りず外部システムの状態が判断材料に入るなら、RLS に寄せずサーバー側へ認可を集約する。有効化漏れは目視ではなく Security Advisor の lint（0013・0007・0008）で拾い、動作確認は SQL エディタではなくロールを切り替えた状態で行ってください。

作業の順番としては、先に `supabase start` でローカルに同じスタックを立て、マイグレーションに有効化を含め、pgTAP のテストで anon と authenticated の許可・拒否を固定してから本番へ push する流れが手戻りを減らします。性能面は公式のトラブルシューティング記事に実測値が載っており、ヘルパー関数を `select` で包むだけで 178,000ms が 12ms に落ちた例、参照列へのインデックス追加で 171ms が 0.1ms 未満になった例が示されています。遅いのは RLS そのものではなく、ポリシーの書き方だと考えて手を入れる順序を決めてください。

## Supabaseのクライアント直結構成でRLSが唯一の防壁になる理由

RLS を「あると望ましい設定」と捉えるか「無ければ穴が開く設定」と捉えるかは、アーキテクチャで決まります。Supabase は後者です。

### Data API経由のリクエストがanonロールとして実行される仕組み

supabase-js から発行したクエリは、PostgREST を通って PostgreSQL に届きます。このときリクエストに載せた API 鍵が Postgres のロールに対応づけられ、未ログインなら `anon`、ログイン済みなら `authenticated` として SQL が実行される仕組みです。[Postgresのロールに関する公式ドキュメント](https://supabase.com/docs/guides/database/postgres/roles)と[API鍵のページ](https://supabase.com/docs/guides/getting-started/api-keys)が、鍵とロールの対応表を明示しています。ログイン済みリクエストではアクセストークンの `sub` クレームが取り出され、`auth.uid()` がその値を返します。アプリケーションサーバーを挟まない以上、接続元を信用する前提は置けません。Supabase 全体の構成やFirebaseとの設計思想の差は[SupabaseとFirebaseの違いを整理した比較記事](https://www.issoh.co.jp/column/details/3002/)で扱っています。

### RLS未設定のテーブルが全件読み書き可能になるまでの経路と対策

public スキーマのテーブルは既定で Data API に露出します。RLS が無効なら行を絞る評価そのものが走らないため、ロールに `grant` された操作はそのまま全行へ通る構造です。Supabase の Database Linter はこの状態を lint 0013「rls\_disabled\_in\_public」として報告し、[Advisorsのドキュメント](https://supabase.com/docs/guides/observability/advisors)では「公開テーブルでRLSが無効で、誰でもCRUDできる」という説明が付いています。テーブル追加のたびに有効化を忘れないよう、マイグレーションの雛形に `enable row level security` を含めておくと事故が減ります。なお[Supabase CLIの宣言的スキーマとdb diffの制約](https://www.issoh.co.jp/tech/details/17063/)のとおり、`db diff` はポリシー文を検出しないため、ポリシーは手書きのマイグレーションで管理してください。

### 汎用のRLS解説とSupabase固有の設定で扱う範囲の切り分け

ポリシーの概念やSQL Server・Oracle での設定手順といった製品横断の話題は[Row Level Securityの仕組みとDB別の設定方法](https://www.issoh.co.jp/tech/details/5442/)にまとめてあります。本記事が扱うのは、`auth` スキーマのヘルパー関数、API 鍵とロールの対応、ダッシュボード側の検知機能という、Supabase でしか出てこない部分だけです。リアルタイム配信側の認可、つまり `realtime.messages` へのポリシーや private channel の扱いは[Supabase Realtimeの実装｜3方式の使い分けとRLS併用・同時接続の上限](https://www.issoh.co.jp/tech/details/17058/)で扱っています。

## supabase CLIでローカルに検証環境を立ててRLSを試す手順

ポリシーを本番プロジェクトのダッシュボードで直接書き始めると、失敗の巻き戻しに手間がかかります。同じスタックを手元に立ててから始めてください。

### Dockerを用意してsupabase initからstartまでを実行する

[ローカル開発の公式ガイド](https://supabase.com/docs/guides/local-development/cli/getting-started)は、ローカルスタックが Docker コンテナ上で動くため先にコンテナランタイムを入れるよう求めています。`npx` や npm 経由で CLI を動かす場合の要件は Node.js 20 以降。CLI 本体は [supabase/cli](https://github.com/supabase/cli) の GitHub Releases で配布され、安定版は 2.116系（2026年9月時点）です。プロジェクト依存として入れたなら、コマンドは `npx supabase` の形で呼びます。

```
npm install supabase --save-dev
npx supabase init
npx supabase start
npx supabase status
```

`start` が完了すると、Postgres・Auth・PostgREST・Studio が一式立ち上がり、`status` がローカルの接続情報と鍵を表示します。ここで返る鍵はローカル専用のものなので、本番の鍵と混ぜないようファイルを分けておくのが安全側の運用です。ダッシュボードでの試行と違い、壊しても `npx supabase stop` と `start` で作り直せます。

### マイグレーションにRLS有効化を含めてdb resetで再現する

ポリシーは `supabase/migrations` 配下の SQL に手書きで置き、`npx supabase db reset` でローカルDBを作り直して当たり方を見ます。reset はマイグレーションを先頭から流し直すため、「有効化を書き忘れたテーブルが混ざっていないか」を毎回同じ条件で確かめられる仕組みです。本番への反映は[マイグレーションの公式ガイド](https://supabase.com/docs/guides/deployment/database-migrations)の手順に沿って行い、`db push` の前にローカルの reset を通す順序を崩さないでください。宣言的スキーマとの併用時の注意は[Supabase CLIの宣言的スキーマとdb diffの制約](https://www.issoh.co.jp/tech/details/17063/)に整理しています。

## alter tableとgrantでRLSを有効化しポリシーを作成する手順

公式ガイドが示す最小構成は、有効化・権限付与・ポリシー作成の3段です。順番を守ると、途中で意図せず公開状態になる時間が生まれません。

### alter tableでの有効化とgrantを絞る2段構えの実行手順

まず対象テーブルで RLS を立て、続けて anon と authenticated から一度権限を剥がし、必要な操作だけを渡し直します。公式ガイドはこの3行を並べて提示しています。所有者本人だけに読ませる SELECT ポリシーまでを一続きにすると、次の形です。

```
alter table public.reports enable row level security;

revoke all on table public.reports from anon, authenticated;

grant select, insert, update, delete
  on table public.reports to authenticated;

create policy "Users can view their own reports."
on public.reports
for select
to authenticated
using ( (select auth.uid()) = user_id );
```

RLS と GRANT は別のレイヤで働きます。RLS は GRANT を緩める方向には作用しないので、そもそも渡していない操作はポリシーの書き方に関係なく拒否される設計です。ロールとスキーマ公開範囲の設計そのものについては[PostgreSQLの権限設計と公開範囲の決め方](https://www.issoh.co.jp/tech/details/17002/)が参考になります。

### auth.uid()を使ったSELECTポリシーの最小構成と記述上の注意

公式ガイドの例は `using ( (select auth.uid()) = user_id )` という比較で、`to authenticated` を添えた形になっています。`auth.uid()` は未認証リクエストで null を返し、null との比較結果は真にならないため、匿名アクセスは自動的に落ちる仕組みです。ただし null を前提にした条件を書くと意図が反転するため、公式も null を明示的に扱うよう求めています。`to authenticated` を付けておけば、公式の説明どおり anon ロールのリクエストではその段で評価が止まり、ポリシー式そのものが走りません。

ここで `auth.uid()` を裸で書かず `(select auth.uid())` と包む理由は性能です。包むとプランナが initPlan を作って結果を文単位でキャッシュするため、候補行ごとの再評価が消えます。書き方の差がどれくらい効くかは後段の実測の節で扱います。

### ポリシーが1本も無いテーブルで発生する全件拒否という挙動の確認方法

RLS を有効にしただけでポリシーを書かないと、そのテーブルは Data API から一切読めなくなります。既定が拒否なので、許可はポリシーで足していく形です。Linter はこれを lint 0008「rls\_enabled\_no\_policy」として報告します。逆に、ポリシーだけ書いて有効化を忘れた状態は lint 0007「policy\_exists\_rls\_disabled」で拾われます。この2つは対になっているため、両方を監視対象に入れてください。lint の定義そのものは [supabase/splinter](https://github.com/supabase/splinter) リポジトリで SQL として公開されており、どの条件で発火するかを読んで確かめられます。

## 操作別ポリシー設計におけるUSING句とWITH CHECK句の使い分け

SELECT 用のポリシーを1本書いただけで安心してしまう例が多いのですが、書き込み側は別の句を要求します。

### SELECT・INSERT・UPDATE・DELETEで必要になる句の対応関係

[PostgreSQL公式のCREATE POLICY仕様](https://www.postgresql.org/docs/current/sql-createpolicy.html)には「Policies Applied by Command Type」という表があり、操作ごとに使える句が決まっています。

| 操作     | USING   | WITH CHECK |
| ------ | ------- | ---------- |
| SELECT | 読める行を絞る | 指定できない     |
| INSERT | 指定できない  | 新規行を検査する   |
| UPDATE | 対象行を絞る  | 更新後の行を検査   |
| DELETE | 対象行を絞る  | 指定できない     |

UPDATE で `with check` を省いた場合、公式仕様の記述どおり PostgreSQL は `using` の式を新規行の検査にも流用します。省略しても穴は開きませんが、読める条件と書ける条件を別にしたいとき、たとえば閲覧は同一チーム・更新は本人のみという設計では両方の条件を明示して区別する必要がある設計です。所有者本人だけに書き込みを許す3本は、次の形で揃えられます。

```
create policy "Users can insert their own reports."
on public.reports
for insert
to authenticated
with check ( (select auth.uid()) = user_id );

create policy "Users can update their own reports."
on public.reports
for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );

create policy "Users can delete their own reports."
on public.reports
for delete
to authenticated
using ( (select auth.uid()) = user_id );
```

### insert後のRETURNINGでエラーになる典型パターンと対処の順序

supabase-js で `.insert()` に `.select()` を続けると、SQL としては RETURNING 付きの INSERT になります。

```
const { data, error } = await supabase
  .from('reports')
  .insert({ title: 'Q3 report', user_id: user.id })
  .select();
```

PostgreSQL の公式仕様は、RETURNING を伴う更新系クエリについて「対象リレーションの SELECT 権限が必要で、挿入・更新された行がリレーションの SELECT ポリシーを満たさない場合はエラーが送出される（返すべき行が黙って無視されることはない）」と明記しています。黙って空配列が返るのではなく、明確に失敗する。INSERT ポリシーだけ用意して「行は入るのにレスポンスがエラーになる」と悩む場合、足りないのは SELECT ポリシーです。

### permissiveポリシーの重複がOR結合で権限を広げてしまう問題

既定の permissive ポリシーは、公式仕様が「適用可能な permissive ポリシーは論理 OR で結合される」と書いているとおり、同じ操作に複数あると許可が足し算になります。1本ずつは正しくても、合成結果は最も緩いポリシーまで許可が広がる。制限を重ねたいときは `as restrictive` を使い、こちらは論理 AND で結合される側に載せます。重複した permissive は lint 0006「multiple\_permissive\_policies」として検知されます。

### as restrictiveで多要素認証済みのセッションだけに更新を許す

公式 RLS ガイドの例は、`as restrictive` の UPDATE ポリシーで認証保証レベルのクレームを条件にし、二要素認証を通ったセッションだけに更新を許す構成です。permissive 側の「本人なら更新できる」という許可に、restrictive 側の「MFA を通っていること」を AND で重ねる形になります。

```
create policy "Restrict updates."
on public.profiles
as restrictive
for update
to authenticated
using ( (select auth.jwt()->>'aal') = 'aal2' );
```

restrictive は許可を足さず、既存の許可を削る方向にしか働きません。監査要件で「この操作だけは追加の条件を必ず通す」と決まっているものは、permissive 側の条件式に混ぜ込まず restrictive として独立させたほうが、後からポリシーが増えても条件が崩れません。

## anon・authenticated・service\_roleの境界と新API鍵への移行

ロールの理解を飛ばすと、RLS を書いても効かない経路を自分で作ってしまいます。

### 3つのロールごとに異なるRLS適用可否と鍵の置き場所の区別設計

Supabase が用意する3ロールの性格は次のように分かれます。

- `anon`：未ログインのリクエスト。RLS が適用される
- `authenticated`：ログイン済みのリクエスト。RLS が適用される
- `service_role`：BYPASSRLS 属性を持ち、RLS を通り抜ける。サーバー側に置く

[APIの保護に関する公式ドキュメント](https://supabase.com/docs/guides/api/securing-your-api)は service\_role について、RLS をバイパスするのでサーバーサイドに留めるよう明記しています。バイパスするロールは追加もでき、`alter role "role_name" with bypassrls;` で付与する構文です。増やすほど検査点が増えるため、既定の3つで足りるなら足さないほうが管理は楽になります。このロールがヘッダでどう切り替わり、自動生成APIと関数呼び出しにどう効くかは[Supabase 自動生成API（PostgREST）とRPCの使い分け](https://www.issoh.co.jp/tech/details/17070/)で扱っています。

### sb\_publishableとsb\_secretへ移行する時期と対象範囲の整理

API 鍵の形式は新旧2系統が並走しています。新形式は公開用の `sb_publishable_` と秘匿用の `sb_secret_` で、[公式のAPI鍵ドキュメント](https://supabase.com/docs/guides/getting-started/api-keys)は従来の anon と service\_role という JWT 形式の鍵を2026年末までに非推奨とする旨を案内しています（2026年9月時点）。対応関係は単純で、publishable 鍵は未ログイン時に anon、ログイン時に authenticated として動き、secret 鍵は service\_role として RLS をバイパスします。

移行作業で見落としやすいのは失効の扱いです。同ドキュメントは、新しい鍵を作ってもレガシー鍵は失効せず、ダッシュボードの Settings 内の API Keys で無効化する別手順が要る、と書いています。つまり「新鍵に差し替えた」だけでは旧鍵が生きたまま残る。配布先の棚卸しを終えてから無効化まで進めてください。App Router 側でこの鍵を置く場所と認可を二重化する手順は[Supabase Next.js連携の実装とRLS前提の権限設計](https://www.issoh.co.jp/tech/details/17065/)、鍵の取得場所とローテーションの可否は[Supabase APIキーの管理を扱った記事](https://www.issoh.co.jp/tech/details/17068/)で整理しています。

### service\_roleを使ってよい処理と使うべきでない処理の線引き

管理者向けのバッチ、外部 Webhook の受信、他システムとのデータ同期は service\_role で構いません。これらはユーザー文脈を持たない処理だからです。一方、ログイン中のユーザーの操作を代行する目的で service\_role を使うのは見送るべき設計になります。理由は明快で、その瞬間に認可の判断がアプリケーションコードへ全て移り、RLS が保険として働かなくなるから。実装を急ぐ場面でも、ここだけは妥協しないほうが後の調査コストは下がります。関数側で secret 鍵を扱う場合の環境変数の受け取り方と管理手順は、[Supabase Edge Functionsの実行環境とシークレット管理](https://www.issoh.co.jp/tech/details/17061/)で扱っています。

## JWTクレームによる権限判定とuser\_metadataを認可に使わない理由

所有者 ID の比較で足りるうちは単純ですが、ロールや権限表が絡むと保存場所の選定が効いてきます。

### user\_metadataがクライアント側から書き換え可能という事実

Supabase Auth のユーザーレコードには user\_metadata と app\_metadata があります。[ユーザーデータの管理に関する公式ドキュメント](https://supabase.com/docs/guides/auth/managing-user-data)のとおり、前者はクライアントの `updateUser` から本人が書き換えられる領域です。ここにロールを持たせてポリシーで読むと、利用者が自分で管理者を名乗れてしまいます。認可の材料に使ってよいのは、クライアントから書き換えられない app\_metadata かカスタムクレーム側だけ。

この取り違えは通常の動作確認では表面化しません。ただし機械では拾えます。Advisors には lint 0015「rls\_references\_user\_metadata」があり、ユーザー自身が編集できるメタデータをポリシーが参照している状態を指摘します。設計レビューで潰すのが本筋ですが、検知の網は張っておいてください。関連して lint 0012「auth\_allow\_anonymous\_sign\_ins」は、匿名サインインを有効にしたプロジェクトで匿名ユーザーが authenticated ロールと同じポリシーを共有してしまう状態を指摘します。匿名サインインを使うなら、`to authenticated` だけでは絞り切れないと考えて条件を足す必要があります。

### Custom Access Token Hookでロールをクレームへ載せる実装手順

[公式のRBACガイド](https://supabase.com/docs/guides/api/custom-claims-and-role-based-access-control-rbac)は、トークン発行前に走る Custom Access Token Hook で JWT を編集し、ロールをクレームとして埋め込む方式を示しています。ガイドが載せている関数はそのまま動く形で、`user_roles` テーブルからロールを引いてクレームへ書き足す作りです。

```
create or replace function public.custom_access_token_hook(event jsonb)
returns jsonb
language plpgsql
stable
as $$
  declare
    claims jsonb;
    user_role public.app_role;
  begin
    select role into user_role from public.user_roles
      where user_id = (event->>'user_id')::uuid;
    claims := event->'claims';
    if user_role is not null then
      claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
    else
      claims := jsonb_set(claims, '{user_role}', 'null');
    end if;
    event := jsonb_set(event, '{claims}', claims);
    return event;
  end;
$$;
```

ロールと権限の型は列挙型で定義し、`app_role` と `app_permission` を先に作ってから `user_roles` と `role_permissions` を並べます。フックを登録したあとに発行されたトークンだけがクレームを持つため、既存セッションには載りません。この点は次の節で扱います。

### authorize関数を作ってポリシーから権限表を引く実装の手順

権限表を引く判定はポリシー式に直接書かず、security definer 関数に閉じ込めます。公式ガイドの `authorize` 関数は、JWT のクレームからロールを取り、`role_permissions` を数えて真偽を返す作りです。`stable security definer set search_path = ''` を付けるところまでがガイドの書き方です。

```
create or replace function public.authorize(
  requested_permission app_permission
)
returns boolean as $$
declare
  bind_permissions int;
  user_role public.app_role;
begin
  select (auth.jwt() ->> 'user_role')::public.app_role into user_role;
  select count(*) into bind_permissions
    from public.role_permissions
    where role_permissions.permission = requested_permission
      and role_permissions.role = user_role;
  return bind_permissions > 0;
end;
$$ language plpgsql stable security definer set search_path = '';

create policy "Allow authorized delete access"
on public.channels
for delete
to authenticated
using ( (select authorize('channels.delete')) );
```

ポリシーから関数を呼ぶ形にすると、権限ロジックが1箇所にまとまり、テーブルが増えても書き換え範囲は広がりません。呼び出しを `(select authorize(...))` と包んでいるのも意図があって、行ごとの再評価を避けるためです。

### JWTが古い状態のまま残る前提での権限変更の反映設計と更新手順

クレーム方式には反映遅れが付いてきます。ロールを剥奪しても、発行済みアクセストークンの有効期限が切れるまでは古いクレームが残る。即時の剥奪が要件に入るなら、クレーム参照ではなくポリシー内で権限表を join する方式へ切り替えるか、セッション失効の操作を併用してください。JWT が発行されるまでの流れとセッションの既定値は[Supabase Authの構成とJWT発行・セッション設定を扱った記事](https://www.issoh.co.jp/tech/details/17245/)にまとめています。ここは性能と即時性のトレードオフで、監査要件がある業務システムほど join 側に倒す判断が妥当になります。

## RLS有効化漏れの検知手順とポリシーが効いているかの検証方法

設定した本人が目視で確認する運用は、テーブルが20を超えたあたりで破綻します。機械で拾う仕組みに寄せてください。

### Security Advisorのlintで拾えるRLS関連の指摘とその意味

Studio のプロジェクトから Advisors を開くと、Security Advisor と Performance Advisor の2つのビューに指摘が並びます。重大度は ERROR・WARN・INFO の3段（2026年9月時点）。RLS まわりで押さえるべきものは次の9つです。

- 0013 rls\_disabled\_in\_public：public のテーブルが RLS 無効
- 0007 policy\_exists\_rls\_disabled：ポリシーはあるが有効化されていない
- 0008 rls\_enabled\_no\_policy：有効だがポリシーが無く全件拒否
- 0010 security\_definer\_view：ビューが RLS を迂回している
- 0002 auth\_users\_exposed：ビュー越しに auth.users が露出
- 0006 multiple\_permissive\_policies：permissive の重複で許可が拡大
- 0003 auth\_rls\_initplan：ヘルパー関数が行ごとに評価され低速
- 0012 auth\_allow\_anonymous\_sign\_ins：匿名ユーザーが同じポリシーを共有
- 0015 rls\_references\_user\_metadata：編集可能なメタデータを参照

上位3つは有効化まわり、0010 と 0002 は迂回経路、0006 と 0003 は重複と性能、0012 と 0015 は認可材料の取り違えを指す指摘です。判定は決定的なので、CI から取得して継続監視に載せられます。リリース前に一度通し、指摘ゼロを条件にするだけで、掛け忘れによる事故はほぼ止まります。

### SQLエディタでの確認が素通りしてしまう理由と正しい試し方の検証手順

ダッシュボードの SQL エディタで `select` を叩いても、RLS の効きは確かめられません。エディタは特権ロールで実行されるうえ、[PostgreSQL公式の行セキュリティのページ](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)が書いているとおり、テーブル所有者は既定で RLS を迂回するからです。検証したいときは、同一セッション内でロールとクレームを差し替えます。公式のトラブルシューティング記事も、計測時にこの手順を使っています。

```
set session role authenticated;
set request.jwt.claims to '{"role":"authenticated","sub":"5950b438-b07c-4012-8190-6ce79e4bd8e5"}';

explain analyze select count(*) from public.reports;

reset role;
reset request.jwt.claims;
```

所有者経由の迂回まで塞ぎたいテーブルには `alter table public.reports force row level security;` という選択肢もあります。ORM から接続すると所有者としてつながるためポリシーが素通りします。専用ロールでの手当ては[SupabaseのORM連携（Prisma・Drizzle）の記事](https://www.issoh.co.jp/tech/details/17072/)にまとめました。

### pgTAPでポリシーのテストを書きsupabase test dbで回す手順

手で `set session role` を打つ検証は、テーブルとポリシーが増えると回しきれません。[データベーステストの公式ガイド](https://supabase.com/docs/guides/database/testing)は pgTAP を使い、`supabase/tests/database` 配下に SQL のテストを置いて `supabase test db` で実行する方式を示しています。テストは `begin` と `rollback` で囲まれるため、データを汚さずに許可と拒否をアサートできます。

```
begin;
select plan(3);

select has_table('public', 'reports', 'reports table should exist');

select policies_are(
  'public', 'reports',
  array['Users can view their own reports.',
        'Users can insert their own reports.',
        'Users can update their own reports.']
);

set local role authenticated;
set local request.jwt.claims to '{"sub":"00000000-0000-0000-0000-000000000001"}';

select is_empty(
  'select id from public.reports where user_id <> auth.uid()',
  'rows owned by other users must be hidden'
);

select * from finish();
rollback;
```

実行は `npx supabase test db` です。[pgTAP拡張のドキュメント](https://supabase.com/docs/guides/database/extensions/pgtap)には `has_policy` や `policies_are` といったポリシー向けのアサーションが載っているので、「このテーブルに想定した名前のポリシーが揃っているか」を機械で固定できます。anon で読めないこと、他人の行が見えないこと、この2本だけでも入れておけば、後任によるポリシーの書き換えを検知できる構成です。テストをどこで走らせるかは[テスト全体像のガイド](https://supabase.com/docs/guides/local-development/testing/overview)が整理しており、DB 層の pgTAP とアプリ層の E2E を役割で分ける前提になっています。

## ポリシーの遅さをEXPLAIN ANALYZEで測って書き換える手順

RLS が遅いという話の多くは、ポリシーの書き方の問題です。公式のトラブルシューティング記事には書き換え前後の実測値が並んでいるので、当て推量ではなく数字で判断できます。

### auth.uid()をselectで包んでinitPlanにするときの実測差

[RLSの性能とベストプラクティスの公式記事](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv)は、ヘルパー関数を `select` で包む書き換えについて、178,000ms が 12ms になったテスト結果を示しています。裸で `auth.uid()` や自作の判定関数を呼ぶと候補行ごとに評価が走り、包めばプランナが initPlan を作って文単位でキャッシュする。同記事は、行データによって結果が変わらない関数にだけ適用できる、という条件も添えています。

もう1つ大きいのが security definer 関数への切り出しで、他テーブルの RLS をバイパスする関数に判定を寄せたテストでは 11,000ms が 7ms になったと記載されています。桁が変わる種類の改善なので、遅いポリシーを見つけたら先にこの2つを当ててください。

### ポリシー参照列へのインデックス追加とjoinの向きを変える書き換え

同記事はインデックスについて、主キーやユニーク制約が付いていない参照列に索引を張る例（171ms が 0.1ms 未満、大きなテーブルで100倍以上の改善）を挙げています。join を含むポリシーは条件の向きを反転させ、9,000ms が 20ms になった例も示されています。

```
create index reports_user_id_idx
  on public.reports using btree (user_id);

create policy "Team members can read reports."
on public.reports
for select
to authenticated
using (
  team_id in (
    select team_id from public.team_user
    where user_id = (select auth.uid())
  )
);
```

書き換えの要点は、`auth.uid()` を join 先の巨大なテーブルへ当てるのではなく、対象テーブルの列を副問い合わせの結果で絞る向きにすることです。加えて `to` でロールを明示すると、anon からのアクセスで 170ms が 0.1ms 未満になったテストも同記事に載っています。ポリシー評価の前に段が止まるので、指定を省く理由はありません。共有基盤で特定テナントの負荷が他へ波及する現象は[ノイジーネイバー問題の検知と対策](https://www.issoh.co.jp/tech/details/7732/)で扱いました。

## RLSに寄せず認可をサーバー側へ集約すべき3つの条件と判断基準

RLS は万能ではありません。次の条件に当てはまるなら、DB にポリシーを積むのは見送り、Edge Function やアプリケーションサーバーに secret 鍵を置いて認可を集約する構成を選んでください。第一に、テナントが3階層以上に入れ子で、毎回の判定に複数テーブルの join が必要な場合。第二に、権限が外部 IdP や基幹システムの状態で決まり、DB 内の値だけでは判定しきれない場合。第三に、主要な処理が複数テーブル横断の集計で、ポリシー評価が実行時間の大半を占める場合です。

この3つに該当しないなら、RLS をDB側に置いたほうが安全側に倒れます。集約側へ寄せる判断をしたときも、テーブルの RLS を無効に戻すのは避けてください。有効化したまま authenticated への grant を外し、secret 鍵の経路だけを通す形にしておけば、将来クライアント直結の画面を1枚足したときに全公開へ戻る事故が起きません。会員ごとのデータ分離を含む設計と実装をまとめて任せたい場合は[会員管理システム開発](https://www.issoh.co.jp/service/system/usersystem/)でご相談ください。

## よくある質問

Supabase の RLS 設定でつまずきやすい論点を、実装時の判断が分かれる順に取り上げます。

### SupabaseではRLSを必ず有効にする必要がありますか？

public スキーマに置き Data API から触るテーブルは、有効にしてください。Supabase の Database Linter が lint 0013「rls\_disabled\_in\_public」を報告するのは、無効なテーブルが実質的に全公開になるからです。API へ露出させたくないテーブルは、public 以外のスキーマに置いて Data API の公開スキーマから外す方法もあります。

### auth.uid()がnullになるのはどのようなときですか？

未認証のリクエスト、つまり anon ロールで実行されたときです。`auth.uid()` はアクセストークンの `sub` クレームから値を取るため、トークンが無ければ返せる ID がありません。`(select auth.uid()) = user_id` の比較は null 相手だと真にならず、匿名アクセスは結果的に落ちます。

### service\_roleの鍵をフロントエンドに置いてはいけない理由は何ですか？

service\_role は BYPASSRLS 属性を持ち、ポリシーを通り抜けて全行にアクセスできるロールだからです。ブラウザに配布した時点で、閲覧者はどのテーブルの全行も読み書きできる状態になります。新形式では `sb_secret_` が同じ役割を担い、こちらもサーバー専用です。公開してよいのは `sb_publishable_`、従来の anon 鍵にあたるほうだけと覚えてください。

### RLSを有効にするとクエリは遅くなりますか？

書き方次第で桁が変わります。ヘルパー関数を裸で呼ぶと候補行ごとに評価が走り、公式のトラブルシューティング記事が示すテスト結果は 178,000ms という値まで悪化したものです。`(select auth.uid())` の形に包んで 12ms、参照列にインデックスを張って 0.1ms 未満という改善例も同じ記事に並びます。遅いポリシーは Performance Advisor が lint 0003 で検知します。

### ポリシーを書いたのにデータが返らないときは何を確認しますか？

確認順序は3段です。まず `grant` で該当ロールに操作を渡しているか。RLS は GRANT を緩めないため、権限が無ければポリシーと関係なく拒否されます。次に操作と句の対応で、INSERT に `using` を書いていないか、RETURNING を使うのに SELECT ポリシーが無いか。最後にロールの取り違えで、`to authenticated` のポリシーへ anon で来ていないかを見ます。

### ローカルでRLSを試すには何を用意すればよいですか？

コンテナランタイムと Node.js 20 以降、それに supabase CLI の3つです。`npx supabase init` と `npx supabase start` でローカルに同じスタックが立ち、`npx supabase status` がローカル専用の鍵を返します。ポリシーはマイグレーションに書いて `npx supabase db reset` で流し直し、当たり方を毎回同じ条件で見る運用にしてください。

### ポリシーが意図どおり効いているかを自動で確かめられますか？

pgTAP でテストを書き、`supabase test db` で回せます。テストは `supabase/tests/database` 配下の SQL として置き、`policies_are` でポリシーの揃いを、`set local role` と `is_empty` で他人の行が見えないことをアサートする形です。CI に載せておくと、後からポリシーを書き換えたときに気づける状態を保てます。

## 関連記事

- [Row Level Security（RLS）とは？行単位アクセス制御の仕組みとDB別の設定方法を解説](https://www.issoh.co.jp/tech/details/5442/)：製品別の設定手順と RLS そのものの概念を扱っています。
- [SupabaseとFirebaseの違いを徹底比較｜料金・認証・どっちを選ぶか](https://www.issoh.co.jp/column/details/3002/)：Supabase Auth の機能範囲と Firebase との設計思想の差を整理しています。
- [PostgreSQL MCPサーバーの構成と権限設計｜読み取り専用ロールとスキーマ公開範囲の決め方](https://www.issoh.co.jp/tech/details/17002/)：RLS の土台になるロール分割と公開範囲の設計を扱っています。
- [ノイジーネイバー問題とは？原因・検知・対策をわかりやすく解説](https://www.issoh.co.jp/tech/details/7732/)：特定テナントの負荷が他へ波及する現象と検知手段をまとめています。
- [pgmqとは？PostgreSQLをキュー化する拡張の使い方とSupabase連携](https://www.issoh.co.jp/tech/details/4240/)：Supabase 上で PostgreSQL 拡張を組む別事例として参照できます。

---

出典: [Supabase RLSの設定手順とAuth連携ポリシー設計｜有効化漏れの検知まで](<https://www.issoh.co.jp/tech/details/17056/>)（株式会社一創）
