supabase-mcp-integration について
Supabase を使ったアプリ開発向けのスキルで、認証、PostgreSQL 操作、リアルタイム購読、ストレージ、TypeScript 連携を扱います。RLS の認可パターンと実装例も紹介します。
主な機能
- 01認証とセッション管理
- 02PostgreSQL のクエリと変更
- 03リアルタイム購読とブロードキャスト
- 04ファイルストレージ操作
- 05TypeScript の型連携
- 06RLS の認可パターン
活用シーン
- 01OAuth、MFA、パスワードレス認証の実装
- 02リアルタイム共同作業やダッシュボードの構築
- 03マルチテナントアプリの行単位認可の設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ manutej/ crush-mcp-server --skill supabase-mcp-integrationインストール後、Claude Code で/supabase-mcp-integrationを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
EXAMPLES.mdREADME.mdREFERENCE.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