Supabase trigger関数の SET search_path TO public 忘れで本番ログイン全滅した話
本番環境で新規ユーザー登録が完全に死んだ。エラーメッセージはただ一言、Database error saving new user。原因を突き止めるのに数時間かかった。そしてまったく同じ罠を、2回踏んだ。
新規登録が全滅した朝
PiloTube(パイロチューブ)の開発が佳境に入ってきたある日、本番環境でユーザー登録のテストをしようとした。メールアドレスを入力して送信ボタンを押す。
エラー。
Database error saving new user
最初は「あ、また何かミスったかな」くらいの感覚だった。ローカルでは動いている。Supabase のダッシュボードを開いて、Auth のログを確認する。エラーは確かに出ている。でも詳細がほとんどない。
「Supabase のサーバー側の問題か?」と思いながら、Postgres のログを掘り始めた。
原因は trigger 関数の search_path 設定漏れ
数時間後、ようやく原因にたどり着いた。
Supabase では、ユーザーが登録されると auth.users テーブルにレコードが挿入される。それをトリガーに、public.users テーブルにプロフィールを自動作成する trigger 関数を自分は書いていた。よくある構成だ。
問題はその trigger 関数が SECURITY DEFINER で定義されていたこと。
SECURITY DEFINER とは、「関数を呼び出したユーザーの権限ではなく、関数を定義したユーザー(多くの場合 postgres)の権限で実行する」という設定だ。RLS(Row Level Security / Supabaseのアクセス制御機能)をバイパスするためによく使われる。
この SECURITY DEFINER 関数には、セキュリティ上の理由から search_path を明示的に設定する必要がある。設定しないと、関数の実行時に search_path が意図しない状態になり、テーブルが見つからなくなったり、想定外のスキーマを参照したりする。
自分が書いていた trigger 関数はこんな感じだった。
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
INSERT INTO public.users (id, email)
VALUES (NEW.id, NEW.email);
RETURN NEW;
END;
$$;
一見問題なさそうに見える。でも足りない。
正しくはこう書かなければいけない。
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO public -- ← これが必要
AS $$
BEGIN
INSERT INTO public.users (id, email)
VALUES (NEW.id, NEW.email);
RETURN NEW;
END;
$$;
SET search_path TO public の1行が抜けていただけで、本番の新規登録が全滅していた。
ローカルでは動いていた理由
これが厄介なところで、ローカル環境では再現しなかった。
Supabase のローカル開発環境(Docker ベース)と本番環境では、Postgres の設定がわずかに異なる。本番環境では search_path のデフォルト設定がより厳格になっていることがある。だからローカルでは動いて、本番でだけ落ちる。
「本番でだけ落ちる」は、デバッグとして最悪の部類だ。再現できないから、手がかりが少ない。
気づきのきっかけ
Supabase の公式ドキュメントをあらためて読み直していたとき、SECURITY DEFINER 関数に関する注意書きを見つけた。
For security reasons, you should set the search_path when creating SECURITY DEFINER functions.
「セキュリティ上の理由から」と書いてある。セキュリティのための設定が、機能の破壊を引き起こしていた。
ドキュメントには確かに書いてあった。自分が読み飛ばしていただけだ。
同じ罠を2回踏んだ
修正して、本番でテスト。動いた。よし、と思って次の機能開発に進んだ。
数週間後。別の trigger 関数を追加した。今度は public.users の更新時に別テーブルを同期する処理だった。
本番でテスト。
エラー。
……また SET search_path TO public が抜けていた。
正直、声が出た。「また同じやつか」と。
1回目は「知らなかった」で済む。2回目は「忘れた」だ。自分の中でルール化できていなかった。
対策として決めたこと
同じミスを3回しないために、trigger 関数を書くときのテンプレートを固定することにした。
-- trigger 関数テンプレート(SECURITY DEFINER 版)
CREATE OR REPLACE FUNCTION public.関数名()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO public -- 必須。忘れると本番で死ぬ
AS $$
BEGIN
-- 処理
RETURN NEW;
END;
$$;
このコメント「必須。忘れると本番で死ぬ」は、将来の自分への警告として残している。
また、新しい trigger 関数を追加したときは、必ずローカルと本番の両方で登録フローを通しでテストすることをルールにした。「ローカルで動いたから大丈夫」は、Supabase の本番環境には通用しない。
PiloTube 開発での教訓
PiloTube はまだ開発中のサービスだ。ユーザー数が少ないうちにこういうバグを踏めたのは、ある意味ラッキーだったとも言える。本番でユーザーが増えてから同じミスをしていたら、もっとまずかった。
ただ、2回踏んだのはまずい。
エラーメッセージが Database error saving new user という素っ気ない一言しか出ないのも、デバッグを難しくしている要因だった。Supabase の Auth 周りのエラーは、詳細が Postgres ログの奥深くに埋まっていることが多い。次からは最初に Postgres ログを見に行く、という順番を身体に染み込ませた。
読者へのヒント
Supabase で trigger 関数を書くなら、以下を頭に入れておいてほしい。
① SECURITY DEFINER を使うなら SET search_path TO public は必須
書き忘れると、本番環境でだけ落ちる可能性がある。ローカルで動いていても安心できない。
② エラー Database error saving new user が出たら Postgres ログを掘れ
Supabase ダッシュボードの Auth ログだけでは情報が足りないことが多い。Supabase Studio の「Logs」→「Postgres」を確認する。
③ trigger 関数はテンプレートを固定しろ 毎回ゼロから書くと、必ずどこかで設定が抜ける。コメント付きのテンプレートをスニペットとして保存しておくのが一番手っ取り早い。
④ 本番環境でのフロー通しテストをサボらない 「ローカルで動いた」は「本番で動く」を保証しない。特に Supabase の Auth 周りは、本番とローカルで挙動が微妙に違う。
同じ罠を踏んでいる人が、世界のどこかに絶対いると思って書いた。少しでも役に立てば。
チャプター生成AI
URL貼るだけ。AIがチャプターを自動生成。
YouTubeのURLをコピーして貼る
「生成する」を押す
概要欄にコピペして完了
月3回まで無料 · クレジットカード不要