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 Editorでpg_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しか許可されておらず、INSERTやUPDATEのポリシーが存在しないことが分かった。
※ ALLは{migration_sync}ロール専用(to migration_sync)で、SELECT/INSERT/UPDATE/DELETE すべての操作を許可するポリシーである。
重要: RLSポリシーはロールごとに設定される。フロントエンド(supabase-js)からのアクセスはanon(未認証)またはauthenticated(認証済み)ロールで実行されるため、migration_syncロールのALLポリシーは適用されない。
そのため、authenticated向けのINSERTとUPDATEポリシーが無い状態では、フロントからの登録は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を有効化すると、デフォルトですべての操作が拒否される
- 操作を許可するには、明示的にポリシーを定義する必要がある
- ポリシーはロールごとに設定される(
anon、authenticatedなど)
ポリシーの種類:
| コマンド | 説明 | 使用する条件 |
|---|---|---|
| SELECT | データの読み取り | USING |
| INSERT | データの挿入 | WITH CHECK |
| UPDATE | データの更新 | USING(既存行の参照)、WITH CHECK(更新後の検証) |
| DELETE | データの削除 | USING |
USING と WITH 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_authenticatedとonidazo_update_authenticatedが追加され、authenticatedロールでINSERTとUPDATEが可能になったことを確認した。
動作確認
ポリシーを追加後、フロントエンドから再度upsertを実行したところ、正常に登録できるようになった。
ブラウザのネットワークタブでPOST /rest/v1/public.onidazoが201 Createdまたは200 OKで成功することを確認した。
まとめ
- 403でも「認証が無い」とは限らない(RLSで拒否されることがある)
new row violates row-level security policyはRLSポリシー不足/条件不一致のサインupsertの場合はINSERTとUPDATEの両方のポリシーを用意すると安全- 変更はマイグレーションとしてSQL管理しておくと追跡しやすい