🐕
# Supabase × Clerk 完全統合ガイド
Clerk で認証したユーザーが Supabase 上で「自分のデータのみ」を読み書きできるようにするには、Row-Level Security (RLS) の設定と、クライアント側での JWT 対置が必須です。本記事では次の2つをまとめて解説します。
- RLS テーブル設定とポリシー (SQL 完全版)
- Supabase クライアントの実装変更 (サービスキー → anon + JWT)
1. RLS テーブル設定とポリシー
1.1 前提
- テーブル名:
user_api_keys
- ユーザーID列:
user_id
(型: TEXT) - JWT の
sub
クレームに Clerk のユーザーID が入っている
1.2 完全版 SQL
以下を Supabase の SQL エディタに貼り付けて一気に実行してください。
-- 1) user_id にユニーク制約を追加
ALTER TABLE public.user_api_keys
DROP CONSTRAINT IF EXISTS user_api_keys_user_id_key;
ALTER TABLE public.user_api_keys
ADD CONSTRAINT user_api_keys_user_id_key UNIQUE (user_id);
-- 2) 挿入時に自動で JWT の sub をセット
ALTER TABLE public.user_api_keys
ALTER COLUMN user_id SET DEFAULT requesting_user_id();
-- 3) RLS を有効化
ALTER TABLE public.user_api_keys
ENABLE ROW LEVEL SECURITY;
-- 4) 既存ポリシーをすべて削除
DROP POLICY IF EXISTS "Select own key" ON public.user_api_keys;
DROP POLICY IF EXISTS "Insert own key" ON public.user_api_keys;
DROP POLICY IF EXISTS "Update own key" ON public.user_api_keys;
DROP POLICY IF EXISTS "Delete own key" ON public.user_api_keys;
-- 5) RLS ポリシーを再登録
CREATE POLICY "Select own key"
ON public.user_api_keys
FOR SELECT
TO authenticated
USING (requesting_user_id() = user_id);
CREATE POLICY "Insert own key"
ON public.user_api_keys
FOR INSERT
TO authenticated
WITH CHECK (requesting_user_id() = user_id);
CREATE POLICY "Update own key"
ON public.user_api_keys
FOR UPDATE
TO authenticated
USING (requesting_user_id() = user_id)
WITH CHECK (requesting_user_id() = user_id);
CREATE POLICY "Delete own key"
ON public.user_api_keys
FOR DELETE
TO authenticated
USING (requesting_user_id() = user_id);
1.3 解説
-
ユニーク制約 をつけることで、
.upsert({user_id, ...}, { onConflict: 'user_id' })
が機能。 -
デフォルト値
requesting_user_id()
で INSERT 時にユーザーIDを自動セット。 - USING 句で SELECT/UPDATE/DELETE の行絞り込み。
- WITH CHECK 句で INSERT/UPDATE 後の整合性を保証。
-
TO authenticated
により、認証ユーザーのみ適用。匿名は別制御。
2. Supabase クライアントの実装変更
⚠️ 絶対にサービスロールキーではなく、クライアント(anon)キー+Clerk が発行した JWT を使ってリクエストしてください。
- サービスロールキーは RLS を完全にバイパス してしまいます(supabase.com)。
- anon キーは RLS と連動し、ユーザーの認証状態(JWT)を組み合わせることで正しくアクセス制御できます(supabase.com)。
2.1 元の実装
// app/lib/utils/supabase.ts
import { getAuth } from "@clerk/remix/ssr.server";
import { ActionFunctionArgs } from "@remix-run/cloudflare";
import { createClient } from "@supabase/supabase-js";
export async function getSupabaseClient(args: ActionFunctionArgs) {
const { context } = args;
const env = context.cloudflare.env;
const { userId } = await getAuth(args);
// サービスロールキーを使い、全権限でアクセス(RLS をバイパス)
const key = env.SUPABASE_SERVICE_ROLE_KEY || env.NEXT_PUBLIC_SUPABASE_KEY!;
return createClient(env.NEXT_PUBLIC_SUPABASE_URL!, key);
}
問題点
- サービスロールキーは RLS を無視してしまう → 結果として 全ユーザーデータにアクセス可能 になる(supabase.com)。
-
requesting_user_id()
は JWT を検証できず、NULL が返るため RLS 条件が成立しない。
2.2 変更後の実装
// app/lib/utils/supabase.ts
import { createClient, SupabaseClient } from "@supabase/supabase-js";
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/cloudflare";
import { getAuth } from "@clerk/remix/ssr.server";
export async function getSupabaseClient(
args: ActionFunctionArgs | LoaderFunctionArgs
): Promise<SupabaseClient> {
const { context } = args;
const env = context.cloudflare.env;
// 1) Clerk で認証&Supabase 用 JWT を取得
const { userId, getToken } = await getAuth(args);
const url = env.NEXT_PUBLIC_SUPABASE_URL!;
const anonKey = env.NEXT_PUBLIC_SUPABASE_KEY!;
// 2) 未認証時:anon キーのみで読み取り可
if (!userId) {
return createClient(url, anonKey);
}
// 3) Clerk の JWT を token として取得
const token = await getToken({ template: 'supabase' });
if (!token) throw new Error('Supabase 用 JWT が取得できませんでした');
// 4) anon キー + Authorization ヘッダー付きでクライアント生成
return createClient(url, anonKey, {
global: {
headers: {
Authorization: `Bearer ${token}`,
},
},
});
}
改修ポイント
- サービスロールキーを排除 → クライアント(anon)キーのみを使用し、RLS 制御を尊重(supabase.com)。
-
Authorization: Bearer <JWT> を送信 → Supabase が正しく JWT を検証して
request.jwt.claims
を設定(supabase.com)。 - 未認証時は anon キーのみ → 認証前の読み取り要件を担保。
3. RLS とクライアントの連携イメージ
. RLS とクライアントの連携イメージ
- クライアントは anon キー + JWT でリクエストを送信。
- Supabase は JWT を検証し、
request.jwt.claims
にsub
を保持。 - RLS ポリシー内の
requesting_user_id() = user_id
が TRUE となり、自分の行だけ読み書き可。
4. まとめ
- DB 側:ユニーク制約+デフォルト値+RLS ポリシー
- クライアント側:anon キー + Clerk JWT の Authorization
この2つを組み合わせることで、Clerk 認証ユーザーは自分の API キー情報のみを安全に扱えます。ぜひ本ガイドをコピー&ペーストして導入を進めてください!
Discussion