1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Supabase × Next.js】RLS設計で絶対にハマるポイント

1
Posted at

はじめに

Supabase × Next.js の組み合わせは、個人開発や小規模プロダクトにおいて非常に強力です。
認証・DB・RLS(Row Level Security)が一体化しており、「正しく使えば」かなり安全な構成を簡単に作れます。

ただし、そのRLSが本当に分かりづらい。

  • SQLは成功しているのに select が空配列になる
  • insert はできるのに select できない
  • Service Role Key を使うと全部動く
  • anon key に戻すと全部壊れる

今回は、Supabase × Next.js で実際にハマったRLS設計の落とし穴と、
「どう考えると破綻しないか」を整理します。

RLSは「誰が」「どこから」叩くかを最初に決めないと破綻する

SupabaseのRLSで一番重要なのは、このクエリは「誰として」「どこから」実行されるのかを最初に決め切ることです。

ここが曖昧なまま進めると、

  • policy を足しても直らない
  • なぜ通らないか分からない
  • 最終的に service_role に逃げる

という負のループに入ります。

Supabase RLSで混乱する理由

理由①:認証は通っているのに、データが取れない

Supabaseでは、

認証(auth)
権限(RLS)

が完全に別レイヤーです。

そのため、

next.js
const { data: { user } } = await supabase.auth.getUser()

で user が取れても、

next.js
const { data } = await supabase.from("workouts").select("*")

が 空配列になることがあります。

これは RLSが拒否しているが、エラーにはならないためです。

理由②:Service Role Key が「万能すぎる」

Next.js で Server Actions や API Route を書いていると、

createClient(process.env.SUPABASE_SERVICE_ROLE_KEY)

を使えば すべてのRLSを無視できます。

これにより、

「動いた!」
「RLSなんか要らないのでは?」

という錯覚に陥ります。

しかしこれは 管理者として直DBを叩いている状態であり、フロントエンドや将来の拡張を考えると非常に危険です。

実際にハマった構成
前提

  • Next.js App Router
  • Supabase Auth 使用
  • テーブル:user_plan_progress
user_id uuid not null
plan_type text
completed_sessions int

やりたかったこと

  • ログインユーザーが
  • 自分のデータだけ
  • select / insert / update できる

失敗したRLS例

よくある失敗①:insert だけ許可して select を忘れる
create policy "allow insert"
on user_plan_progress
for insert
with check ( auth.uid() = user_id );

この状態で insert は成功します。
しかし select は 常に空配列になります。

理由は単純で、select用のpolicyが存在しないからです。

よくある失敗②:auth.uid() が null になる

Next.js Server Component / Server Action から anon key で叩くと、

auth.uid()

は null になります。

その結果、

auth.uid() = user_id

は常に false になり、RLSに弾かれます。

「ログインしているのに null?」となる原因は、

  • Cookie が渡っていない
  • createClient の設定ミス
  • server / client の役割混同

がほとんどです。

正しく考えるRLS設計の順序

ここからが一番重要です。

① クエリの実行主体を分ける

まず、以下を分離して考えます。

実行場所 使用キー RLS
ブラウザ anon key 有効
Server Actions anon key + cookie 有効
管理処理 service role 無効

「service role を使う場所」を最小限にするのが鉄則です。

② RLSはCRUDすべてを書く

最低限、以下は揃えます。

-- select
create policy "select own rows"
on user_plan_progress
for select
using ( auth.uid() = user_id );

-- insert
create policy "insert own rows"
on user_plan_progress
for insert
with check ( auth.uid() = user_id );

-- update
create policy "update own rows"
on user_plan_progress
for update
using ( auth.uid() = user_id );

「insertできたからOK」は 罠 です。

③ user_id は必ずアプリ側で明示的に渡す

next.js
const user = await supabase.auth.getUser()

await supabase.from("user_plan_progress").insert({
  user_id: user.data.user.id,
  plan_type: "SQUAT",
  completed_sessions: 0
})

DB側で auth.uid() を自動セットしようとすると、
デバッグが一気に難しくなります。

RLSで詰まったときのチェックリスト

最後に、詰まったときの確認ポイントです。

  • select 用 policy はあるか
  • auth.uid() が null になっていないか
  • Service Role Key で誤魔化していないか
  • server / client で createClient を使い分けているか
  • 「誰として実行しているか」を説明できるか
  • これが説明できない状態は、だいたい壊れています。

まとめ

SupabaseのRLSは、強力・安全ですが、思想理解が必須です。

「とりあえず動かす」フェーズでは邪魔に感じますが、最初に設計を間違えると、後から必ず地獄を見る仕組みでもあります。

RLSはSQLではなく、アプリケーション設計そのものだと割り切るのが一番の近道でした。
同じところでハマる人の助けになれば幸いです。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?