開発2026-06-06
Next.js App Router × Supabase:service role key をサーバー専用にして anon key を公開しない設計
ISR・サーバーコンポーネントで Supabase を使うとき、service role key をサーバー側に閉じれば NEXT_PUBLIC_ に anon key を出さずに済む。設計と注意点をまとめた。
Next.js App Router で Supabase を使うとき、anon key を NEXT_PUBLIC_ に入れたくない場合がある。サーバーサイドに閉じた設計でどう解決するかをまとめた。
事象・背景
Next.js のサーバーコンポーネントや ISR で Supabase からデータを取得したい。しかし NEXT_PUBLIC_SUPABASE_ANON_KEY をフロントに出したくない(クライアント側でユーザーが Supabase に直接アクセスしないアーキテクチャ)。
解決策:サーバー専用クライアントを作る
NEXT_PUBLIC_ を使わず、サーバーサイドのみで動く Supabase クライアントを作る。
// lib/supabaseServer.ts
import { createClient } from '@supabase/supabase-js';
export const supabaseServer = createClient(
process.env.SUPABASE_URL!, // NEXT_PUBLIC_ なし → サーバー専用
process.env.SUPABASE_SERVICE_ROLE_KEY!, // service role key
);
// app/page.tsx(サーバーコンポーネント)
import { supabaseServer } from '@/lib/supabaseServer';
export default async function Page() {
const { data } = await supabaseServer
.from('articles')
.select('*')
.eq('published', true);
return <ArticleList articles={data} />;
}
環境変数は SUPABASE_URL と SUPABASE_SERVICE_ROLE_KEY のみ。NEXT_PUBLIC_ なしなのでブラウザには一切届かない。
注意点:service role key は RLS を無視する
service role key は Row Level Security(RLS)を完全にバイパスする。anon key では弾かれるデータも全て取得・更新できてしまう。
サーバー側で使う場合でも、クエリに必ず適切なフィルタを入れる。「サーバーにしか届かないから大丈夫」という油断は危ない。
// ❌ RLS バイパス状態で全件取得(意図しないデータ漏れのリスク)
const { data } = await supabaseServer.from('users').select('*');
// ✅ 必要な条件を明示的に絞る
const { data } = await supabaseServer
.from('articles')
.select('*')
.eq('published', true)
.order('date', { ascending: false });
補足
- このパターンはデータ取得がサーバー完結する場合(ISR・SSG・サーバーコンポーネント)に向いている。クライアント側でリアルタイム更新や認証フローが必要な場合は anon key + RLS の設計が適切
SUPABASE_SERVICE_ROLE_KEYは Vercel の環境変数に登録し、NEXT_PUBLIC_なしにすることで本番ビルドでも安全に扱える- ISR のキャッシュと組み合わせると、DB アクセスをビルド時・再検証時だけに抑えて負荷を下げられる
※ 本記事にはアフィリエイトリンクが含まれます。