[Supabase] INSERT/UPSERTで403エラーが出た話(RLSポリシー不足の解決方法)

問題:Supabaseで403(Forbidden)になりINSERTできなかった

Supabase(supabase-js)からテーブルにupsertしようとしたところ、HTTP 403(Forbidden)となり登録できなかった。

ブラウザのネットワークタブでは次のようにPOST /rest/v1/...が403で失敗していた。

POST https://<project>.supabase.co/rest/v1/public.onidazo?on_conflict=... 403 (Forbidden)

レスポンスボディは以下の通り。

{
  "code": "42501",
  "details": null,
  "hint": null,
  "message": "new row violates row-level security policy for table \"onidazo\""
}

この記事では、このエラーの原因と解決方法を紹介する。

環境

  • Supabase(Postgres)
  • supabase-js
  • Next.js(フロントエンド)
  • RLS(Row Level Security)有効

原因:RLSポリシーが不足していた

このエラーは、テーブルに RLS(Row Level Security / 行レベルセキュリティ) が有効になっている状態で、実行ロール(anon / authenticated など)に対してINSERT(またはUPSERTに伴うUPDATE)を許可するポリシーが存在しない、またはWITH CHECK条件を満たしていないときに発生する。

今回のポイントは次の通り。

  • Authorization(Bearer token)とセッションは存在していた
  • それでもnew row violates row-level security policyで拒否されていた

つまり、認証が無いのではなく、「そのユーザーで書き込んでよい」というルールがDB側に定義されていないことが原因だった

実装手順

1. 現状のRLSポリシーを確認

まず、SupabaseのSQL Editorpg_policiesを見ると、そのテーブルに設定されているポリシー一覧と条件(USING / WITH CHECK)を確認できる。

select
  schemaname,
  tablename,
  policyname,
  roles,
  cmd,
  qual as using_expression,
  with_check as with_check_expression
from pg_policies
where schemaname = 'public'
  and tablename  = 'onidazo'
order by policyname;

実行結果:

schemaname tablename policyname roles cmd using_expression with_check_expression
public onidazo onidazo_select {authenticated} SELECT true null
public onidazo migration_sync_all_rw {migration_sync} ALL true true

この結果から、authenticatedロールにはSELECTしか許可されておらず、INSERTUPDATEのポリシーが存在しないことが分かった。

ALL{migration_sync}ロール専用(to migration_sync)で、SELECT/INSERT/UPDATE/DELETEすべての操作を許可するポリシーである。

重要: RLSポリシーはロールごとに設定される。フロントエンド(supabase-js)からのアクセスはanon(未認証)またはauthenticated(認証済み)ロールで実行されるため、migration_syncロールのALLポリシーは適用されない。

そのため、authenticated向けのINSERTUPDATEポリシーが無い状態では、フロントからの登録は403になる。

ロールとは?

ロール(Role) は、PostgreSQLにおける「誰がアクセスしているか」を識別するための権限グループのこと。Supabaseでは以下のロールが自動的に用意されている:

  • anon(anonymous): 未認証のユーザー。supabase.createClient()で作成したクライアントがログインせずにAPIを呼び出す際に使用される
  • authenticated: 認証済みのユーザー。signIn()などでログイン後、JWTトークンを持っている状態で使用される
  • service_role: Supabaseのサービスキーを使った管理者レベルのアクセス。RLSをバイパスできる
  • migration_sync: マイグレーション用の特殊ロール(プロジェクトによって異なる)

RLSポリシーは「どのロールに対して、どの操作を許可するか」を定義するため、フロントエンドから呼び出す場合はanonまたはauthenticatedロール向けのポリシーが必須となる。

2. RLS(Row Level Security)について

RLS(Row Level Security)は、PostgreSQLのテーブル単位で行レベルのアクセス制御を行う仕組みである。

RLSの特徴:

  • テーブルに対してRLSを有効化すると、デフォルトですべての操作が拒否される
  • 操作を許可するには、明示的にポリシーを定義する必要がある
  • ポリシーはロールごとに設定される(anonauthenticatedなど)

ポリシーの種類:

コマンド 説明 使用する条件
SELECT データの読み取り USING
INSERT データの挿入 WITH CHECK
UPDATE データの更新 USING(既存行の参照)、WITH CHECK(更新後の検証)
DELETE データの削除 USING

USINGWITH CHECK の違い:

  • USING: 既存の行を参照できるかどうかを判定する条件(SELECT、UPDATE、DELETEで使用)
  • WITH CHECK: 新しい行や更新後の行が条件を満たすかを検証(INSERT、UPDATEで使用)

Supabaseでは、フロントエンドからのアクセスはanon(未認証)またはauthenticated(認証済み)ロールで実行されるため、これらのロール向けにポリシーを設定する必要がある。

3. マイグレーションでINSERT/UPDATEポリシーを追加

upsertは内部的にINSERTまたはUPDATEになるため、基本的には両方のポリシーを用意しておくのが安全である。

今回は、Supabase CLIを使ってマイグレーションファイルとして管理する方法で実装した。

3-1. マイグレーションファイルの作成

Supabase CLIでマイグレーションファイルを作成した。

supabase migration new add_onidazo_policies

このコマンドにより、supabase/migrations/ディレクトリに<timestamp>_add_onidazo_policies.sqlというファイルが作成された。

3-2. ポリシーの定義

作成されたマイグレーションファイル(例:supabase/migrations/20251217000000_add_onidazo_policies.sql)に、以下のポリシーを定義した。

-- onidazo RLS policies
-- INSERT を許可
create policy "onidazo_insert_authenticated"
on public.onidazo
for insert
to authenticated
with check (auth.uid() is not null);
 
-- UPDATE を許可(UPSERT の更新分)
create policy "onidazo_update_authenticated"
on public.onidazo
for update
to authenticated
using (auth.uid() is not null)
with check (auth.uid() is not null);

ポリシーの内容:

  • for insert to authenticated: authenticatedロール(認証済みユーザー)のINSERT操作を対象
  • with check (auth.uid() is not null): JWTトークンにユーザーIDが含まれている(=ログイン済み)ことを条件とする
  • for update to authenticated: authenticatedロールのUPDATE操作を対象
  • using (auth.uid() is not null): 既存行を参照する際の条件
  • with check (auth.uid() is not null): 更新後の行が条件を満たすかを検証

auth.uid()はSupabaseが提供する関数で、JWTトークンからユーザーIDを取得する。ログイン済みであればnullにならない。

3-3. マイグレーションの適用

マイグレーションファイルを作成後、以下のコマンドでデータベースに適用した。

supabase db push

これにより、本番環境(またはリモート環境)のデータベースに新しいRLSポリシーが追加された。

3-4. ポリシーの追加を確認

マイグレーション適用後、再度pg_policiesを確認して、ポリシーが正しく追加されていることを確認した。

select
  schemaname,
  tablename,
  policyname,
  roles,
  cmd,
  qual as using_expression,
  with_check as with_check_expression
from pg_policies
where schemaname = 'public'
  and tablename  = 'onidazo'
order by policyname;

実行結果:

schemaname tablename policyname roles cmd using_expression with_check_expression
public onidazo onidazo_insert_authenticated {authenticated} INSERT null true
public onidazo onidazo_select {authenticated} SELECT true null
public onidazo onidazo_update_authenticated {authenticated} UPDATE true true
public onidazo migration_sync_all_rw {migration_sync} ALL true true

onidazo_insert_authenticatedonidazo_update_authenticatedが追加され、authenticatedロールでINSERTUPDATEが可能になったことを確認した。

動作確認

ポリシーを追加後、フロントエンドから再度upsertを実行したところ、正常に登録できるようになった

ブラウザのネットワークタブでPOST /rest/v1/public.onidazo201 Createdまたは200 OKで成功することを確認した。

まとめ

  • 403でも「認証が無い」とは限らない(RLSで拒否されることがある
  • new row violates row-level security policyRLSポリシー不足/条件不一致のサイン
  • upsertの場合はINSERTUPDATE両方のポリシーを用意すると安全
  • 変更はマイグレーションとしてSQL管理しておくと追跡しやすい

参考リンク