はじめに
Supabase は 2026-10-30 から、既存の全プロジェクトで public スキーマの新規テーブルへの権限(GRANT)の自動付与をやめます。
これからは、テーブルを作っただけでは Data API(PostgREST / supabase-js)から触れません。
筆者は期限前から、この状態を一度踏んでいます。
RLS ポリシーを正しく書いたつもりなのに、新しく作ったテーブルへの操作が全部 42501 で失敗しました。
エラーコードが RLS 違反と同じなので、ポリシーの書き間違いを疑って遠回りしました。
この記事は、Supabase を使っていて SQL Editor や migration でテーブルを作っている方に向けたものです。
読み終えると、次の2つができるようになります。
- 自分のプロジェクトが影響を受けるかを SQL で確認する
- migration に GRANT を書く型を、そのまま持ち帰る
TL;DR
-
2026-10-30 以降、public の新規テーブルには
anon/authenticated/service_roleの権限が自動で付かない。既存テーブルの権限はそのまま -
GRANT と RLS は別の層。GRANT が無いと、RLS を評価する前に
42501 permission deniedで弾かれる - 同じ
42501でも、メッセージを見れば GRANT 不足か RLS 違反かを見分けられる -
新しいテーブルの migration には GRANT を必ず書く。必要なロール・必要な操作だけに絞り、
anonには安易に付けない - 本番が無事でも、migration から環境を作り直すと全テーブルが
42501になることがある。スキーマの正本にも GRANT を書いておく
何が変わるのか
公式の告知(Changelog)から、要点をまとめます。
| 日付 | 内容 |
|---|---|
| 2026-04-28 | プロジェクト作成時に新しい挙動を選べるようになった |
| 2026-05-30 | 新規プロジェクトは新しい挙動がデフォルトになった |
| 2026-10-30 | 既存の全プロジェクトに新しい挙動が適用される |
変更の中身は次のとおりです。
- これまでは、public スキーマに作ったテーブルに
anon/authenticated/service_roleのSELECT, INSERT, UPDATE, DELETEが既定の権限(default privileges)で自動的に付いていた - これからは、この既定の権限が外れる。Data API から使うテーブルには、明示的な GRANT が必要になる
- 既存テーブルは影響を受けない。付いている権限はそのまま残る
- 対象は Data API(PostgREST / GraphQL)経由のアクセス。Postgres への直接接続は関係ない
service_role も対象です。サーバー側で service_role キーを使っていると「RLS をバイパスするから大丈夫」と思いがちですが、RLS をバイパスできても GRANT はバイパスできません。サーバーからだけ使うテーブルでも、service_role への GRANT が必要です。
影響が出るのは、大きく次の2つです。
- これから作るテーブル
-
migration やスキーマ定義から環境を作り直すとき(新しいプロジェクト、ローカルでの
supabase db resetなど)
2つ目は見落としやすいポイントです。
公式の Discussion でも、新しいデフォルトで supabase db reset をすると過去のテーブルに GRANT が付かない、という点が取り上げられています(Discussion #45329)。
GRANT と RLS は別の層
まず、なぜ「RLS を正しく書いたのに動かない」のかを整理します。
Postgres は、Data API から来たリクエストを2段階でチェックします。
| 層 | 決めること | 例 |
|---|---|---|
| テーブル権限(GRANT) | そのロールが、そのテーブルをそもそも操作してよいか |
authenticated は notes に INSERT できる |
| RLS(行レベルセキュリティ) | そのロールが、どの行を見てよいか・書いてよいか | 自分の owner_id の行だけ |
GRANT は外側の門で、RLS はその内側の門です。
外側の門が閉じていると、内側の RLS は評価すらされません。
だから、ポリシーをどれだけ正しく書いても 42501 のままです。
公式ドキュメントも「公開するオブジェクトには両方を使うこと」と書いています(Securing your API)。
実際にハマった例:期限前から起きていた
筆者の場合、既存のテーブルは問題なく動いていました。
ところが、後から SQL Editor で create table した新しいテーブルだけ、アプリからの操作がすべて失敗しました。
code: 42501
message: permission denied for table xxx
RLS を有効にしてポリシーも書いた直後だったので、まずポリシーを疑いました。
しかし、原因は新しいテーブルに authenticated への GRANT が付いていなかったことでした。
既存テーブルには以前から権限が付いていたため、それまで問題が表に出なかったのです。
振り返ると、今回の廃止は「条件によってはすでに起きていたこと」が、全プロジェクトの新規テーブルに広がる変更だと言えます。
42501 の見分け方:同じコードで原因が2つ
42501 は「権限不足」を表す Postgres のエラーコードです。
GRANT が無いときも、RLS に弾かれたときも同じコードになります。
見分けるにはメッセージを見ます。
| メッセージ | 原因 | 対処 |
|---|---|---|
permission denied for table xxx |
テーブル権限(GRANT)が無い | GRANT を付ける |
new row violates row-level security policy for table "xxx" |
RLS ポリシーに合わない | ポリシーまたはクエリを見直す |
新しい挙動では、GRANT 不足のときに hint に必要な GRANT 文が入って返ってきます(公式 Changelog より)。
エラーオブジェクトは message だけでなく hint まで見るのがおすすめです。
const { error } = await supabase
.from('notes')
.insert({ body: 'test', owner_id: userId })
if (error) {
console.log(error.code, error.message, error.hint)
// 42501 / permission denied for table notes → GRANT が無い
// 42501 / new row violates row-level security policy → RLS に合わない
}
補足:RLS 側でよくある落とし穴
GRANT とは別の原因ですが、見分けるときに一緒に知っておくと便利なものを1つ挙げます。
supabase-js の insert().select() は、SQL でいうと INSERT ... RETURNING です。
このとき Postgres は、挿入した行が SELECT ポリシーも満たすことを求めます(PostgreSQL: CREATE POLICY)。
たとえば SELECT ポリシーが「トリガーで後から作られる別テーブルの行」を前提にしていると、INSERT ポリシーは満たしていても失敗します。
INSERT ポリシーを何度見直しても直らないときは、SELECT ポリシーも確認してみてください。
自分のプロジェクトが影響を受けるか確かめる
既存テーブルの権限を見る
SQL Editor で次のどちらかを実行します。
-- テーブルごとの権限(ACL)をまとめて見る
select relname, relacl
from pg_class
where relnamespace = 'public'::regnamespace
and relkind = 'r'
order by relname;
relacl に authenticated=arwd... のような表示があれば付与済みです。
文字の意味は、r=SELECT、a=INSERT、w=UPDATE、d=DELETE です(PostgreSQL: Privileges)。
ロールと操作を1行ずつ見たい場合は、こちらが読みやすいです。
select table_name, grantee, privilege_type
from information_schema.role_table_grants
where table_schema = 'public'
and grantee in ('anon', 'authenticated', 'service_role')
order by table_name, grantee, privilege_type;
確認するポイント
| 確認すること | 状態 | 判断 |
|---|---|---|
| 本番の既存テーブルに権限が付いているか | 付いている | 当面は影響なし |
| migration / スキーマ定義に GRANT が書かれているか | 書かれていない | 環境を作り直すと全テーブルで 42501 |
| 今後テーブルを追加する予定があるか | ある | migration に GRANT を書く運用に切り替える |
本番が無事でも、2行目に当てはまるなら対応しておく価値があります。
「本番の権限は、過去の自動付与のおかげで付いているだけ」という状態だからです。
対策:migration に GRANT を明示する
新しいテーブルの migration の型
テーブル作成・RLS 有効化・ポリシー・GRANT を、同じ migration ファイルにセットで書きます。
別ファイルに分けると、GRANT だけ適用し忘れる事故が起きやすくなります。
create table if not exists public.notes (
id uuid primary key default gen_random_uuid(),
owner_id uuid not null references auth.users (id) on delete cascade,
body text not null,
created_at timestamptz not null default now()
);
alter table public.notes enable row level security;
create policy "notes: 自分の行だけ読める" on public.notes
for select to authenticated
using ((select auth.uid()) = owner_id);
create policy "notes: 自分の行だけ書ける" on public.notes
for insert to authenticated
with check ((select auth.uid()) = owner_id);
-- テーブル権限(RLS の前段)。2026-10-30 以降は自動で付かないので明示する。
-- ログインユーザーだけが使うテーブルなので anon には付けない。
grant select, insert, update, delete on table public.notes to authenticated;
ポリシー内の auth.uid() を (select auth.uid()) で包んでいるのは、行ごとの再評価を避けるためです(Row Level Security)。
付ける権限の決め方
自動付与は「3ロールに4操作すべて」でした。
明示するなら、必要なものだけに絞れます。
| テーブルの使われ方 | 付けるもの |
|---|---|
| ログインユーザーだけが読み書きする |
authenticated に必要な操作だけ |
| 未ログインでも読める公開データ |
anon に select だけ(+RLS で行を絞る) |
| 読むだけのマスタ |
select だけ |
サーバー(service_role)からも操作する |
service_role にも必要な操作を付ける |
anon に付けると、未ログインでのアクセス経路が1つ増えます。
RLS で守られていても、使わない経路は最初から作らない方が安全側です。
テーブル以外にも GRANT が要るもの
公式ドキュメントと Discussion では、テーブル以外にも次のものが挙げられています。
-
関数(RPC):
execute権限を明示する - ビュー: テーブルと同様に GRANT が要る。同じ migration で付ける
-
シーケンス: 既定の権限の取り消し対象に含まれる。
serial系の列を使う場合は確認する
関数は、呼べるロールを絞る書き方にしておくと安心です。
Postgres の関数は、既定で PUBLIC(全ロール)に execute が付くためです。
revoke execute on function public.my_rpc(uuid) from public, anon;
grant execute on function public.my_rpc(uuid) to authenticated;
スキーマの正本にも既存テーブルぶんを書く
スキーマ全体を1ファイル(schema.sql など)で管理している場合は、既存の全テーブルぶんの GRANT も書いておきます。
本番にはすでに付いているので、実行しても何も変わりません。
効くのは、そのファイルから環境を作り直したときだけです。
小技:Supabase のロールが無い環境でも落ちない書き方
型生成や CI のために、素の Postgres コンテナへスキーマを流すことがあります。
その環境には authenticated などの Supabase 固有ロールが無いため、GRANT がエラーになります。
pg_roles で存在を確認してから付けると、両方の環境で同じファイルを使えます。
do $$
begin
if exists (select 1 from pg_roles where rolname = 'authenticated') then
grant select, insert, update, delete
on table public.table_a, public.table_b to authenticated;
end if;
end $$;
create policy ... to authenticated のようにポリシー側でもロールを参照している場合は、この書き方だけでは足りません。ポリシー作成時にもロールが必要になるため、その環境では先に create role authenticated; などでスタブのロールを作っておく必要があります。
期限前に新しい挙動で試しておく
10/30 を待たずに、既存プロジェクトを新しい挙動に切り替えることもできます。
公式 Changelog では、SQL Editor で次を実行する方法が案内されています。
alter default privileges for role postgres in schema public
revoke select, insert, update, delete on tables from anon, authenticated, service_role;
alter default privileges for role postgres in schema public
revoke usage, select on sequences from anon, authenticated, service_role;
これは以後に作るテーブルの既定の権限を変えるもので、既存テーブルの権限は外しません。
いきなり本番で試すのが不安なら、開発用のプロジェクトで実行してから、テーブルを1つ作って 42501 と hint を確認してみるのがおすすめです。
ついでにやっておくとよいこと
- 既定の権限に頼らない: 「勝手に付く」前提をやめる。migration を読めば権限が全部分かる状態にしておくと、レビューもしやすくなる
-
anonに何が付いているかを棚卸しする: 自動付与で付いていた不要なanon権限は、外しておくと安全側になる。ただし、外す前に未ログインで使う機能が無いかを必ず確認する - Security Advisor を確認する: ダッシュボードの Advisors で、RLS 未設定のテーブルなどの警告も合わせて見ておく
まとめ
- 2026-10-30 以降、public の新規テーブルには
anon/authenticated/service_roleの権限が自動で付かない - GRANT は外側の門、RLS は内側の門。GRANT が無いと RLS 以前に
42501で弾かれる -
42501はmessageとhintで原因を見分ける。permission denied for tableなら GRANT 不足 - 新しいテーブルの migration には、RLS・ポリシーと一緒に GRANT を書く。付けるのは必要なロール・操作だけ
- 本番が無事でも、スキーマの正本に GRANT が無ければ作り直した環境で壊れる。今のうちに書いておく