メインコンテンツへスキップ
Think You Lab
ブログログイン無料で始める
トップ/ブログ/how-to
SupabaseRLSセキュリティPostgreSQLNext.js

Supabase RLS(行レベルセキュリティ)の書き方入門 — 実テーブルのポリシー例つき

Supabase RLS の仕組みと書き方を解説。THINK YOU LAB 本番スキーマの実例(users / memberships / courses / event_logs)を使って、CREATE POLICY の構文・auth.uid() の使い方・Service Role Key の使い所を説明します。

2026-04-15·約14分

LINE 公式アカウント

AI副業スタートガイドをLINEで無料配布中

目次
  • RLS とは何か — なぜ必要か
  • テーブルの「デフォルト全公開」という罠
  • RLS の役割
  • middleware 認証との役割分担
  • RLS を有効化する
  • `ALTER TABLE ... ENABLE ROW LEVEL SECURITY;` の意味
  • ポリシーの書き方
  • `CREATE POLICY` の構文
  • `auth.uid()` — ログイン中ユーザーの UUID
  • `auth.role()` — `anon` / `authenticated` の切り替え
  • `EXISTS` サブクエリで他テーブルの条件を参照する
  • THINK YOU LAB での実例
  • `users` テーブル — 「本人は全列読み書き」「他会員は display_name のみ」「admin は全件」
  • `memberships` テーブル — 「本人は読み取り」「admin は全件」
  • `courses` / `lessons` — 「active な会員のみ published コースを閲覧可」
  • `event_logs` — 「INSERT は全員可」「SELECT は admin のみ」
  • Service Role Key — RLS をバイパスする場面
  • Service Role Key とは
  • Stripe Webhook での使用例
  • Service Role Key の保管場所と禁止事項
  • よくある詰まりポイント
  • RLS 有効化後に API が 0件を返すようになった
  • anon キーでも読めてしまう
  • `auth.uid()` が NULL になる
  • まとめ

目次

  • RLS とは何か — なぜ必要か
  • テーブルの「デフォルト全公開」という罠
  • RLS の役割
  • middleware 認証との役割分担
  • RLS を有効化する
  • `ALTER TABLE ... ENABLE ROW LEVEL SECURITY;` の意味
  • ポリシーの書き方
  • `CREATE POLICY` の構文
  • `auth.uid()` — ログイン中ユーザーの UUID
  • `auth.role()` — `anon` / `authenticated` の切り替え
  • `EXISTS` サブクエリで他テーブルの条件を参照する
  • THINK YOU LAB での実例
  • `users` テーブル — 「本人は全列読み書き」「他会員は display_name のみ」「admin は全件」
  • `memberships` テーブル — 「本人は読み取り」「admin は全件」
  • `courses` / `lessons` — 「active な会員のみ published コースを閲覧可」
  • `event_logs` — 「INSERT は全員可」「SELECT は admin のみ」
  • Service Role Key — RLS をバイパスする場面
  • Service Role Key とは
  • Stripe Webhook での使用例
  • Service Role Key の保管場所と禁止事項
  • よくある詰まりポイント
  • RLS 有効化後に API が 0件を返すようになった
  • anon キーでも読めてしまう
  • `auth.uid()` が NULL になる
  • まとめ

Supabase を使い始めて最初に詰まるポイントのひとつが RLS(Row Level Security / 行レベルセキュリティ) です。

「テーブルを作ったのに API から取得すると 0件が返る」「なぜか他のユーザーのデータも見えてしまう」——これらの多くは RLS の設定が正しく理解できていないことが原因です。

この記事では次の順で RLS を解説します。

  1. RLS とは何か、なぜ有効化すると「全拒否」になるのか
  2. CREATE POLICY の構文と使い方
  3. THINK YOU LAB 本番スキーマの実例(users / memberships / courses / event_logs)
  4. Service Role Key で RLS をバイパスする場面(Stripe Webhook)

RLS とは何か — なぜ必要か

テーブルの「デフォルト全公開」という罠

Supabase(PostgreSQL)でテーブルを作成した直後、RLS を有効化していない状態では匿名キー(anon key)を使えば誰でも全データを読み書きできます。

// RLS 無効 = 誰でも全件取得できる
const { data } = await supabase.from("users").select("*");

フロントエンドには anon key が必ず含まれているため、RLS なしのテーブルは事実上公開されています。

RLS の役割

RLS を有効化すると、デフォルトで全アクセスが拒否されます。 ポリシーを明示的に定義した行だけが、定義した操作(SELECT / INSERT / UPDATE / DELETE)を許可されます。

「デフォルト全拒否」というホワイトリスト方式が RLS の基本思想です。

middleware 認証との役割分担

middleware(proxy.ts)と RLS は別々のレイヤーで動きます。

| レイヤー | 守るもの | 動くタイミング | |----------|----------|--------------| | middleware | ページへのアクセス | HTTP リクエストがサーバーに届いた瞬間 | | RLS | データベースの行 | SQL クエリが実行される瞬間 |

middleware を通過したとしても、DB クエリがユーザーの権限外のデータを取得しようとすれば RLS がブロックします。二層防御の設計です。

ページレベルの認証ガード(middleware)はこちら → Next.js App Router の middleware で Supabase 認証ガードを作る


RLS を有効化する

ALTER TABLE ... ENABLE ROW LEVEL SECURITY; の意味

ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;

これを実行した瞬間から、このテーブルへのすべての SELECT / INSERT / UPDATE / DELETE が拒否されます。ポリシーが1件もない状態でもデータを誰も読めなくなります。

THINK YOU LAB では initial_schema.sql のマイグレーションで、全テーブルを一括で有効化しています。

ALTER TABLE public.users               ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.plans               ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.memberships         ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.courses             ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.lessons             ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.event_logs          ENABLE ROW LEVEL SECURITY;

RLS を有効化した後、必ず必要なポリシーを定義します。ポリシーがないテーブルはアプリから一切アクセスできません。


ポリシーの書き方

CREATE POLICY の構文

CREATE POLICY "ポリシー名"
  ON テーブル名
  FOR 操作 (ALL / SELECT / INSERT / UPDATE / DELETE)
  USING (条件式)          -- SELECT / UPDATE / DELETE に適用
  WITH CHECK (条件式);    -- INSERT / UPDATE に適用

各キーワードの意味:

| キーワード | 説明 | |-----------|------| | FOR ALL | SELECT / INSERT / UPDATE / DELETE すべてに適用 | | FOR SELECT | 読み取りのみ | | USING (条件) | 既存の行に対する条件。SELECT / UPDATE / DELETE で評価される | | WITH CHECK (条件) | 新しい行に対する条件。INSERT / UPDATE で評価される |

auth.uid() — ログイン中ユーザーの UUID

USING (auth.uid() = user_id)

auth.uid() は現在のリクエストを送ってきているユーザーの UUID を返します。未認証の場合は NULL を返します。

auth.role() — anon / authenticated の切り替え

USING (auth.role() = 'authenticated')

auth.role() は現在のユーザーのロールを返します。ログイン済みなら 'authenticated'、未ログインなら 'anon' です。

EXISTS サブクエリで他テーブルの条件を参照する

USING (
  EXISTS (SELECT 1 FROM public.users u WHERE u.id = auth.uid() AND u.role = 'admin')
)

「users テーブルで自分のレコードを引いたとき、role が 'admin' であれば許可する」という条件です。


THINK YOU LAB での実例

users テーブル — 「本人は全列読み書き」「他会員は display_name のみ」「admin は全件」

-- 本人は自分のレコードを全操作可
CREATE POLICY "users: 本人は全列読み書き"
  ON public.users FOR ALL
  USING (auth.uid() = id);
 
-- ログイン済み会員なら他のユーザーの行も SELECT 可(display_name 等を表示するため)
CREATE POLICY "users: 他会員はdisplay_name/avatar_urlのみ読める"
  ON public.users FOR SELECT
  USING (auth.role() = 'authenticated');
 
-- admin は全ユーザーを全操作可
CREATE POLICY "users: adminは全件読み書き"
  ON public.users FOR ALL
  USING (
    EXISTS (SELECT 1 FROM public.users u WHERE u.id = auth.uid() AND u.role = 'admin')
  );

3つのポリシーが重なっています。PostgreSQL RLS はポリシーの OR 評価で動くため、どれか1つが許可すれば操作できます。

memberships テーブル — 「本人は読み取り」「admin は全件」

-- 自分の会員ステータスは自分だけ読める
CREATE POLICY "memberships: 本人は読み取り可"
  ON public.memberships FOR SELECT
  USING (auth.uid() = user_id);
 
-- admin は全件操作可(Webhook 経由での更新も含む)
CREATE POLICY "memberships: adminは全件読み書き"
  ON public.memberships FOR ALL
  USING (
    EXISTS (SELECT 1 FROM public.users u WHERE u.id = auth.uid() AND u.role = 'admin')
  );

memberships は会員の有効・無効・プランを管理する最重要テーブルです。本人は自分のレコードを読むだけで書き込みはできません。Stripe Webhook による更新はアプリサーバーが Service Role Key を使って行います。

courses / lessons — 「active な会員のみ published コースを閲覧可」

-- active ステータスの会員のみ、公開済みコースを閲覧可
CREATE POLICY "courses: 会員はpublishedのみ閲覧可"
  ON public.courses FOR SELECT
  USING (
    is_published = true
    AND EXISTS (
      SELECT 1 FROM public.memberships m
      WHERE m.user_id = auth.uid() AND m.status = 'active'
    )
  );
 
CREATE POLICY "courses: adminは全件読み書き"
  ON public.courses FOR ALL
  USING (
    EXISTS (SELECT 1 FROM public.users u WHERE u.id = auth.uid() AND u.role = 'admin')
  );

この設計で「未払い会員が直接 API を叩いてもコンテンツが返らない」ことを DB レベルで保証します。RLS があれば、API ルートのガードを書き忘れても、DB からデータが出ることはありません。

event_logs — 「INSERT は全員可」「SELECT は admin のみ」

-- 未ログインでも INSERT 可(アクセスログ・行動ログの収集)
CREATE POLICY "event_logs: 全員INSERT可"
  ON public.event_logs FOR INSERT
  WITH CHECK (true);
 
-- SELECT は admin のみ(ログの閲覧・集計)
CREATE POLICY "event_logs: adminのみSELECT"
  ON public.event_logs FOR SELECT
  USING (
    EXISTS (SELECT 1 FROM public.users u WHERE u.id = auth.uid() AND u.role = 'admin')
  );

WITH CHECK (true) は「どんな内容でも書き込み可」という意味です。INSERT を許可しつつ、SELECT は admin にしか許可していないため、蓄積したログが外部に漏れることはありません。

一方で plans テーブルは全公開が必要なため:

-- plans: 全員読み取り可(料金ページは anon からアクセスされる)
CREATE POLICY "plans: 全員読み取り可"
  ON public.plans FOR SELECT
  USING (true);

USING (true) は「すべての行を SELECT 可」という意味です。pricing ページはログイン前のユーザーも閲覧するため、anon キーから料金プランのデータを取得できる必要があります。


Service Role Key — RLS をバイパスする場面

Service Role Key とは

| キー | 用途 | RLS | |------|------|-----| | anon key | フロントエンド・API ルートで使用 | RLS が適用される | | service role key | サーバーサイドのみ(Webhook・バッチ処理) | RLS をバイパスする |

Stripe Webhook での使用例

Stripe が決済完了を通知してくる Webhook リクエストには Supabase の認証情報(Cookie)がありません。通常の anon クライアントを使って memberships テーブルを更新しようとしても RLS がブロックします。

// /api/webhooks/stripe/route.ts
import { createClient } from "@supabase/supabase-js";
 
// Service Role Key でクライアント作成 → RLS バイパス
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);
 
await supabase
  .from("memberships")
  .update({ status: "active" })
  .eq("user_id", userId);

Service Role Key の保管場所と禁止事項

# .env.local(絶対にクライアントサイドに公開しない)
SUPABASE_SERVICE_ROLE_KEY=eyJhb...

NEXT_PUBLIC_ プレフィックスを絶対につけてはいけません。 ブラウザに露出すると誰でも全データを読み書き・削除できる状態になります。


よくある詰まりポイント

RLS 有効化後に API が 0件を返すようになった

原因: ポリシーが設定されていない

-- Supabase SQL Editor で実行
SELECT * FROM pg_policies WHERE tablename = 'users';

ポリシーが0件なら必要なポリシーを追加してください。

anon キーでも読めてしまう

原因: USING (true) のポリシーが設定されている(意図せず全公開になっている)

plans テーブルのように意図的に全公開するケースを除き、USING (true) は使わないようにします。

auth.uid() が NULL になる

原因の多くは以下のいずれか:

  1. Cookie が正しく設定されていない
  2. middleware(proxy.ts)が Cookie のリフレッシュに失敗している
  3. Webhook や管理処理で anon key を使っている(Service Role Key を使うべき箇所)

まとめ

  • RLS 有効化 = デフォルト全拒否。ポリシーを定義した行だけが操作可能になる

  • USING は既存行への条件(SELECT / UPDATE / DELETE)、WITH CHECK は新規行への条件(INSERT / UPDATE)

  • auth.uid() でログイン中ユーザーを識別、auth.role() で anon / authenticated を区別する

  • admin 判定は EXISTS (SELECT 1 FROM users WHERE ... AND role = 'admin') で実装する

  • Service Role Key は RLS をバイパスする強力なキー。サーバーサイドのみで使い、絶対にフロントエンドに露出させない

  • Service Role を使う Stripe Webhook の実装はこちら → Supabase + Stripe Webhook で会員ステータスを自動更新する

  • ページレベルの認証ガード(middleware)はこちら → Next.js App Router の middleware で Supabase 認証ガードを作る


RLS を書けるようになると「誰が何のデータにアクセスできるか」を DB 側で制御できます。RLS ポリシーのパターン集(読み取り専用/ユーザー分離/管理者アクセス)を LINE で無料配布中です。

→ Supabase RLS ポリシーテンプレを受け取る(LINE登録・無料)

LINE 公式アカウント

Supabase RLS ポリシーテンプレート集をLINEで無料配布中

R

Rikuto (LAB)

非エンジニアが Claude Code × n8n × Supabase で副業システムを作り続ける実験記。 失敗も含めたリアルな一次情報を発信しています。

THINK YOU LAB 運営

関連記事

  • Next.js App Router の middleware で Supabase 認証ガードを作る — 実装コード全公開

    2026-04-15

  • Stripe Checkout を Next.js Server Action で実装する — Supabase Auth 連携つき

    2026-04-15

  • Supabase + Stripe Webhook で会員ステータスを自動更新する — Next.js App Router 実装

    2026-04-15

← ブログ一覧へ
X でシェアLINE でシェア
← 前の記事Vercel に Next.js をデプロイする — 環境変数・Root Directory・Supabase 連携の設定方法
次の記事 →Supabase Auth で magic link ログインを実装する — Next.js App Router 対応

目次

  • RLS とは何か — なぜ必要か
  • テーブルの「デフォルト全公開」という罠
  • RLS の役割
  • middleware 認証との役割分担
  • RLS を有効化する
  • `ALTER TABLE ... ENABLE ROW LEVEL SECURITY;` の意味
  • ポリシーの書き方
  • `CREATE POLICY` の構文
  • `auth.uid()` — ログイン中ユーザーの UUID
  • `auth.role()` — `anon` / `authenticated` の切り替え
  • `EXISTS` サブクエリで他テーブルの条件を参照する
  • THINK YOU LAB での実例
  • `users` テーブル — 「本人は全列読み書き」「他会員は display_name のみ」「admin は全件」
  • `memberships` テーブル — 「本人は読み取り」「admin は全件」
  • `courses` / `lessons` — 「active な会員のみ published コースを閲覧可」
  • `event_logs` — 「INSERT は全員可」「SELECT は admin のみ」
  • Service Role Key — RLS をバイパスする場面
  • Service Role Key とは
  • Stripe Webhook での使用例
  • Service Role Key の保管場所と禁止事項
  • よくある詰まりポイント
  • RLS 有効化後に API が 0件を返すようになった
  • anon キーでも読めてしまう
  • `auth.uid()` が NULL になる
  • まとめ
Think You Lab
このブログについて料金プランプライバシーポリシーお問い合わせ特定商取引法ログイン

© 2026 Think You Lab