mcpField

supabase-expert

github.com/eddiebe147開発128 回インストール
リポジトリ

supabase-expert について

Supabase を使う開発者向けの統合ガイドスキルです。データベース設計、RLS、認証、Edge Functions、リアルタイム機能に加え、型生成やクエリの改善、問題の調査を扱います。

主な機能

  1. 01テーブルとリレーションの設計
  2. 02RLS ポリシーの作成とデバッグ
  3. 03認証と OAuth の設定
  4. 04Edge Functions の構築
  5. 05TypeScript 型生成とクエリ改善

活用シーン

  1. 01ユーザーデータのアクセス制御設定
  2. 02アプリへの Supabase 認証の組み込み
  3. 03リアルタイム購読や Edge Functions の実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/eddiebe147/claude-settings --skill supabase-expert

インストール後、Claude Code で/supabase-expertを入力して読み込まれたか確認します。

詳細

Supabase 統合エキスパートスキル

概要

このスキルを使うと、安全でスケーラブルな Supabase 統合を構築できます。データベース設計、Row Level Security(RLS)ポリシー、認証、Edge Functions、リアルタイム機能に使用してください。

基本原則

1. セキュリティを最優先する
  • ユーザーデータを含むテーブルでは、必ず RLS を有効にします
  • service role キーは、安全なサーバーコンテキストでのみ使用します
  • クライアント側の操作には anon キーを使用します
  • ポリシーを十分にテストします
2. 型安全性
  • スキーマから TypeScript 型を生成します
  • アプリケーションでは生成した型を使用します
  • スキーマの変更に合わせて型を最新の状態に保ちます
3. パフォーマンス
  • 頻繁にクエリされる列にはインデックスを使用します
  • 大規模なデータセットにはページネーションを実装します
  • 返却するフィールドを制限するには select() を使用します
  • 適切な場合はキャッシュします

データベーススキーマの設計

基本的なテーブル作成
-- Create a table with standard fields
create table public.items (
  id uuid default gen_random_uuid() primary key,
  created_at timestamp with time zone default timezone('utc'::text, now()) not null,
  updated_at timestamp with time zone default timezone('utc'::text, now()) not null,
  user_id uuid references auth.users(id) on delete cascade not null,
  title text not null,
  description text,
  status text default 'draft' check (status in ('draft', 'published', 'archived'))
);

-- Create updated_at trigger
create or replace function public.handle_updated_at()
returns trigger as $$
begin
  new.updated_at = now();
  return new;
end;
$$ language plpgsql;

create trigger set_updated_at
  before update on public.items
  for each row
  execute function public.handle_updated_at();

-- Create index
create index items_user_id_idx on public.items(user_id);
create index items_status_idx on public.items(status);
外部キーとリレーション
-- One-to-many relationship
create table public.comments (
  id uuid default gen_random_uuid() primary key,
  created_at timestamp with time zone default now() not null,
  item_id uuid references public.items(id) on delete cascade not null,
  user_id uuid references auth.users(id) on delete cascade not null,
  content text not null
);

-- Many-to-many relationship
create table public.item_tags (
  item_id uuid references public.items(id) on delete cascade,
  tag_id uuid references public.tags(id) on delete cascade,
  primary key (item_id, tag_id)
);

Row Level Security(RLS)

基本的な RLS パターン
-- Enable RLS
alter table public.items enable row level security;

-- Users can read their own items
create policy "Users can read own items"
  on public.items for select
  using (auth.uid() = user_id);

-- Users can insert their own items
create policy "Users can insert own items"
  on public.items for insert
  with check (auth.uid() = user_id);

-- Users can update their own items
create policy "Users can update own items"
  on public.items for update
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

-- Users can delete their own items
create policy "Users can delete own items"
  on public.items for delete
  using (auth.uid() = user_id);
高度な RLS パターン
-- Public read, authenticated write
create policy "Anyone can read published items"
  on public.items for select
  using (status = 'published');

create policy "Authenticated users can insert"
  on public.items for insert
  to authenticated
  with check (true);

-- Role-based access
create policy "Admins can do everything"
  on public.items for all
  using (
    exists (
      select 1 from public.user_roles
      where user_id = auth.uid()
      and role = 'admin'
    )
  );

-- Shared access
create policy "Users can read shared items"
  on public.items for select
  using (
    auth.uid() = user_id
    or exists (
      select 1 from public.item_shares
      where item_id = items.id
      and shared_with = auth.uid()
    )
  );
匿名/ゲストアクセス
-- Allow anonymous reads
create policy "Anonymous can read public content"
  on public.items for select
  to anon
  using (status = 'published');

-- Allow anonymous inserts (for guest mode)
create policy "Anonymous can create items"
  on public.items for insert
  to anon
  with check (true);

クライアント統合

クライアントのセットアップ(Next.js)
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export function createServerClient() {
  const cookieStore = cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
      },
    }
  )
}
CRUD 操作
// Query data
const { data, error } = await supabase
  .from('items')
  .select('*')
  .eq('status', 'published')
  .order('created_at', { ascending: false })
  .limit(10)

// Insert data
const { data, error } = await supabase
  .from('items')
  .insert({ title: 'New Item', user_id: userId })
  .select()
  .single()

// Update data
const { data, error } = await supabase
  .from('items')
  .update({ title: 'Updated Title' })
  .eq('id', itemId)
  .select()
  .single()

// Delete data
const { error } = await supabase
  .from('items')
  .delete()
  .eq('id', itemId)

// Complex joins
const { data, error } = await supabase
  .from('items')
  .select(`
    *,
    comments (
      id,
      content,
      user:user_id (
        email
      )
    )
  `)
  .eq('user_id', userId)
リアルタイムサブスクリプション
// Subscribe to changes
const channel = supabase
  .channel('items-changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'items',
      filter: `user_id=eq.${userId}`,
    },
    (payload) => {
      console.log('Change received!', payload)
      // Update local state
    }
  )
  .subscribe()

// Cleanup
channel.unsubscribe()

認証

メールアドレス/パスワード認証
// Sign up
const { data, error } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'password123',
  options: {
    data: {
      display_name: 'User Name',
    },
  },
})

// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'password123',
})

// Sign out
const { error } = await supabase.auth.signOut()

// Get current user
const { data: { user } } = await supabase.auth.getUser()
OAuth プロバイダー
// Google OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
  },
})

// Handle callback
// app/auth/callback/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const code = searchParams.get('code')

  if (code) {
    const supabase = createServerClient()
    await supabase.auth.exchangeCodeForSession(code)
  }

  return NextResponse.redirect(new URL('/dashboard', request.url))
}
認証ミドルウェア
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  const response = NextResponse.next()

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return request.cookies.get(name)?.value
        },
        set(name: string, value: string, options: any) {
          response.cookies.set(name, value, options)
        },
        remove(name: string, options: any) {
          response.cookies.set(name, '', { ...options, maxAge: 0 })
        },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()

  // Redirect to login if not authenticated
  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return response
}

export const config = {
  matcher: ['/dashboard/:path*'],
}

Edge Functions

基本的な Edge Function
// supabase/functions/hello/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  try {
    // Get Supabase client
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_ANON_KEY') ?? '',
      {
        auth: {
          autoRefreshToken: false,
          persistSession: false,
        },
      }
    )

    // Get user from auth header
    const authHeader = req.headers.get('Authorization')
    const token = authHeader?.replace('Bearer ', '')
    const { data: { user } } = await supabase.auth.getUser(token)

    if (!user) {
      return new Response(
        JSON.stringify({ error: 'Unauthorized' }),
        { status: 401, headers: { 'Content-Type': 'application/json' } }
      )
    }

    // Your logic here
    const { data, error } = await supabase
      .from('items')
      .select('*')
      .eq('user_id', user.id)

    return new Response(
      JSON.stringify({ data }),
      { headers: { 'Content-Type': 'application/json' } }
    )
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 500, headers: { 'Content-Type': 'application/json' } }
    )
  }
})

型の生成

# Generate TypeScript types
npx supabase gen types typescript --project-id your-project-id > types/supabase.ts

# Use in code
import { Database } from '@/types/supabase'

type Item = Database['public']['Tables']['items']['Row']
type ItemInsert = Database['public']['Tables']['items']['Insert']
type ItemUpdate = Database['public']['Tables']['items']['Update']

一般的なパターン

ソフトデリート
alter table public.items add column deleted_at timestamp with time zone;

create policy "Users cannot see deleted items"
  on public.items for select
  using (deleted_at is null);

-- Soft delete function
create or replace function soft_delete_item(item_id uuid)
returns void as $$
begin
  update public.items
  set deleted_at = now()
  where id = item_id;
end;
$$ language plpgsql security definer;
監査ログ
create table public.audit_logs (
  id uuid default gen_random_uuid() primary key,
  created_at timestamp with time zone default now() not null,
  user_id uuid references auth.users(id),
  table_name text not null,
  record_id uuid not null,
  action text not null,
  changes jsonb
);

-- Trigger function
create or replace function public.audit_trigger()
returns trigger as $$
begin
  insert into public.audit_logs (user_id, table_name, record_id, action, changes)
  values (
    auth.uid(),
    TG_TABLE_NAME,
    NEW.id,
    TG_OP,
    to_jsonb(NEW) - to_jsonb(OLD)
  );
  return NEW;
end;
$$ language plpgsql security definer;

トラブルシューティング

よくある問題
  1. 401 エラー:RLS ポリシーを確認し、ユーザーが認証済みであることを確認します
  2. 403 エラー:RLS ポリシーによって操作がブロックされています
  3. 行が見つからない:ポリシーによって除外されている可能性があります
  4. 接続の問題:URL と API キーを確認します
  5. 型の不一致:スキーマの変更後に型を再生成します
RLS のデバッグ
-- Test as specific user
set request.jwt.claims = '{"sub": "user-uuid-here"}';

-- Check what policies apply
select * from pg_policies where tablename = 'items';

-- Disable RLS temporarily (for testing only!)
alter table public.items disable row level security;

ベストプラクティスのチェックリスト

  • ユーザーデータを含むすべてのテーブルで RLS を有効にする
  • 外部キーと頻繁にクエリされる列にインデックスを作成する
  • 主キーには UUID を使用する
  • created_at と updated_at のタイムスタンプを追加する
  • 重要なデータにはソフトデリートを実装する
  • 列挙型のフィールドには CHECK 制約を使用する
  • TypeScript 型を生成して使用する
  • RLS ポリシーを十分にテストする
  • service role キーはサーバー側でのみ使用する
  • 適切なエラーハンドリングを実装する
  • 機密性の高い操作には監査ログを追加する
  • 複数ステップの操作にはトランザクションを使用する

このスキルを使用する場面

次の作業では、このスキルを呼び出してください。

  • データベーススキーマの設計
  • RLS ポリシーの作成またはデバッグ
  • 認証のセットアップ
  • Edge Functions の構築
  • リアルタイム機能の実装
  • Supabase の問題のトラブルシューティング
  • データベースクエリの最適化
  • 型生成のセットアップ

コメント