mcpField
S
supabase-mcp-integration

supabase-mcp-integration

github.com/manutej開発144 回インストール48 週間前に更新
リポジトリ

supabase-mcp-integration について

Supabase を使ったアプリ開発向けのスキルで、認証、PostgreSQL 操作、リアルタイム購読、ストレージ、TypeScript 連携を扱います。RLS の認可パターンと実装例も紹介します。

主な機能

  1. 01認証とセッション管理
  2. 02PostgreSQL のクエリと変更
  3. 03リアルタイム購読とブロードキャスト
  4. 04ファイルストレージ操作
  5. 05TypeScript の型連携
  6. 06RLS の認可パターン

活用シーン

  1. 01OAuth、MFA、パスワードレス認証の実装
  2. 02リアルタイム共同作業やダッシュボードの構築
  3. 03マルチテナントアプリの行単位認可の設定

インストール

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

$npx skills add https://github.com/manutej/crush-mcp-server --skill supabase-mcp-integration

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

EXAMPLES.md
README.md
REFERENCE.md

詳細

Supabase MCP 統合

PostgreSQL を基盤とするオープンソースの Backend-as-a-Service プラットフォーム、Supabase を使用して、本番環境に対応したアプリケーションを構築するための包括的なスキルです。認証、データベース操作、リアルタイムサブスクリプション、ストレージ、TypeScript との統合、Row-Level Security のパターンについて説明します。

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

次のような場合に、このスキルを使用してください。

  • PostgreSQL バックエンドを使用して、フルスタックのウェブアプリケーションまたはモバイルアプリケーションを構築する場合
  • 認証(メール、OAuth、マジックリンク、MFA)とセッション管理を実装する場合
  • リアルタイムアプリケーション(チャット、コラボレーション、ライブダッシュボード)を作成する場合
  • 画像の最適化と CDN 配信を備えたファイルストレージを管理する場合
  • きめ細かな認可機能を備えたマルチテナント SaaS アプリケーションを構築する場合
  • Firebase から SQL ベースのバックエンドに移行する場合
  • TypeScript で型安全なデータベース操作が必要な場合
  • データベースの認可に Row-Level Security(RLS)を実装する場合
  • 複雑なクエリ、結合、リレーションを備えたアプリケーションを構築する場合
  • データベーススキーマから即時に REST/GraphQL API を設定する場合

コアコンセプト

Supabase プラットフォームのアーキテクチャ

Supabase は、エンタープライズグレードのオープンソースコンポーネントを基盤とする統合プラットフォームです。

主要コンポーネント:

  • PostgreSQL データベース:拡張機能(PostGIS、pg_vector)を備えた完全な Postgres
  • GoTrue(Auth):複数のプロバイダーに対応した JWT ベースの認証
  • PostgREST:データベーススキーマから自動生成される REST API
  • Realtime:データベースの変更、ブロードキャスト、プレゼンスに対応する WebSocket サーバー
  • Storage:CDN と画像最適化に対応した S3 互換のファイルストレージ
  • Edge Functions:グローバルに分散されたサーバーレス関数(Deno ランタイム)

統合クライアントライブラリ:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

// All features through single client
await supabase.auth.signIn()           // Authentication
await supabase.from('users').select()  // Database
supabase.channel('room').subscribe()   // Realtime
await supabase.storage.from().upload() // Storage
Row-Level Security(RLS)

PostgreSQL ポリシーを使用したデータベースレベルの認可です。

  • アクセスルールをデータベースに直接定義できます
  • すべてのクエリに自動的に適用されます
  • JWT 認証と統合されています
  • 行および列レベルで、きめ細かな制御が可能です
JWT ベースの認証

Supabase Auth は JSON Web Token を使用します。

  • 認証が成功すると発行されます
  • データベースクエリに自動的に含まれます
  • RLS ポリシーの評価に使用されます
  • 長時間のセッションにはリフレッシュトークンのフローを使用します
型安全性

データベーススキーマから TypeScript の型を自動生成します。

  • 稼働中のデータベースから型を生成できます
  • 型安全なクエリとミューテーションが可能です
  • コンパイル時にエラーを検出できます
  • IDE のオートコンプリートに対応しています

Supabase クライアントのセットアップ

インストール
# npm
npm install @supabase/supabase-js

# yarn
yarn add @supabase/supabase-js

# pnpm
pnpm add @supabase/supabase-js

# bun
bun add @supabase/supabase-js
環境設定
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xyzcompany.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# For server-side operations (keep secure!)
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

**セキュリティに関する注意:**クライアント側のコードで service_role キーを公開しないでください。

クライアントの初期化パターン(推奨)
// lib/supabase.ts

import { createClient, SupabaseClient } from '@supabase/supabase-js'
import { Database } from './database.types'

function validateEnvironment() {
  const url = process.env.NEXT_PUBLIC_SUPABASE_URL
  const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY

  if (!url) {
    throw new Error('Missing environment variable: NEXT_PUBLIC_SUPABASE_URL')
  }

  if (!anonKey) {
    throw new Error('Missing environment variable: NEXT_PUBLIC_SUPABASE_ANON_KEY')
  }

  return { url, anonKey }
}

let supabaseInstance: SupabaseClient<Database> | null = null

export function getSupabaseClient(): SupabaseClient<Database> {
  if (!supabaseInstance) {
    const { url, anonKey } = validateEnvironment()

    supabaseInstance = createClient<Database>(url, anonKey, {
      auth: {
        autoRefreshToken: true,
        persistSession: true,
        detectSessionInUrl: true
      },
      global: {
        headers: {
          'X-Application-Name': 'MyApp'
        }
      }
    })
  }

  return supabaseInstance
}

// Export singleton instance
export const supabase = getSupabaseClient()
設定オプション
const options = {
  // Database configuration
  db: {
    schema: 'public'  // Default schema
  },

  // Authentication configuration
  auth: {
    autoRefreshToken: true,     // Automatically refresh tokens
    persistSession: true,        // Persist session to localStorage
    detectSessionInUrl: true,    // Detect session from URL hash
    flowType: 'pkce',           // Use PKCE flow for OAuth
    storage: customStorage,      // Custom storage implementation
    storageKey: 'sb-auth-token' // Storage key for session
  },

  // Global configuration
  global: {
    headers: {
      'X-Application-Name': 'my-app',
      'apikey': SUPABASE_ANON_KEY
    },
    fetch: customFetch  // Custom fetch implementation
  },

  // Realtime configuration
  realtime: {
    params: {
      eventsPerSecond: 10
    },
    timeout: 10000,
    heartbeatInterval: 30000
  }
}

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, options)
プラットフォーム固有のセットアップ

AsyncStorage を使用する React Native:

import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  auth: {
    storage: AsyncStorage,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false
  }
})

Expo SecureStore を使用する React Native:

import * as SecureStore from 'expo-secure-store'
import { createClient } from '@supabase/supabase-js'

const ExpoSecureStoreAdapter = {
  getItem: (key: string) => SecureStore.getItemAsync(key),
  setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
  removeItem: (key: string) => SecureStore.deleteItemAsync(key)
}

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  auth: {
    storage: ExpoSecureStoreAdapter,
    autoRefreshToken: true,
    persistSession: true
  }
})

認証と認可

メールアドレス/パスワード認証

サインアップ:

const { data, error } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'secure-password',
  options: {
    data: {
      // Additional user metadata
      display_name: 'John Doe',
      avatar_url: 'https://example.com/avatar.jpg'
    },
    emailRedirectTo: 'https://yourapp.com/welcome'
  }
})

if (error) {
  console.error('Signup failed:', error.message)
  return
}

console.log('User created:', data.user)
console.log('Session:', data.session)

サインイン:

const { data, error } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'secure-password'
})

if (error) {
  console.error('Login failed:', error.message)
  return
}

console.log('User:', data.user)
console.log('Session token:', data.session?.access_token)
マジックリンク(パスワードレス)
const { data, error } = await supabase.auth.signInWithOtp({
  email: '[email protected]',
  options: {
    emailRedirectTo: 'https://yourapp.com/login',
    shouldCreateUser: true
  }
})

if (error) {
  console.error('Failed to send magic link:', error.message)
  return
}

console.log('Magic link sent')
ワンタイムパスワード(OTP)— 電話番号
// Send OTP
const { data, error } = await supabase.auth.signInWithOtp({
  phone: '+1234567890',
  options: {
    channel: 'sms' // or 'whatsapp'
  }
})

// Verify OTP
const { data: verifyData, error: verifyError } = await supabase.auth.verifyOtp({
  phone: '+1234567890',
  token: '123456',
  type: 'sms'
})
OAuth(ソーシャルログイン)
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://yourapp.com/auth/callback',
    scopes: 'email profile',
    queryParams: {
      access_type: 'offline',
      prompt: 'consent'
    }
  }
})

// Supported providers:
// apple, google, github, gitlab, bitbucket, discord, facebook,
// twitter, microsoft, linkedin, notion, slack, spotify, twitch, etc.
セッション管理
// Get current session
const { data: { session }, error } = await supabase.auth.getSession()

if (session) {
  console.log('Access token:', session.access_token)
  console.log('User:', session.user)
  console.log('Expires at:', session.expires_at)
}

// Get current user
const { data: { user }, error } = await supabase.auth.getUser()

// Refresh session
const { data, error } = await supabase.auth.refreshSession()

// Sign out
const { error } = await supabase.auth.signOut()
認証状態の変更
const { data: { subscription } } = supabase.auth.onAuthStateChange(
  (event, session) => {
    console.log('Auth event:', event)

    switch (event) {
      case 'SIGNED_IN':
        console.log('User signed in:', session?.user)
        break

      case 'SIGNED_OUT':
        console.log('User signed out')
        break

      case 'TOKEN_REFRESHED':
        console.log('Token refreshed')
        break

      case 'USER_UPDATED':
        console.log('User updated:', session?.user)
        break

      case 'PASSWORD_RECOVERY':
        console.log('Password recovery initiated')
        break
    }
  }
)

// Cleanup
subscription.unsubscribe()
ユーザー管理
// Update user
const { data, error } = await supabase.auth.updateUser({
  email: '[email protected]',
  password: 'new-password',
  data: {
    display_name: 'New Name',
    avatar_url: 'https://example.com/new-avatar.jpg'
  }
})

// Reset password
const { data, error } = await supabase.auth.resetPasswordForEmail(
  '[email protected]',
  {
    redirectTo: 'https://yourapp.com/reset-password'
  }
)

// Update password after reset
const { data: updateData, error: updateError } = await supabase.auth.updateUser({
  password: 'new-secure-password'
})
多要素認証(MFA)
// Enroll MFA
const { data: enrollData, error: enrollError } = await supabase.auth.mfa.enroll({
  factorType: 'totp',
  friendlyName: 'My Phone'
})

// Verify enrollment
const { data: verifyData, error: verifyError } = await supabase.auth.mfa.verify({
  factorId: enrollData.id,
  code: '123456'
})

// Challenge (during sign-in)
const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({
  factorId: 'factor-id'
})

// Verify challenge
const { data, error } = await supabase.auth.mfa.verify({
  factorId: 'factor-id',
  challengeId: challengeData.id,
  code: '123456'
})

データベース操作

SELECT クエリ

基本的な選択:

// Select all columns
const { data, error } = await supabase
  .from('users')
  .select()

// Select specific columns
const { data, error } = await supabase
  .from('users')
  .select('id, email, created_at')

フィルタリング:

// Equal
const { data } = await supabase
  .from('users')
  .select()
  .eq('status', 'active')

// Not equal
const { data } = await supabase
  .from('users')
  .select()
  .neq('role', 'admin')

// Greater than / Less than
const { data } = await supabase
  .from('products')
  .select()
  .gt('price', 100)
  .lte('stock', 10)

// In array
const { data } = await supabase
  .from('users')
  .select()
  .in('id', [1, 2, 3, 4, 5])

// Pattern matching
const { data } = await supabase
  .from('users')
  .select()
  .like('email', '%@gmail.com')

// Case-insensitive pattern matching
const { data } = await supabase
  .from('products')
  .select()
  .ilike('name', '%laptop%')

// Full text search
const { data } = await supabase
  .from('articles')
  .select()
  .textSearch('title', 'postgres database')

// Null checks
const { data } = await supabase
  .from('users')
  .select()
  .is('deleted_at', null)

並べ替えとページネーション:

// Order by
const { data } = await supabase
  .from('posts')
  .select()
  .order('created_at', { ascending: false })

// Multiple ordering
const { data } = await supabase
  .from('users')
  .select()
  .order('last_name', { ascending: true })
  .order('first_name', { ascending: true })

// Limit results
const { data } = await supabase
  .from('posts')
  .select()
  .limit(10)

// Pagination with range
const { data } = await supabase
  .from('posts')
  .select()
  .range(0, 9)  // First 10 items (0-indexed)

結合とネストされたクエリ:

// One-to-many relationship
const { data } = await supabase
  .from('users')
  .select(`
    id,
    email,
    posts (
      id,
      title,
      created_at
    )
  `)

// Many-to-many with junction table
const { data } = await supabase
  .from('users')
  .select(`
    id,
    email,
    user_roles (
      role:roles (
        id,
        name
      )
    )
  `)

// Nested filtering
const { data } = await supabase
  .from('users')
  .select(`
    id,
    email,
    posts!inner (
      id,
      title
    )
  `)
  .eq('posts.published', true)

集計:

// Count
const { count, error } = await supabase
  .from('users')
  .select('*', { count: 'exact', head: true })

// Count with filtering
const { count } = await supabase
  .from('users')
  .select('*', { count: 'exact', head: true })
  .eq('status', 'active')
INSERT 操作
// Insert single row
const { data, error } = await supabase
  .from('users')
  .insert({
    email: '[email protected]',
    name: 'John Doe',
    age: 30
  })
  .select()  // Return inserted row

// Insert multiple rows
const { data, error } = await supabase
  .from('users')
  .insert([
    { email: '[email protected]', name: 'User One' },
    { email: '[email protected]', name: 'User Two' },
    { email: '[email protected]', name: 'User Three' }
  ])
  .select()

// Upsert (Insert or Update)
const { data, error } = await supabase
  .from('users')
  .upsert({
    id: 1,
    email: '[email protected]',
    name: 'Updated Name'
  }, {
    onConflict: 'id'  // Conflict column(s)
  })
  .select()
UPDATE 操作
// Update with filter
const { data, error } = await supabase
  .from('users')
  .update({ status: 'inactive' })
  .eq('last_login', null)
  .select()

// Update single row by ID
const { data, error } = await supabase
  .from('users')
  .update({ name: 'New Name' })
  .eq('id', userId)
  .select()
  .single()

// Increment value
const { data, error } = await supabase
  .from('profiles')
  .update({ login_count: supabase.raw('login_count + 1') })
  .eq('id', userId)
DELETE 操作
// Delete with filter
const { error } = await supabase
  .from('users')
  .delete()
  .eq('status', 'banned')

// Delete single row
const { error } = await supabase
  .from('posts')
  .delete()
  .eq('id', postId)

// Soft delete pattern
const { error } = await supabase
  .from('users')
  .update({ deleted_at: new Date().toISOString() })
  .eq('id', userId)
RPC(リモートプロシージャコール)
// Call function without parameters
const { data, error } = await supabase
  .rpc('get_user_count')

// Call function with parameters
const { data, error } = await supabase
  .rpc('calculate_discount', {
    product_id: 123,
    user_id: 456
  })

リアルタイムサブスクリプション

データベース変更のサブスクリプション
// Listen to all changes
const channel = supabase
  .channel('db-changes')
  .on(
    'postgres_changes',
    {
      event: '*',          // All events: INSERT, UPDATE, DELETE
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('Change received:', payload)
      console.log('Event type:', payload.eventType)
      console.log('New data:', payload.new)
      console.log('Old data:', payload.old)
    }
  )
  .subscribe()

// Cleanup
channel.unsubscribe()

特定のイベントを監視する:

// INSERT only
const insertChannel = supabase
  .channel('post-inserts')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('New post created:', payload.new)
    }
  )
  .subscribe()

// UPDATE only
const updateChannel = supabase
  .channel('post-updates')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('Post updated:', payload.new)
    }
  )
  .subscribe()

// Filter changes for specific rows
const channel = supabase
  .channel('user-posts')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'posts',
      filter: `user_id=eq.${userId}`
    },
    (payload) => {
      console.log('User post changed:', payload)
    }
  )
  .subscribe()
ブロードキャストメッセージ
// Send broadcast
const channel = supabase.channel('room-1')

channel.subscribe((status) => {
  if (status === 'SUBSCRIBED') {
    channel.send({
      type: 'broadcast',
      event: 'cursor-move',
      payload: { x: 100, y: 200, user: 'Alice' }
    })
  }
})

// Receive broadcast
const channel = supabase
  .channel('room-1')
  .on('broadcast', { event: 'cursor-move' }, (payload) => {
    console.log('Cursor moved:', payload)
  })
  .subscribe()
プレゼンスの追跡
// Track user presence
const channel = supabase.channel('online-users')

// Set initial state
channel.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    await channel.track({
      user: 'user-1',
      online_at: new Date().toISOString(),
      status: 'online'
    })
  }
})

// Listen to presence changes
channel.on('presence', { event: 'sync' }, () => {
  const state = channel.presenceState()
  console.log('Online users:', state)
})

channel.on('presence', { event: 'join' }, ({ newPresences }) => {
  console.log('Users joined:', newPresences)
})

channel.on('presence', { event: 'leave' }, ({ leftPresences }) => {
  console.log('Users left:', leftPresences)
})

// Cleanup
channel.unsubscribe()

ストレージ操作

バケット管理
// List buckets
const { data: buckets, error } = await supabase
  .storage
  .listBuckets()

// Create bucket
const { data, error } = await supabase
  .storage
  .createBucket('avatars', {
    public: false,           // Private bucket
    fileSizeLimit: 1048576,  // 1MB limit
    allowedMimeTypes: ['image/png', 'image/jpeg']
  })

// Update bucket
const { data, error } = await supabase
  .storage
  .updateBucket('avatars', {
    public: true
  })

// Delete bucket
const { data, error } = await supabase
  .storage
  .deleteBucket('avatars')
ファイルのアップロード
// Standard upload
const file = event.target.files[0]
const filePath = `${userId}/${Date.now()}-${file.name}`

const { data, error } = await supabase
  .storage
  .from('avatars')
  .upload(filePath, file, {
    cacheControl: '3600',
    upsert: false
  })

// Upload with progress tracking
const { data, error } = await supabase
  .storage
  .from('videos')
  .upload(filePath, file, {
    onUploadProgress: (progress) => {
      const percent = (progress.loaded / progress.total) * 100
      console.log(`Upload progress: ${percent.toFixed(2)}%`)
    }
  })
ファイルのダウンロードと URL
// Download file
const { data, error } = await supabase
  .storage
  .from('avatars')
  .download('path/to/file.jpg')

// Get public URL (for public buckets)
const { data } = supabase
  .storage
  .from('avatars')
  .getPublicUrl('path/to/file.jpg')

// Create signed URL (for private buckets)
const { data, error } = await supabase
  .storage
  .from('private-files')
  .createSignedUrl('path/to/file.pdf', 60) // Expires in 60 seconds
画像変換
const { data } = supabase
  .storage
  .from('avatars')
  .getPublicUrl('user-avatar.jpg', {
    transform: {
      width: 200,
      height: 200,
      resize: 'cover',  // or 'contain', 'fill'
      quality: 80,
      format: 'webp'
    }
  })
ファイル管理
// List files
const { data, error } = await supabase
  .storage
  .from('avatars')
  .list('user-123', {
    limit: 100,
    offset: 0,
    sortBy: { column: 'name', order: 'asc' }
  })

// Delete files
const { data, error } = await supabase
  .storage
  .from('avatars')
  .remove(['path/to/file1.jpg', 'path/to/file2.jpg'])

// Move file
const { data, error } = await supabase
  .storage
  .from('avatars')
  .move('old/path/file.jpg', 'new/path/file.jpg')

// Copy file
const { data, error } = await supabase
  .storage
  .from('avatars')
  .copy('source/file.jpg', 'destination/file.jpg')

TypeScript との統合

データベース型の生成
# Install Supabase CLI
npm install -g supabase

# Login
supabase login

# Generate types
supabase gen types typescript --project-id YOUR_PROJECT_ID > database.types.ts
生成した型の使用
import { createClient } from '@supabase/supabase-js'
import { Database } from './database.types'

// Create typed client
const supabase = createClient<Database>(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!
)

// Type-safe queries
const { data, error } = await supabase
  .from('users')
  .select('id, email, created_at')
  .eq('id', userId)

// data is typed as:
// Array<{ id: string; email: string; created_at: string }> | null

// Type-safe inserts
const { data, error } = await supabase
  .from('posts')
  .insert({
    title: 'My Post',
    content: 'Content here',
    user_id: userId,
    published: true
  })
  .select()
ヘルパー型
import { Database } from './database.types'

// Get table row type
type User = Database['public']['Tables']['users']['Row']

// Get insert type
type NewUser = Database['public']['Tables']['users']['Insert']

// Get update type
type UserUpdate = Database['public']['Tables']['users']['Update']

// Get enum type
type UserRole = Database['public']['Enums']['user_role']

// Use in functions
function createUser(user: NewUser): Promise<User> {
  // Implementation
}

行レベルセキュリティ(RLS)

RLS を有効にする
-- Enable RLS on a table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
一般的な RLS パターン

公開読み取りアクセス:

CREATE POLICY "Public profiles are visible to everyone"
ON profiles
FOR SELECT
TO anon, authenticated
USING (true);

ユーザーが自身のデータのみを参照できる場合:

CREATE POLICY "Users can only see own data"
ON posts
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);

ユーザーが自身のデータのみを変更できる場合:

CREATE POLICY "Users can insert own posts"
ON posts
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can update own posts"
ON posts
FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can delete own posts"
ON posts
FOR DELETE
TO authenticated
USING (auth.uid() = user_id);

マルチテナントパターン:

CREATE POLICY "Users can only see data from own organization"
ON documents
FOR SELECT
TO authenticated
USING (
  organization_id IN (
    SELECT organization_id
    FROM user_organizations
    WHERE user_id = auth.uid()
  )
);

ロールベースのアクセス制御:

CREATE POLICY "Admin can see all users"
ON users
FOR SELECT
TO authenticated
USING (
  auth.jwt() ->> 'role' = 'admin'
  OR auth.uid() = id
);

ベストプラクティス

クライアントの初期化

シングルトンパターンを使用する:

  • クライアントのインスタンスを 1 つ作成し、アプリ全体で再利用します
  • リクエストごとに新しいクライアントを作成するのは避けます
  • モジュールレベルの変数またはコンテキストに保存します
エラー処理

必ずエラーを確認する:

const { data, error } = await supabase
  .from('users')
  .select()

if (error) {
  console.error('Error fetching users:', error.message)
  // Handle error appropriately
  return
}

// Use data safely
console.log(data)

Promise を拒否するには throwOnError() を使用する:

try {
  const { data } = await supabase
    .from('users')
    .insert({ name: 'John' })
    .throwOnError()

  console.log('User created:', data)
} catch (error) {
  console.error('Failed to create user:', error)
}
セキュリティ

サービスロールキーを決して公開しない:

  • クライアント側のコードでは anon キーを使用します
  • service_role キーはサーバー側のコードでのみ使用します
  • サービスロールキーはサーバー環境変数に保管します

必ず RLS を有効にする:

  • すべてのテーブルで RLS を有効にします
  • 各テーブルに適切なポリシーを作成します
  • ポリシーを十分にテストします

ユーザー入力を検証する:

  • クライアント側のデータを決して信用しないでください
  • データベースの制約と検証を使用します
  • クライアントとデータベースの両方で検証します
パフォーマンス

select を適切に使用する:

// Bad: Fetch all columns
const { data } = await supabase.from('users').select()

// Good: Only fetch needed columns
const { data } = await supabase.from('users').select('id, email')

ページネーションを使用する:

// Bad: Fetch all rows
const { data } = await supabase.from('posts').select()

// Good: Paginate results
const { data } = await supabase
  .from('posts')
  .select()
  .range(0, 9)
  .order('created_at', { ascending: false })

データベースの列にインデックスを付ける:

  • 頻繁にクエリされる列にインデックスを追加します
  • 外部キーにインデックスを付けます
  • 複数列のクエリには複合インデックスを使用します
接続管理

クライアントのインスタンスを再利用する:

  • リクエストごとに新しいクライアントを作成しないでください
  • クライアントの初期化にはシングルトンパターンを使用します
  • サーバー側ではコネクションプーリングを検討します

サブスクリプションをクリーンアップする:

useEffect(() => {
  const channel = supabase.channel('room-1')

  channel.subscribe(/* ... */)

  return () => {
    channel.unsubscribe()
  }
}, [])

一般的なパターンとワークフロー

ユーザー認証フロー
// 1. Sign up
const { data: signUpData, error: signUpError } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'secure-password'
})

// 2. Listen to auth state
supabase.auth.onAuthStateChange((event, session) => {
  if (event === 'SIGNED_IN') {
    // Redirect to dashboard
  }
})

// 3. Protected route check
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
  // Redirect to login
}

// 4. Sign out
await supabase.auth.signOut()
RLS を使用した CRUD
// Enable RLS on table
/*
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can CRUD own todos"
ON todos
FOR ALL
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
*/

// Create
const { data, error } = await supabase
  .from('todos')
  .insert({
    user_id: session.user.id,
    title: 'My Todo',
    completed: false
  })
  .select()

// Read (only user's own todos due to RLS)
const { data, error } = await supabase
  .from('todos')
  .select()

// Update
const { data, error } = await supabase
  .from('todos')
  .update({ completed: true })
  .eq('id', todoId)

// Delete
const { error } = await supabase
  .from('todos')
  .delete()
  .eq('id', todoId)
リアルタイムチャットの実装
import { useEffect, useState } from 'react'
import { supabase } from '@/lib/supabase'

function Chat({ roomId, userId }) {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    // Fetch existing messages
    const fetchMessages = async () => {
      const { data } = await supabase
        .from('messages')
        .select()
        .eq('room_id', roomId)
        .order('created_at', { ascending: true })

      if (data) setMessages(data)
    }

    fetchMessages()

    // Subscribe to new messages
    const channel = supabase
      .channel(`room-${roomId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'messages',
          filter: `room_id=eq.${roomId}`
        },
        (payload) => {
          setMessages((prev) => [...prev, payload.new])
        }
      )
      .subscribe()

    return () => {
      channel.unsubscribe()
    }
  }, [roomId])

  const sendMessage = async (content: string) => {
    await supabase.from('messages').insert({
      room_id: roomId,
      user_id: userId,
      content
    })
  }

  return (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>{msg.content}</div>
      ))}
    </div>
  )
}
進行状況付きファイルアップロード
async function uploadAvatar(file: File, userId: string) {
  const filePath = `${userId}/${Date.now()}-${file.name}`

  const { data, error } = await supabase.storage
    .from('avatars')
    .upload(filePath, file, {
      cacheControl: '3600',
      upsert: false,
      onUploadProgress: (progress) => {
        const percent = (progress.loaded / progress.total) * 100
        console.log(`Upload: ${percent.toFixed(2)}%`)
      }
    })

  if (error) {
    console.error('Upload failed:', error.message)
    return null
  }

  // Get public URL
  const { data: urlData } = supabase.storage
    .from('avatars')
    .getPublicUrl(filePath)

  // Update user profile with avatar URL
  await supabase
    .from('profiles')
    .update({ avatar_url: urlData.publicUrl })
    .eq('id', userId)

  return urlData.publicUrl
}

トラブルシューティング

よくある問題

RLS によってクエリがブロックされる:

  • RLS が有効か確認します:ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
  • ポリシーが存在し、ユースケースに合致することを確認します
  • 異なるユーザーコンテキストでポリシーをテストします
  • SELECT/UPDATE/DELETE には USING 句を使用します
  • INSERT/UPDATE には WITH CHECK 句を使用します

認証セッションが保持されない:

  • 設定で persistSession: true が指定されていることを確認します
  • ストレージ(localStorage)が利用可能か確認します
  • Cookie がブロックされていないことを確認します
  • サードパーティー Cookie が有効か確認します(OAuth の場合)

Realtime が動作しない:

  • Supabase ダッシュボードでテーブルの Realtime を有効にします
  • RLS ポリシーがサブスクリプションを許可しているか確認します
  • チャネルのサブスクリプションが成功していることを確認します
  • ネットワーク/ファイアウォールが WebSocket をブロックしていないか確認します

型生成エラー:

  • Supabase CLI がインストールされ、最新の状態であることを確認します
  • プロジェクト ID が正しいことを確認します
  • Supabase へのネットワーク接続を確認します
  • --debug フラグを付けて型を再生成してみます
RLS ポリシーのデバッグ
-- Test policy as specific user
SET request.jwt.claims.sub = 'user-uuid-here';

-- Run query to see what's visible
SELECT * FROM posts;

-- Reset to admin
RESET request.jwt.claims.sub;
パフォーマンスの問題

クエリが遅い:

  • 頻繁にクエリされる列にインデックスを追加します
  • EXPLAIN ANALYZE を使用してクエリプランを分析します
  • 不要な列の取得を避けます
  • 大規模なデータセットにはページネーションを使用します

接続数が多すぎる:

  • コネクションプーリングを使用します
  • クライアントのインスタンスを再利用します
  • 使用していないサブスクリプションを閉じます
  • サーバー側のロジックには Edge Functions の使用を検討します

本番環境へのデプロイ

環境設定
# Production .env
NEXT_PUBLIC_SUPABASE_URL=https://prod-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=prod-anon-key
SUPABASE_SERVICE_ROLE_KEY=prod-service-role-key

# Staging .env
NEXT_PUBLIC_SUPABASE_URL=https://staging-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=staging-anon-key
SUPABASE_SERVICE_ROLE_KEY=staging-service-role-key
データベースのマイグレーション

スキーマのマイグレーションには Supabase CLI を使用します:

# Initialize migrations
supabase migration new create_posts_table

# Apply migrations
supabase db push

# Generate migration from changes
supabase db diff -f create_users_table
モニタリング
  • Supabase ダッシュボードのモニタリングを有効にします
  • エラーとパフォーマンスの問題に対するアラートを設定します
  • データベース接続を監視します
  • API の使用状況とクォータを追跡します
  • 認証イベントのロギングを設定します
バックアップ戦略
  • Supabase ダッシュボードで自動バックアップを有効にします
  • ポイントインタイムリカバリーを設定します
  • 復元手順をテストします
  • スキーマとデータを定期的にエクスポートします
  • バックアップを別の場所に保管します
スケーリングに関する考慮事項
  • より高い上限を利用するには、Supabase のプランをアップグレードします
  • データベース接続プーリングを使用します
  • キャッシュ(Redis など)を実装します
  • 読み取り負荷が高い場合は、読み取りレプリカを検討します
  • 計算負荷の高い処理には Edge Functions を使用します
  • データベースのインデックスを最適化します
  • 時間のかかるクエリを監視し、最適化します

スキルのバージョン: 1.0.0 最終更新日: 2025年10月 スキルのカテゴリ: Backend-as-a-Service、データベース統合、認証、リアルタイム 対応対象: React、Next.js、Vue、Angular、React Native、Flutter、Node.js、Deno

コメント