メインコンテンツへスキップ

Supabase RLS完全ガイド|データベース層で守る鉄壁のセキュリティ設計

··2098 文字·5 分
Supabase(PostgreSQL)をバックエンドに採用する際、最も重要でありながら、最も「正しく理解されていない」機能が RLS(Row Level Security) です。

従来の開発スタイルでは、APIサーバー側で「このユーザーはこのデータを閲覧できるか」をIF文で制御していましたが、Supabase(BaaS)ではその役割を データベース自身 が担います。この記事では、RLSの仕組みから、実務で安心して使えるポリシー設計のベストプラクティス、そしてパフォーマンス最適化までを徹底解説します。


1. RLS(行レベルセキュリティ)の本質
#

RLSは、PostgreSQLのテーブルに対して 「特定の条件を満たす行(レコード)だけを可視化・操作可能にする」 セキュリティ機能です。

なぜ「APIサーバー」ではなく「DB」で守るのか?
#

従来のAPIサーバーによるバリデーションと、Supabase RLSによるデータベース層でのセキュリティの違いを比較した図
図:従来のアーキテクチャ vs Supabase RLS

Supabaseの最大の特徴は、フロントエンド(Next.js等)から直接データベースへクエリを投げられる点にあります。ここでAPIサーバーによる仲介がないため、「誰でもすべてのデータが見えてしまうのではないか?」という懸念が生じます。

RLSは、そのクエリが実行される データベース層の直前 で動作します。

  1. ユーザーがリクエストを送信。
  2. PostgreSQLが「このユーザーに許可されたデータはどれか?」をRLSポリシーに照らし合わせる。
  3. 許可された行だけ を取得対象として扱い、他は存在しないものとして無視される。

つまり、開発者が誤って「全件取得」のクエリをフロントエンドで書いてしまっても、RLSが有効であれば「ログイン中の本人のデータ」しか返ってこないのです。これが「最後の防壁」と呼ばれる理由です。


2. RLS導入の基本:USING と WITH CHECK の違い
#

ポリシーを定義する際、USINGWITH CHECK の使い分けが混乱の元になります。

  • USING (参照制御):
    • 既存の行を 読み取る(SELECT) または 削除する(DELETE) 際に適用される条件。
    • 条件に合わない行は、SELECTクエリでは「存在しないもの」として扱われ、DELETEでは無視されます。
  • WITH CHECK (更新制御):
    • 新しく 作成する(INSERT) または 更新する(UPDATE) 後のデータが満たすべき条件。
    • 条件を満たさないデータを保存しようとすると、エラーが発生します。
操作適用される条件
SELECTUSING
INSERTWITH CHECK
UPDATEUSING (更新前) + WITH CHECK (更新後)
DELETEUSING

3. 実務で多用するポリシー設計パターン
#

パターンA:自分自身のデータのみ管理
#

-- 読み取り
CREATE POLICY "Users can view their own profiles"
ON profiles FOR SELECT
TO authenticated
USING ( auth.uid() = id );

-- 追加
CREATE POLICY "Users can create their own profile"
ON profiles FOR INSERT
TO authenticated
WITH CHECK ( auth.uid() = id );

パターンB:公開・非公開フラグによる制御
#

-- 公開記事は全員、非公開は本人のみ
CREATE POLICY "Anyone can view public posts, authors can view private"
ON posts FOR SELECT
USING ( is_public = true OR auth.uid() = author_id );

4. 【中級編】パフォーマンスを落とさない設計
#

RLSはクエリのたびに評価されるため、複雑な結合(JOIN)を含むポリシーはパフォーマンスを著しく低下させます。

❌ アンチパターン:ポリシー内でのサブクエリ
#

-- データの件数が増えるほど指数関数的に遅くなる可能性がある
USING (
  EXISTS (
    SELECT 1 FROM team_members 
    WHERE team_id = posts.team_id AND user_id = auth.uid()
  )
);

✅ 解決策1:Security Definer関数の利用
#

複雑な条件判定は、PostgreSQLの関数として定義し、その関数の結果をポリシーで利用するのが定石です。SECURITY DEFINER を使うことで、一時的に高い権限でチェックを実行し、結果だけを返せます。

✅ 解決策2:JWT(カスタムクレーム)の活用
#

特定のユーザーが「管理者(admin)」かどうかを判定する場合、毎回DBを引くのではなく、Supabase AuthのJWTに is_admin などのフラグを含めておき、auth.jwt() ->> 'is_admin' で参照すると高速です。


5. 重要:service_role キーと RLS の関係
#

Supabaseには、全てのRLSをバイパスできる強力な service_role キーが存在します。

  • クライアント(ブラウザ): 必ず anon キーを使い、RLSを有効にする。
  • Edge Functions / サーバーサイド: service_role キーを使い、複雑なバリデーションや管理者操作を行う。
注意

service_role キーを決してフロントエンドのコード(.env.local 等)に含めないでください。漏洩した場合、データベース内の全てのデータが読み書き可能になってしまいます。


まとめ:RLSは「後付け」できない
#

Supabaseでの開発において、セキュリティの主役は常にデータベース(RLS)であるべきです。

  1. テーブルを作ったら即座に ENABLE RLS
  2. 「Default Deny(デフォルト拒否)」 の原則に従い、許可するものだけを足していく。
  3. USINGWITH CHECK の違いを正しく理解し、更新時のバリデーションを怠らない。

このフローを徹底するだけで、あなたのサービスは「堅牢な要塞」へと変わります。「API側で守っているから大丈夫」という慢心を捨て、データベース自身に自身のセキュリティを守らせる──これこそが、モダンなBaaS開発における正解です。


📘 関連記事
#

🔗 参考リンク
#

著者
ゆーふー
Web開発、インフラ、AI技術に興味があるエンジニアです。日々の学びを記録しています。

関連記事

👤 運営者プロフィール

運営者プロフィール画像

ゆーふー

メガベンチャーで働く現役Webエンジニア(歴約2年)。
フロントエンドからインフラ構築、セキュリティ対策まで、実務で得た「現場のリアルな技術知見」を発信しています。