ログオフ
開発2026-06-06

Next.js App Router × Supabase:anon key は隠さなくていい。守るのは RLS、service role key はサーバー専用にする

Supabase の anon key はブラウザに出る前提の鍵で、NEXT_PUBLIC_ に置いても問題ない。守るべきは RLS。一方 service role key は RLS をバイパスする秘密なので、サーバー専用に閉じる。この役割分担を整理した。

Supabase を Next.js で使い始めると、「anon key を NEXT_PUBLIC_ でブラウザに出して大丈夫なのか」と不安になる。結論から言うと、anon key は出していい。守るべきはそこではなく RLS(Row Level Security)だ。逆に絶対に出してはいけないのが service role key で、こちらをサーバー専用に閉じる。2 つの鍵の役割を取り違えると、設計を間違える。

まず前提を正す:2 つの鍵の役割

Supabase はプロジェクトに 2 種類の鍵を発行する。性質が正反対なので、ここを最初に押さえる。

  • anon key:ブラウザに公開される前提で設計された鍵。NEXT_PUBLIC_SUPABASE_ANON_KEY として配っても問題ない。この鍵だけでは何でもできるわけではなく、アクセス制御は RLS(Row Level Security) が担う。anon key は「どのテーブルに、RLS の許す範囲で問い合わせていいか」の入口にすぎない。
  • service role keyRLS を完全にバイパスする高権限の秘密。これを持つと全行を読み書きできる。クライアントに出した瞬間、DB が丸ごと公開されたのと同じになる。サーバー専用に閉じるのは当然の前提で、NEXT_PUBLIC_ を付けてはいけない。

よくある誤解が「anon key を隠せば安全」という発想だ。これは逆で、anon key を隠しても RLS が緩ければデータは漏れるし、anon key が見えていても RLS が正しければ守られる。セキュリティの本体は鍵の秘匿ではなく RLS のポリシーにある。

anon key + RLS:クライアントから直接触る通常パターン

ユーザー認証やリアルタイム更新があるなら、これが基本形になる。anon key はブラウザに出し、RLS でアクセス範囲を縛る。

// lib/supabaseClient.ts
import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, // ブラウザに出てOK
);
-- 公開記事だけ anon に読ませる RLS ポリシー
alter table articles enable row level security;

create policy "public can read published articles"
  on articles for select
  using (published = true);

このポリシーがあれば、anon key を持つクライアントは published = true の行しか取れない。鍵が見えていることは問題にならない。

サーバー専用クライアント:RLS を越える操作を閉じ込めたいとき

では service role key の出番はどこか。RLS では表現しきれない特権操作(管理画面での全件集計、ユーザーをまたいだ書き込み、バッチ処理など)をサーバー側に閉じ込めたいときだ。あわせて「クライアントから直接 DB を触らせず、Route Handler / Server Action を必ず経由させる」というアーキテクチャを選ぶ場合にも使う。

// lib/supabaseAdmin.ts —— サーバーからしか import しない
import { createClient } from '@supabase/supabase-js';

export const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,               // NEXT_PUBLIC_ なし
  process.env.SUPABASE_SERVICE_ROLE_KEY!,  // RLS をバイパスする秘密
);
// app/api/articles/route.ts(Route Handler = サーバー専用)
import { supabaseAdmin } from '@/lib/supabaseAdmin';

export async function GET() {
  const { data } = await supabaseAdmin
    .from('articles')
    .select('*')
    .eq('published', true);

  return Response.json(data);
}

SUPABASE_SERVICE_ROLE_KEYNEXT_PUBLIC_ を付けない。これでバンドルに混ざらず、ブラウザには届かない。Vercel の環境変数に登録するときも同様に NEXT_PUBLIC_ なしで入れる。

service role key を使うときの注意:RLS が効いていない

service role key を使うクライアントでは RLS が一切働かない。anon key なら弾かれたデータも全部読めて、書ける。「サーバーにしか届かないから大丈夫」と油断すると、クエリの絞り込み漏れがそのまま漏洩・破壊につながる。RLS という安全網を自分で外している状態だと意識して、条件は明示的に書く。

// ❌ RLS が効かない状態で全件取得(絞り忘れがそのまま事故になる)
const { data } = await supabaseAdmin.from('users').select('*');

// ✅ 必要な条件を必ず明示する
const { data } = await supabaseAdmin
  .from('articles')
  .select('*')
  .eq('published', true)
  .order('date', { ascending: false });

どちらを選ぶか

anon key + RLSservice role key(サーバー専用)
鍵の公開ブラウザに出してOK絶対に出さない
アクセス制御RLS が担保RLS をバイパス。自分で絞る
向いている場面認証・リアルタイム・クライアント直アクセス特権操作・全件処理・DB を直接触らせない構成

迷ったら anon key + RLS が既定。これが Supabase の想定する標準的な使い方だ。service role key は「RLS を越える必要がある処理だけ」をサーバーに閉じ込めるための道具と捉えるといい。

  • ISR・SSG・サーバーコンポーネントでデータがサーバー完結するなら、Route Handler / Server Action 経由にしてクライアントから DB を見せない構成が組める
  • ISR のキャッシュと組み合わせれば、DB アクセスをビルド時・再検証時だけに抑えて負荷を下げられる
  • どちらの鍵を使うにせよ、RLS は必ず有効にしておく。service role 側のミスがあっても、anon 経路は RLS で守られている状態を保てる

関連記事

開発 一覧へ