ブログへ戻る

SupabaseのRLSが動かない——Unified Logsを使ったデバッグ手順

認証は成功しているのにクエリが空を返す、INSERTがポリシー違反で弾かれる。こうした問題を、Auth・PostgREST・Postgresの3層にまたがってSupabase Unified Logsで追跡し、RLS失敗の5つの原因を特定・修正する手順を説明する。

認証は成功している。ユーザーはSupabase Authに存在する。それなのにデータベースのクエリが空の配列を返す——あるいはINSERTがnew row violates row-level security policyで失敗する。

こうした問題で難しいのは、多くの場合、修正内容そのものではない。失敗がAuth・PostgREST・PostgreSQLのどの層で起きたのかを特定することだ。

Supabaseの旧来のログ画面では、Auth・API・Postgresの各ログページを行き来しながらタイムスタンプを目で合わせる必要があった。2026年7月16日にオープンベータとなったUnified Logsは、全サービスのログを1つの検索画面に統合する。この変更が最も効果を発揮するのが、こうした複数層にまたがる障害のデバッグだ。

Unified Logsがもたらす変化

RLSで失敗したリクエストは、通常3つの層を通過する。

  1. Auth — JWTは有効か。ユーザーは認証されているか。
  2. PostgREST — リクエストはどのPostgreSQLロールとして実行されたか。
  3. Postgres — どのポリシーが操作を拒否したか。

Unified Logsは、APIゲートウェイ・Postgres・Auth・Storage・PostgREST・Realtime・コネクションプーラーのログを1つのビューに集約する。サービス・ログレベル・ステータスコード・HTTPメソッド・パス名でフィルタリングでき、それらを自由に組み合わせられる。タイムラインのヒストグラムはレベル別に色分けされ、エラーの急増が一目でわかる。

RLSのデバッグにとっての実質的なメリットは、失敗が発生した時刻前後の時間範囲を設定してerrorレベルでフィルタするだけで、Auth・PostgREST・Postgresの各イベントを時系列順に1つの画面で確認できる点にある。

RLS失敗を再現する最小構成

最も多いRLS失敗のパターンは、Row Level Securityを有効にしてSELECTポリシーを書いた後、INSERTポリシーを追加せずにINSERTを試みるというものだ。最小の再現手順は次のとおりだ。

create table posts (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null,
  title text not null
);

alter table posts enable row level security;

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

この状態では、認証済みユーザーが自分の行をSELECTすることはできる。しかしINSERTを試みると失敗する。supabase-js v2.111.0では、クライアントが受け取るエラーは次のとおりだ。

const { error } = await supabase
  .from('posts')
  .insert({ user_id: session.user.id, title: 'Hello' })

// error.code    → '42501'
// error.message → 'new row violates row-level security policy for table "posts"'

これは期待どおりの動作だ。RLSポリシーは操作ごとに独立している。SELECTポリシーはINSERTの許可を与えない。RLSが有効でINSERTポリシーが存在しないテーブルでは、Postgresがどのユーザーのリクエストであっても全INSERTをブロックする。

エラーコード42501はPostgreSQLのinsufficient_privilegeに対応する標準コードだ。クライアントのエラーオブジェクトとPostgresログの両方に現れるため、Unified Logsでサービスをまたいで同一リクエストを特定するためのアンカーとして使える。

Unified Logsで失敗を読む

エラーを再現したら、次の順序で確認する。

  1. クライアントのエラーオブジェクトまたはブラウザのネットワークタブから失敗したリクエストのタイムスタンプを確認する。
  2. SupabaseプロジェクトのダッシュボードでLogsを開く。
  3. Unified Logsの時間範囲を失敗前後に設定する。
  4. Sourcesフィルタで対象ログを絞り込むか、全ソースを表示したままseverity_text = 'error'でフィルタする。event_messageフィールドにrow-level security42501を検索してもよい。
  5. 各サービスのエントリを確認する。

auth_logs: 失敗したリクエストの前にサインインイベントが記録されているか確認する。Authではユーザーが認証済みに見えているのにリクエストが失敗している場合、問題はポリシーまたはロールの割り当てにある。

edge_logs(APIゲートウェイ / PostgREST): HTTPリクエストのメソッド・パス・ステータスコードと、リクエストが実行されたPostgreSQLロールが確認できる。ロールがauthenticatedではなくanonになっている場合、JWTが渡されていない、不正なJWTが使われている、またはトークンが期限切れになっている。ブロックされたINSERTはHTTP 403を返す。

postgres_logs: RLSエラーが記録される層だ。event_messageフィールドにブロックされたステートメントとエラーコード42501insufficient_privilege)、ポリシー違反メッセージが記録される。このエントリのタイムスタンプと同じリクエストのedge_logsエントリのタイムスタンプはほぼ一致するはずで、それが同一リクエストを特定するアンカーになる。

Unified LogsのLiveボタンを使うと、バグを再現しながらリアルタイムでイベントが流れてくる様子を確認できる。後から検索するよりも直感的にデバッグできる。

注: Unified Logsはこの記事の作成時点(2026年7月29日)でオープンベータである。正式リリースまでに画面やフィルタのフィールド名が変わる可能性がある。

RLS失敗の5つの原因

原因1:リクエストが認証されていない

最も根本的な失敗で、データベースがリクエストをanonロールとして受け取るケースだ。有効なJWTが含まれていない。

ログでの確認: PostgRESTのエントリにロールとしてanonが表示される。対応するサインインイベントがAuthログの同じ時間帯に存在しない。

代表的な原因:

  • サービスロールキーでSupabaseクライアントを初期化していてauth.getSession()がnullを返す
  • アクセストークンが期限切れになる前にリフレッシュされていない
  • サーバーサイドクライアントとブラウザクライアントを混同している——サーバーサイドクライアントはユーザーのセッションを自動的に引き継がない

原因2:auth.uid()がnullになる

ユーザーがサインインしていても、データベース側でauth.uid()がnullを返すことがある。JWTがPostgresに正しく転送されていない場合に発生する。

SQLではnull = user_idは常にfalseになる。つまりauth.uid() = user_idと書かれたポリシーは、auth.uid()がnullのときすべてのアクセスを無音でブロックする。未認証リクエストと、ログイン済みユーザーが自分のものではない行を参照したケースが、結果として同じに見える。

Postgresログにクエリが実行されたが結果がゼロ件(SELECT)またはポリシー違反(INSERT)と記録されている場合、auth.uid()が実際に解決されているか確認する。Supabaseの SQL Editorで試せる。

select auth.uid();

ユーザーのJWTを渡すクライアントから実行する。SQL Editorではnullになるが、アプリ上でサインイン済みのユーザーからは正しいUUIDが返ってくる場合、JWTがPostgresに届いていない。

原因3:対象操作のポリシーが存在しない

RLSポリシーは操作ごとに独立している。for selectで定義したポリシーはINSERT・UPDATE・DELETEをカバーしない。

各操作に必要な句を整理すると次のとおりだ。

| 操作 | 必要な句 | |------------|-------------------------------| | SELECT | USING | | INSERT | WITH CHECK | | UPDATE | USING + WITH CHECK(かつSELECTポリシーも必要)| | DELETE | USING |

SELECTポリシーしかない状態でINSERTを試みると、正しいユーザーが有効なJWTを持っていても、テーブルレベルのRLSブロックが発動してポリシー違反エラーが返る。

原因4:INSERTのuser_idauth.uid()と一致しない

with check (auth.uid() = user_id)というポリシーは、認証済みユーザーのIDと、INSERTされる行のuser_idの値を比較する。クライアントが別のUUIDや空文字列を送信した場合、比較が失敗する。

よく見られるケース:

  • クライアントがINSERTペイロードを手動で構築し、プレースホルダーを送信している
  • user_idフィールドをJWTのsub claimとは異なるデータソースから取得している
  • フォームやAPI層がUUIDのフォーマットを変換している

Postgresログでブロックされたステートメントを確認し、user_idの値がポリシーの評価時に使われるauth.uid()と一致しているか照合する。

原因5:サービスロールでテストしていた

サービスロールはRLSを完全にバイパスする。開発中にサービスロールを使って実装とテストを進めると、RLSの失敗は発生しない。実際のユーザーがアプリにアクセスして初めて問題が表面化する。

「テストでは動いた」という報告の多くはこのパターンだ。サービスロールは管理用の操作のためのものであり、全ポリシーをスキップするのが意図された動作だ。

Unified Logsでは、サービスロールを使ったリクエストは異なるロール名で表示される。アプリがユーザーのセッションJWTを使うべき場所でサービスロールを使っている場合、全ての実ユーザーリクエストがテストとは異なる挙動をとる。

ポリシーを修正する

上記の再現手順で欠けているINSERTポリシーは次のように追加する。

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

with check句が、新しい行のuser_idが認証済みユーザーのIDと一致するかを検証する。これがなければPostgresはINSERTをブロックする。

(select auth.uid())という形式——関数呼び出しをサブセレクトでラップする——はSupabaseが推奨するパターンだ。行ごとに関数を呼び出す代わりに、PostgreSQLがステートメント単位で結果をキャッシュできるようになり、行数が多いテーブルでは顕著なパフォーマンス改善が得られる。

ユーザーが自分の投稿を更新できるようにする場合は、UPDATEポリシーも追加する。UPDATEにはUSING(対象となる既存行の条件)とWITH CHECK(更新後の行の状態の条件)の両方が必要で、SELECTポリシーも存在している必要がある。

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

ログで修正を確認する

INSERTポリシーを追加した後、同じリクエストを再度実行する。Unified Logsで確認する内容:

  • Authログに対象ユーザーのサインイン成功が記録されている。
  • PostgRESTログにINSERTリクエストのステータスとして201が記録されている。
  • Postgresログにそのリクエストのエラーがない——42501もポリシー違反メッセージも消えている。

修正後にログを確認する意義は2つある。ポリシーの変更が正しく機能したことの確認と、成功したクロスサービスリクエストがログ上でどう見えるかのベースライン取得だ。このベースラインは、後で別の障害が発生したときの比較対象になる。

デバッグチェックリスト

SupabaseのRLSリクエストが失敗したとき:

□ リクエストに有効で期限切れでないアクセストークンが含まれているか
□ PostgRESTログのロールが "authenticated" になっているか
□ auth.uid() が期待するUUID(nullでない)を返しているか
□ 対象テーブルにRLSが有効になっているか
□ 実行しようとした操作(SELECT / INSERT / UPDATE / DELETE)のポリシーが存在するか
□ INSERT の場合、ポリシーに WITH CHECK が使われているか
□ UPDATE の場合、SELECT ポリシーも存在しているか
□ INSERT ペイロードの user_id が auth.uid() と一致しているか
□ クライアントがサービスロールキーではなく anon キーを使っているか
□ Auth・PostgREST・Postgres の各ログが同じ時間帯を指しているか

このリストをUnified Logsの1画面で確認できる——別のAuthやPostgresのログセクションへ移動せずに——というのが、この機能がこの種の問題にもたらす具体的な改善だ。