Supabaseの「new row violates row-level security policy」をINSERTと返却処理から切り分ける

SupabaseのINSERTでRLS違反が出たとき、権限を広げずに拒否された処理段階とpolicyを切り分けます。

AIものさし編集部

SupabaseでINSERT時に new row violates row-level security policy が出たら、最初に権限を広げるのではなく、どの段階で拒否されたかを分けます。確認順は、実際のDB roleと認証ユーザー、テーブル権限(GRANT)、INSERTのWITH CHECK、upsert時のUPDATE条件、挿入後に行を返すSELECTです。

特にStorageのuploadでは、Supabase公式Troubleshootingが、INSERTポリシーが正しくても、APIがオブジェクト情報を返すSELECT側のRLSポリシー不足で同じ403になる場合を説明しています。一方、すべてのPostgresテーブルで同じ原因とは限りません。エラー文だけで結論を決めず、使ったAPI、要求、DB role、返却設定を確認してください。

この記事は特定のSupabaseプロジェクトを調査・変更した実測記録ではありません。例のpolicyは構造を示す雛形であり、そのまま本番へ適用する前にテーブルの所有者・許可条件・既存grantを確認します。

まず失敗した操作と要求を記録する

同じ画面操作に見えても、内部では通常のINSERT、upsert、INSERT後の返却要求、Storage uploadなど別の処理が動く場合があります。再現した一件について、次を記録してください。JWT、secret、アップロード内容などの機微情報は貼り付けず、必要なら伏字にします。

  • 対象:Postgresのどのschema/tableか、またはStorageのbucketか

  • API:SQL、Supabase JS insert()、upsert()、insert().select()、Storage uploadなど

  • 実行ユーザー:ログイン状態、認証ユーザーID、anon/authenticatedのどちらとして処理されたか

  • 実際のpayload:所有者列、bucket、object path、NOT NULL・unique制約に関係する列

  • HTTP status、Postgres error code/message、Supabase側のrequest IDと時刻

  • 成功した呼び出しとの差:返却の有無、同じユーザーか、同じpathか、同じ環境か

Supabaseの anon roleと「匿名ログインしたユーザー」は同じではありません。Supabase Authの匿名ユーザーは認証済みJWTを持つため、通常は authenticated roleとして扱われます。画面上の「匿名ユーザー」という説明だけから、policyの TO anon が適切だと判断しないでください。

RLSの前にテーブル権限を確認する

Postgresでは、GRANTが「そのroleが操作を実行できるか」を決め、RLS policyが「どの行で操作できるか」を制限します。片方だけ確認しても原因は閉じません。Supabaseの現行ガイドも、公開schemaのテーブルで必要なGRANTと操作別policyの両方を管理するよう説明しています。

特に、権限不足のエラー 42501 は、policy評価より前のGRANT不足を示す場合があります。RLS条件が正しいのに要求が通らないときは、まず実際のroleに対象操作のtable privilegeがあるか、対象テーブルへ期待どおりのgrantが付いているかを確認します。grantがない状態を、policyを広げて直すことはできません。

逆に、GRANTがあるからといって任意の行を挿入できるわけでもありません。公開プロジェクトでは、必要な操作だけを必要なroleへ許可します。Supabaseのservice roleはRLSを迂回するため、クライアントへ公開したり、失敗回避のためにブラウザSDKへ置いたりしないでください。

INSERTはWITH CHECKとpayloadを照合する

INSERT policyは、新しく作られる行が条件を満たすかを WITH CHECK で確認します。たとえば各ユーザーが自分のprofileだけを作れる設計なら、認証ユーザーIDと新規行の所有者列が一致するかが要点です。


create policy "Users can create their own profile"

on public.profiles

for insert

to authenticated

with check ((select auth.uid()) = user_id);

これは構造を示す例です。対象テーブルに user_id があることや、全ユーザーがこの条件でよいことを示すものではありません。自分のschemaへ置き換える前に、所有者列、サーバーが設定する列、auth.uid()の値、JWTのユーザーを照合します。

表は横にスクロールできます

確認点確認方法よくある不一致
role実際のJWTと要求経路を確認signed-out requestなのにauthenticated用policyを想定
auth.uid()認証済み要求で返るユーザーIDを確認未ログイン・異なるアカウント・JWTが切り替わった
所有者列INSERT payloadの値とpolicy条件を比較payloadがnull、別ユーザーID、列名の誤り
INSERT対象schema/tableと対象operationを確認想定外テーブルやviewへの書き込み
table GRANT実行roleに必要なINSERT権限があるか確認policyは存在するがtable privilegeが不足

SQL Editorで管理者として成功したことは、アプリからの認証roleでも成功する証拠にはなりません。管理者やservice roleは通常のクライアントと異なる権限経路を使うため、クライアント要求の確認には本来のroleとJWT条件を使ったテストが必要です。

upsertはUPDATE側の条件も確認する

upsert()は既存行との競合時にUPDATEへ進むことがあります。その場合、INSERTだけを許可していても足りません。競合する行を更新できるかを UPDATE USING で確認し、更新後の行が許可されるかを WITH CHECK で確認します。SupabaseのRLSガイドでは、UPDATEを行うために対応するSELECT policyも必要と説明されています。


create policy "Users can update their own profile"

on public.profiles

for update

to authenticated

using ((select auth.uid()) = user_id)

with check ((select auth.uid()) = user_id);

このpolicyも雛形です。実際には、対象の競合キー、更新できる列、所有者を変更できない条件を確認してください。INSERTの成功だけを試し、同じ要求で競合時のupsertも確認したつもりになると、失敗経路を見落とします。

行の返却を要求しているか調べる

Supabase JavaScriptの insert() は、既定では挿入行を返しません。返却が必要な場合に .select() をチェーンします。そのため、コードに .select() があるか、HTTP要求で返却representationを求めているかを確認します。


// INSERTを行い、挿入行は返さない

const { error } = await supabase

  .from('profiles')

  .insert({ user_id: userId, display_name: displayName })

// INSERT後に行を返す要求

const { data, error } = await supabase

  .from('profiles')

  .insert({ user_id: userId, display_name: displayName })

  .select()

返却要求を付けた場合は、対象roleがSELECTできるか、その新しい行がSELECT policyに合うかを追加確認します。まず同じpayload・同じroleで返却なしと返却ありを比べると、INSERT自体か、行を読む段階かの切り分けになります。ただし実際のAPIやStorageの実装で処理が異なることがあるため、この比較だけで全原因が確定したとは扱いません。

Storage uploadではこの差が特に重要です。Supabaseの公式Troubleshootingは、Storage APIが新規objectのmetadataを返すためINSERTに続けて RETURNING * を実行し、SELECT RLS policyが新しいobjectを読めないと、INSERT policyが正しくてもトランザクション全体が失敗する場合を説明しています。この説明はStorageのuploadに関するものです。通常のPostgresテーブルや別APIでも、同一の内部処理が必ず行われると一般化しないでください。

エラー文を原因ごとに読み分ける

表は横にスクロールできます

観測したこと次に見る箇所判断の注意
INSERTでrow-level policy違反INSERT policyの WITH CHECK、role、payloadの所有者値条件を満たさない新規行が拒否されている可能性。全許可policyへ変えない
SQLSTATE 42501 / permission denied対象roleのGRANTと操作権限GRANT不足はRLS policyを追加しても解消しない
upsertだけ失敗conflict対象、UPDATE USING / WITH CHECK、必要なSELECT policyINSERT成功だけではupsert経路を検証できない
.select()を付けたときだけ失敗SELECT GRANT・SELECT policyと返却対象行Storageでは新規object metadata返却のRLSが原因になる具体例が公式にある
Storage uploadで403と同じ文言storage.objectsのINSERTおよびSELECT条件、bucket/pathSupabase公式Troubleshootingの条件と一致するか確認する
管理者SQLは成功、アプリは失敗アプリのJWT、role、project、payload、client呼び出し管理者の成功をクライアント権限の証明にしない

HTTP statusだけで判断せず、Postgres code、API経路、要求した返却形式を合わせて記録します。ブラウザのnetwork panelやserver logを使う場合も、JWTやAuthorization header、API secret、個人情報を共有しないでください。

安全にpolicyを修正・検証する

原因がINSERT条件にあると確認できても、すぐ USING (true) や WITH CHECK (true) を追加しないでください。まず「誰が、どの行を、どの操作で扱えるべきか」を文章にし、その条件を許可ケースと拒否ケースの両方でテストします。

  1. 開発用またはlocal Supabase環境で、対象tableと現在のgrant/policyを確認する。

  2. 対象roleごとに、正しい所有者のINSERT、別ユーザー所有のINSERT、未ログインINSERTをテストする。

  3. upsertを使うなら、初回INSERTと既存行競合時のUPDATEを別ケースにする。

  4. .select()やStorage uploadがあるなら、作成後に返却・参照される行へのSELECT policyもテストする。

  5. 許可された行が返るだけでなく、許可していないユーザーの読み書きが拒否されることを確認する。

  6. SupabaseのDB test / pgTAPを使い、変更をmigrationとして再現可能にし、テスト結果を保存する。

  7. 本番適用前に、対象データ・ロール・利用中クライアントへの影響とrollback方法をレビューする。

SupabaseのRLSガイドは、select・insert・update・deleteについてanonとauthenticatedの許可・拒否を含むテストを作り、supabase test dbを実行する手順を示しています。必要な操作だけをテストし、service_roleでアプリの検証を代用しないでください。

次にすること

最初の一件で、対象がStorageか通常テーブルか、APIが返却行を要求するか、実行roleとpayloadが何かを確定します。通常テーブルならGRANT → INSERT WITH CHECK → upsert時UPDATE → SELECT/returningの順、Storageならstorage.objectsのpolicyとobject pathに加えて公式TroubleshootingのINSERT/RETURNING説明を照合します。

policyやGRANTを変える前に、許可する主体・行・操作と、拒否すべきケースを明文化してください。原因がまだ特定できない場合は、全許可やservice roleで回避せず、要求・role・error codeを持って管理者へ相談します。Supabaseプロジェクトでの実行や設定変更はこの記事では行っていません。

参考資料


Evidence checked: 2026-10-15. No Supabase project, schema, JWT, request logs, or policy was accessed; the article describes diagnostic steps based on official documentation.