Supabase RLS で安全な有料コンテンツゲートを作る — 設計パターンと実装の落とし穴
有料記事
¥500•11 min read•有料記事
#Supabase#セキュリティ#Next.js#個人開発
なぜ RLS が「ペイウォール」の核心なのか
有料コンテンツを配信するサービスで、最も恐ろしいのは何か。
「お金を払っていない人がコンテンツを見られる」状態。
クライアント側で if (purchased) { showContent() } のように制御していると、JavaScript を無効化したり、API を直接叩いたりするだけで突破される。
サーバー側でチェックしても、「全ユーザーの購入履歴テーブル」をうっかり全件取得できる API を作ってしまえば終わり。
Supabase Row Level Security(RLS)は、この問題をデータベース層で解決する。
データベース自体が「このユーザーはこの行を見れる/見れない」を判定するため、アプリケーション側のバグでは突破できない。
💡
私の体験談
このブログの有料記事機能も、RLS を有効にしています。仮に Next.js 側のチェックロジックにバグがあっても、データベース層でブロックされる二重の防御が効きます。
この記事でわかること:
- RLS の基本概念と「2層防御」の設計思想
- 購入履歴テーブルの RLS ポリシー設計
- Service Role Key と Anon Key の使い分け
- Webhook からの書き込みで RLS をバイパスする正しい方法
- N+1 問題を避ける購入チェックの実装
- サーバーコンポーネントでの効率的な使い方
RLS の基本: 「行レベル」のアクセス制御
通常のテーブル権限は「テーブル全体」に対して付与する。
GRANT SELECT ON article_purchases TO authenticated;
これだと、認証済みユーザーは全員の購入履歴を見れてしまう。
RLS は「どの行を見れるか」を SQL で記述できる。
ALTER TABLE article_purchases ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own purchases"
ON article_purchases
FOR SELECT
USING (auth.uid() = user_id);
これで、Supabase 経由のクエリは自動的に WHERE user_id = '現在のユーザーID' がついた状態で実行される。
別のユーザーの購入履歴を取得しようとしても、結果は空になる。
この先は有料コンテンツです
記事の続きを読むには購入が必要です
¥500