Author

Web系ソフトウェアエンジニアを志望
開発記録や個人的な技術に対する思いを綴ります

FOCUS個人開発/設計
OSArch Linux+Omarchy
STACKTypeScript・Next.js
STATUS学習中 → 就活中

SupabaseのRLSチートシート

tech

はじめに

この記事では、SupabaseのRLSの設定・書き方について解説します。

RLSはSupabaseを利用するうえで最重要項目といっても過言ではないほど重要です。

Supabase初学者だけでなく、経験者の理解を深めるためにもぜひご覧ください。

出典: Supabase 公式ドキュメント - Row Level Security


基本構文

CREATE POLICY "ポリシー名"
  ON テーブル名
  FOR SELECT | INSERT | UPDATE | DELETE | ALL  -- 操作の種類
  TO authenticated | anon | public             -- 対象ロール
  USING (既存行への条件)                        -- SELECT / UPDATE / DELETE に使う
  WITH CHECK (新規行への条件);                  -- INSERT / UPDATE に使う

ポイント

  • USING → 操作する行の条件を指定(どの既存業を対象に選ぶか)
  • WITH CHECK → 追加・更新した行の条件を指定(書き込んだ行が条件を満たしているか)
  • USING / WITH CHECKを使用できない操作に対して指定した場合、マイグレーション適用時にエラーが出ます。
  • すべての操作に対して許可を与えたい場合にのみFOR ALLを使う。ポリシーの設定が面倒くさいからFOR ALLはNG
  • 大文字・小文字の区別はありません。""で囲む場合を除き、create policyCREATE POLICYも同じものとして扱われます。

anon / authenticated / service_role の違い

ロール説明RLS
anon未ログインユーザー(公開APIアクセス)適用される
authenticatedログイン済みユーザー適用される
service_roleサーバー・管理者用(Secret キー)バイパスされる

service_role キーは絶対にフロントエンドコードに含めない authenticatedはログイン済みかどうかの判定を行うだけなので、これ単体ではログイン済みの他ユーザーのデータにもアクセス可能。別途USING (auth.uid() = user_id)などが必要


Step 0: RLSを有効化する

-- テーブル作成後に必ず有効化する
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

注意: RLS を有効化してポリシーが何もない状態 = 全行が誰にも見えない(deny all)


よく使うヘルパー関数

関数返り値用途
auth.uid()UUIDログイン中のユーザーID
auth.jwt()JSONJWTの中身(role・メタデータなど)
auth.jwt() ->> 'role'textJWTのroleを取得
auth.jwt()->>'email'textユーザーのメールアドレス

パターン集

1 全員に読み取りを許可(公開データ)

CREATE POLICY "anyone can read"
  ON posts FOR SELECT
  TO public
  USING (true);

2 ログインユーザーだけが自分のデータを見られる

CREATE POLICY "users can view own data"
  ON posts FOR SELECT
  TO authenticated
  USING (auth.uid() = user_id);

3 ログインユーザーが自分のデータを作成できる

CREATE POLICY "users can insert own data"
  ON posts FOR INSERT
  TO authenticated
  WITH CHECK (auth.uid() = user_id);

4 ログインユーザーが自分のデータを更新できる

CREATE POLICY "users can update own data"
  ON posts FOR UPDATE
  TO authenticated
  USING (auth.uid() = user_id)           -- 更新対象の行を確認
  WITH CHECK (auth.uid() = user_id);     -- 更新後の値を確認

注意: UPDATE には対応する SELECT ポリシーも必要


5 ログインユーザーが自分のデータを削除できる

CREATE POLICY "users can delete own data"
  ON posts FOR DELETE
  TO authenticated
  USING (auth.uid() = user_id);

6 CRUD をまとめて書く(シンプルな自己所有パターン)

-- 上記①〜⑤のセット例(posts テーブル)
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

CREATE POLICY "select own posts"   ON posts FOR SELECT   TO authenticated USING (auth.uid() = user_id);
CREATE POLICY "insert own posts"   ON posts FOR INSERT   TO authenticated WITH CHECK (auth.uid() = user_id);
CREATE POLICY "update own posts"   ON posts FOR UPDATE   TO authenticated USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
CREATE POLICY "delete own posts"   ON posts FOR DELETE   TO authenticated USING (auth.uid() = user_id);

7 公開記事 or 自分の記事を読める

CREATE POLICY "read public or own posts"
  ON posts FOR SELECT
  TO authenticated
  USING (
    published = true
    OR auth.uid() = user_id
  );

8 JWTのカスタムクレームでロール判定(管理者パターン)

-- auth.jwt() の app_metadata に { "role": "admin" } が入っている場合
CREATE POLICY "admins can read all"
  ON posts FOR SELECT
  TO authenticated
  USING (
    (auth.jwt() ->> 'role') = 'admin'
  );

raw_app_meta_data はユーザーが変更できないため、認可データの保存場所として適切


9 マルチテナント(組織ごとにデータ分離)

CREATE POLICY "tenant isolation"
  ON tenant_data FOR ALL
  TO authenticated
  USING (
    tenant_id = (
      SELECT tenant_id FROM users WHERE users.id = auth.uid()
    )
  )
  WITH CHECK (
    tenant_id = (
      SELECT tenant_id FROM users WHERE users.id = auth.uid()
    )
  );

パフォーマンスのベストプラクティス

auth.uid() はキャッシュする

-- ❌ 毎行評価される(遅い)
USING (auth.uid() = user_id)

-- ✅ initPlan でキャッシュされる(速い)
USING ((SELECT auth.uid()) = user_id)

1回のクエリ実行時の各行の評価時にサブクエリで書いておくと、その結果をキャッシュ(確保)しておいて以降の処理に使用されるため数万行単位の場合、パフォーマンスの改善が見込める。 特にSELECT * FROM PROJECTSなどの処理において顕著である。

initPlanとはなんぞや

クエリ実行時の最初に一度だけ実行される計画のこと。

クエリ実行時に各行の評価ごとに結果が変わらない値をプランナーが検出すると、その値を算出するサブクエリをinitPlanに組み込みます。

その結果、その値がキャッシュされ各行評価時の算出をスキップするため、パフォーマンスが向上します。

ここでは、auth.uid()はクエリ実行時で固定のためinitPlanに組み込まれます。

ポリシーで使うカラムにインデックスを張る

CREATE INDEX ON posts(user_id);
CREATE INDEX ON tenant_data(tenant_id);

よくあるミス

ミス対処法
RLS を有効化し忘れるマイグレーションと同時に書く習慣にする
ポリシーを1つも作らずに有効化RLS 有効化直後はすべてのリクエストがブロックされる
UPDATE だけ書いて SELECT がないUPDATE には SELECT ポリシーも必要
service_role key をフロントで使うservice_role は RLS を完全バイパスするためサーバー専用
SQL Editor でテストするSQL Editor は RLS をバイパスするのでクライアント SDK でテストする

ポリシーの確認・削除

-- 現在のポリシーを確認
SELECT * FROM pg_policies WHERE tablename = 'posts';

-- ポリシーを削除
DROP POLICY "select own posts" ON posts;

-- RLS を無効化(緊急時のみ)
ALTER TABLE posts DISABLE ROW LEVEL SECURITY;

参考リンク:

おわりに

今まで触ってきたDB類似システムにはRLS設定などなかったため、今回はかなり挑戦の実装でした。 PostgreSQLを扱う際は必ずRLSを設定することを忘れないでください!