はじめに
takkenai.jpの開発でSupabaseのRow Level Security(RLS)をNext.js App Routerに導入した際、同じ誤りに2回踏みました。「設定したはずなのにデータが取れない」「リリース後にエラーレポートが時」——同じハマり方をする人がいれば少しでも参考になればと思います。
最初に知っておくべきRLSの基本
SupabaseのRLSは、PostgreSQLのRow Level Securityをラップしたものです。有効にすると、ポリシーを書かない限り誰もそのテーブルにアクセスできなくなります。デフォルトは「全拒否」です。
App Routerでは主に2つのクライアントを使い分けます:
// Server Component / Route Handler用(ユーザー認証に必要)
import { createServerClient } from '@supabase/ssr';
// Client Component用
import { createBrowserClient } from '@supabase/ssr';
ミス1:anonキーで認証済みリソースにアクセスしようとした
何が起きたか
RLSを有効にした後、Server Componentからデータを取得しようとしたら空配列が返ってきた。
// これが問題のコード
export default async function Page() {
const supabase = createClient(); // サーバーサイドのつもりで作ったのに
const { data } = await supabase.from('articles').select('*');
// data === [] ... なぜ?
}
原因
Server Component内でcreateClient()と書いていたつもりが、実際にはBrowser環境で初期化された山手がエクスポートされていた。cookies()を渡さず山手をインポートしていたので、anonキーで動作していました。
// lib/supabase/server.ts がこうなっていた
export function createClient() {
return createBrowserClient( // ← 問題!serverではなく browser
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
修正
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export function createClient() {
const cookieStore = cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
},
},
}
);
}
教訓: server.tsとclient.tsを分けて作っても、インポートする場所を間違えると全沢意になる。
ミス2:service_roleキーを従来通り使ってRLSをバイパスした
何が起きたか
最初の問題を修正したものの、次は「管理機能の為に全データを取得したい」シーンでわからずservice_roleキーをRoute Handlerにハードコードしてしまいました。
// app/api/admin/route.ts (不適切な実装)
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // ← RLSを完全無視
);
何がまずいのか
service_roleキーはRLSを完全にバイパスします。これ自体は必要なケースがありますが、問題は2つでした:
- Route Handler自体に認証チェックを増やしたない→ログインしていないユーザーでもアクセスできた
- RLSの恵沢を自分で捨てている→セキュリティ設計が崩壊する
正しいアプローチ
大半のケースではservice_roleは不要。ユーザーのセッションを引き継いだServerコンテキストを使うべきです。
// 正しいRoute Handler
export async function GET(request: Request) {
const supabase = createServerClient(...);
// まず認証チェック
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response('Unauthorized', { status: 401 });
// RLSがユーザーの権限で自動フィルタリングしてくれる
const { data } = await supabase.from('articles').select('*');
return Response.json(data);
}
教訓: RLSは「データ層の認証」。service_roleでバイパスするのは、鍵をかけたのに鍵をフロントドアに山積みするようなもの。
まとめ
| ポイント | 内容 |
|---|---|
createBrowserClient vs createServerClient
|
Server Component内で間違えると認証情報を読まずanonキー動作 |
service_roleキーの使いどころ |
RLSが完全無視される。Route Handler認証チェックも并用すること |
| RLSの正しい層位づけ | アプリケーション層でなくデータ層のセキュリティと組み合わせる |
両方とも「動いているように見える」でデバッグが難しかったです。同じハマり方をする方の参考になれば幸いです。
実際に使っているサービスはtakkenai.jp——宅建試験のAI学習サービスです。ユーザーごとの学習履歴管理にこのスタックを使っています。