🐕

# Supabase × Clerk 完全統合ガイド

に公開

Clerk で認証したユーザーが Supabase 上で「自分のデータのみ」を読み書きできるようにするには、Row-Level Security (RLS) の設定と、クライアント側での JWT 対置が必須です。本記事では次の2つをまとめて解説します。

  1. RLS テーブル設定とポリシー (SQL 完全版)
  2. 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}`,
      },
    },
  });
}

改修ポイント

  1. サービスロールキーを排除 → クライアント(anon)キーのみを使用し、RLS 制御を尊重(supabase.com)。
  2. Authorization: Bearer <JWT> を送信 → Supabase が正しく JWT を検証して request.jwt.claims を設定(supabase.com)。
  3. 未認証時は anon キーのみ → 認証前の読み取り要件を担保。

3. RLS とクライアントの連携イメージ

. RLS とクライアントの連携イメージ

  1. クライアントは anon キー + JWT でリクエストを送信。
  2. Supabase は JWT を検証し、request.jwt.claimssubを保持。
  3. RLS ポリシー内の requesting_user_id() = user_id が TRUE となり、自分の行だけ読み書き可。

4. まとめ

  • DB 側:ユニーク制約+デフォルト値+RLS ポリシー
  • クライアント側:anon キー + Clerk JWT の Authorization

この2つを組み合わせることで、Clerk 認証ユーザーは自分の API キー情報のみを安全に扱えます。ぜひ本ガイドをコピー&ペーストして導入を進めてください!

Discussion