Supabase Developer について
Supabase でフルスタックアプリを構築するチーム向けの開発スキルです。データベース設計とセキュリティ、認証、ストレージ、リアルタイム機能、Edge Functions、マイグレーションを扱います。
主な機能
- 01PostgreSQL スキーマと RLS ポリシーの設計
- 02認証とセッション管理の実装
- 03ファイルストレージとアクセス制御の管理
- 04リアルタイム購読とブロードキャストの構築
- 05Edge Functions の作成とデプロイ
- 06データベースマイグレーションの管理
活用シーン
- 01アプリへの登録とログインの追加
- 02Row Level Security によるユーザーデータ保護
- 03リアルタイム共同作業機能の構築
- 04ファイルアップロードとサーバーレスエンドポイントの処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ daffy0208/ ai-dev-standards --skill supabase-developerインストール後、Claude Code で/supabase-developerを入力して読み込まれたか確認します。
同梱ファイルmanifest.yamlREADME.md
詳細
Supabase 開発者向け
Supabase で本番環境に対応したフルスタックアプリケーションを構築します。
Supabase は、PostgreSQL データベース、認証、ストレージ、リアルタイムサブスクリプション、エッジ関数を提供するオープンソースの Firebase 代替サービスです。このスキルでは、Supabase の全機能を活用して、安全でスケーラブルなアプリケーションを構築する方法を説明します。
このスキルを使用する場面
- 認証:メール、OAuth、マジックリンク、電話番号認証を使ったユーザーのサインアップ/ログインを実装する
- データベース:Row Level Security(RLS)を使用した PostgreSQL スキーマを設計する
- ストレージ:ファイルのアップロード、ダウンロード、アクセス制御を管理する
- リアルタイム:サブスクリプションとブロードキャストを使ってライブ機能を構築する
- エッジ関数:エッジで実行するサーバーレス TypeScript 関数
- マイグレーション:データベーススキーマの変更を管理する
- 統合:Next.js、React、Vue、その他のフレームワークを接続する
Supabase の基本概念
1. データベース(PostgreSQL)
Supabase は拡張機能を備えた PostgreSQL を使用します。
- PostgREST:スキーマから REST API を自動生成します
- pg_graphql:オプションの GraphQL サポートです
- 拡張機能:埋め込み用の pgvector、スケジュール済みジョブ用の pg_cron などです
2. 認証
複数のプロバイダーに対応した組み込み認証機能です。
- メールアドレス/パスワードと確認
- マジックリンク(パスワードレス)
- OAuth(Google、GitHub など)
- 電話番号/SMS 認証
- SAML SSO(エンタープライズ向け)
3. Row Level Security(RLS)
データベースレベルでデータアクセスを強制する PostgreSQL ポリシーです。
- ユーザーは自分のデータのみ読み取れます
- 管理者はすべてのデータを読み取れます
- 誰でも読み取り可能、認証済みユーザーは書き込み可能
4. ストレージ
RLS を備えた S3 互換オブジェクトストレージです。
- パブリックバケットとプライベートバケット
- ファイルサイズと種類の制限
- オンデマンドの画像変換
- CDN 統合
5. リアルタイム
WebSocket ベースのサブスクリプションです。
- データベースの変更(INSERT、UPDATE、DELETE)
- チャネルへのブロードキャストメッセージ
- プレゼンス追跡(オンラインのユーザー)
6. エッジ関数
Deno ベースのサーバーレス関数です。
- エッジでグローバルにデプロイ
- TypeScript/JavaScript ランタイム
- バックグラウンドジョブと Webhook
- カスタム API エンドポイント
Supabase 実装の 6 つのフェーズ
フェーズ 1:プロジェクトのセットアップと設定
目標:Supabase プロジェクトを初期化し、アプリケーションに接続します
1.1 Supabase プロジェクトを作成する
# Option A: Web Dashboard
# 1. Go to https://supabase.com
# 2. Create new project
# 3. Save database password securely
# Option B: CLI (recommended for production)
npx supabase init
npx supabase start
1.2 クライアントライブラリをインストールする
# JavaScript/TypeScript
npm install @supabase/supabase-js
# React helpers (optional)
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs
# For Auth UI components
npm install @supabase/auth-ui-react @supabase/auth-ui-shared
1.3 環境設定
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # Server-side only!
1.4 クライアントを初期化する
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
Next.js 13+ App Router のパターン:
// lib/supabase/client.ts (Client Components)
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 (Server Components)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
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
}
}
}
)
}
フェーズ 2:認証の実装
目標:セッション管理を備えた安全なユーザー認証を実現します
2.1 認証方式
メールアドレス/パスワード認証:
// Sign up
async function signUp(email: string, password: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: 'https://yourapp.com/auth/callback'
}
})
if (error) throw error
return data
}
// Sign in
async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw error
return data
}
// Sign out
async function signOut() {
const { error } = await supabase.auth.signOut()
if (error) throw error
}
OAuth 認証:
// Google OAuth
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourapp.com/auth/callback',
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
})
if (error) throw error
return data
}
// GitHub, Twitter, Discord, etc. - same pattern
マジックリンク(パスワードレス):
async function signInWithMagicLink(email: string) {
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: 'https://yourapp.com/auth/callback'
}
})
if (error) throw error
return data
}
2.2 セッション管理
// Get current session
async function getSession() {
const {
data: { session },
error
} = await supabase.auth.getSession()
return session
}
// Get current user
async function getUser() {
const {
data: { user },
error
} = await supabase.auth.getUser()
return user
}
// Listen to auth changes
supabase.auth.onAuthStateChange((event, session) => {
console.log(event, session)
if (event === 'SIGNED_IN') {
// User signed in
}
if (event === 'SIGNED_OUT') {
// User signed out
}
if (event === 'TOKEN_REFRESHED') {
// Token refreshed
}
})
2.3 保護されたルート(Next.js)
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
const {
data: { session }
} = await supabase.auth.getSession()
// Protected routes
if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', req.url))
}
return res
}
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*']
}
フェーズ 3:データベース設計と RLS
目標:Row Level Security を備えた安全なデータベーススキーマを設計します
3.1 スキーマ設計
-- Example: Blog application schema
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Profiles table (extends auth.users)
CREATE TABLE profiles (
id UUID REFERENCES auth.users(id) PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
full_name TEXT,
avatar_url TEXT,
bio TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Posts table
CREATE TABLE posts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Comments table
CREATE TABLE comments (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
post_id UUID REFERENCES posts(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Indexes for performance
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX comments_post_id_idx ON comments(post_id);
3.2 Row Level Security(RLS)ポリシー
-- Enable RLS on all tables
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
-- Profiles: Users can read all, update only their own
CREATE POLICY "Public profiles are viewable by everyone"
ON profiles FOR SELECT
USING (true);
CREATE POLICY "Users can insert their own profile"
ON profiles FOR INSERT
WITH CHECK (auth.uid() = id);
CREATE POLICY "Users can update their own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
-- Posts: Public can read published, users can manage their own
CREATE POLICY "Published posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true OR auth.uid() = user_id);
CREATE POLICY "Users can create their own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete their own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
-- Comments: Public can read, users can manage their own
CREATE POLICY "Comments are viewable by everyone"
ON comments FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can create comments"
ON comments FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update their own comments"
ON comments FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "Users can delete their own comments"
ON comments FOR DELETE
USING (auth.uid() = user_id);
3.3 データベース関数
-- Automatic updated_at timestamp
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Apply to tables
CREATE TRIGGER handle_profiles_updated_at
BEFORE UPDATE ON profiles
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
CREATE TRIGGER handle_posts_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
-- Automatic profile creation on signup
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, username, full_name, avatar_url)
VALUES (
NEW.id,
NEW.raw_user_meta_data->>'username',
NEW.raw_user_meta_data->>'full_name',
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION handle_new_user();
3.4 TypeScript でのクエリ実行
// Insert
const { data, error } = await supabase
.from('posts')
.insert({
title: 'My First Post',
content: 'Hello World!',
user_id: userId
})
.select()
.single()
// Select with filters
const { data: posts } = await supabase
.from('posts')
.select('*, profiles(*), comments(*)')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(10)
// Update
const { data, error } = await supabase
.from('posts')
.update({ published: true })
.eq('id', postId)
.select()
// Delete
const { error } = await supabase.from('posts').delete().eq('id', postId)
// Count
const { count } = await supabase.from('posts').select('*', { count: 'exact', head: true })
// Full-text search
const { data } = await supabase.from('posts').select('*').textSearch('content', 'supabase', {
type: 'websearch',
config: 'english'
})
フェーズ 4:ストレージの実装
目標:アクセス制御を備えたファイルアップロードを管理します
4.1 バケットの設定
-- Create storage bucket
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);
INSERT INTO storage.buckets (id, name, public)
VALUES ('private-docs', 'private-docs', false);
4.2 ストレージの RLS ポリシー
-- Avatars: Anyone can read, users can upload their own
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');
CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "Users can update their own avatar"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'avatars' AND
auth.uid()::text = (storage.foldername(name))[1]
);
-- Private docs: Only owner can access
CREATE POLICY "Users can access their own documents"
ON storage.objects FOR SELECT
USING (
bucket_id = 'private-docs' AND
auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "Users can upload their own documents"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'private-docs' AND
auth.uid()::text = (storage.foldername(name))[1]
);
4.3 ファイルのアップロード/ダウンロード
// Upload file
async function uploadFile(bucket: string, path: string, file: File) {
const { data, error } = await supabase.storage.from(bucket).upload(path, file, {
cacheControl: '3600',
upsert: false
})
if (error) throw error
return data
}
// Download file
async function downloadFile(bucket: string, path: string) {
const { data, error } = await supabase.storage.from(bucket).download(path)
if (error) throw error
return data
}
// Get public URL
function getPublicUrl(bucket: string, path: string) {
const { data } = supabase.storage.from(bucket).getPublicUrl(path)
return data.publicUrl
}
// Get signed URL (private files)
async function getSignedUrl(bucket: string, path: string, expiresIn: number = 3600) {
const { data, error } = await supabase.storage.from(bucket).createSignedUrl(path, expiresIn)
if (error) throw error
return data.signedUrl
}
// Delete file
async function deleteFile(bucket: string, path: string) {
const { error } = await supabase.storage.from(bucket).remove([path])
if (error) throw error
}
// List files
async function listFiles(bucket: string, folder: string = '') {
const { data, error } = await supabase.storage.from(bucket).list(folder, {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' }
})
if (error) throw error
return data
}
4.4 画像変換
// Get resized image URL
function getTransformedImage(
bucket: string,
path: string,
options: {
width?: number
height?: number
quality?: number
}
) {
const { data } = supabase.storage.from(bucket).getPublicUrl(path, {
transform: {
width: options.width,
height: options.height,
quality: options.quality || 80
}
})
return data.publicUrl
}
フェーズ 5:リアルタイム機能
目標:ライブで共同作業できる機能を構築します
5.1 データベース変更のサブスクリプション
// Subscribe to INSERT events
const subscription = supabase
.channel('posts-channel')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts'
},
payload => {
console.log('New post created:', payload.new)
// Update UI with new post
}
)
.subscribe()
// Subscribe to all events on a table
supabase
.channel('comments-channel')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE
schema: 'public',
table: 'comments'
},
payload => {
console.log('Change detected:', payload)
}
)
.subscribe()
// Subscribe with filters
supabase
.channel('my-posts-channel')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: `user_id=eq.${userId}`
},
payload => {
console.log('My post changed:', payload)
}
)
.subscribe()
// Unsubscribe
subscription.unsubscribe()
5.2 ブロードキャストメッセージ
// Join a room and broadcast messages
const channel = supabase.channel('room-1')
// Send broadcast message
channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello!', user: 'John' }
})
// Receive broadcast messages
channel
.on('broadcast', { event: 'message' }, payload => {
console.log('Message received:', payload)
})
.subscribe()
5.3 プレゼンス追跡
// Track online users
const channel = supabase.channel('online-users', {
config: {
presence: {
key: userId
}
}
})
// Track current user presence
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', state)
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key, newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key, leftPresences)
})
.subscribe(async status => {
if (status === 'SUBSCRIBED') {
await channel.track({
user: userId,
online_at: new Date().toISOString()
})
}
})
// Update presence
await channel.track({ status: 'typing' })
// Stop tracking
await channel.untrack()
フェーズ 6:エッジ関数と高度な機能
目標:サーバーレス関数と高度な機能を利用します
6.1 エッジ関数
// 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 {
// Create Supabase client with service role
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
// Get user from auth header
const authHeader = req.headers.get('Authorization')!
const token = authHeader.replace('Bearer ', '')
const {
data: { user }
} = await supabaseClient.auth.getUser(token)
// Your logic here
const { data, error } = await supabaseClient.from('posts').select('*').eq('user_id', user?.id)
if (error) throw error
return new Response(JSON.stringify({ data }), {
headers: { 'Content-Type': 'application/json' }
})
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
})
}
})
エッジ関数をデプロイする:
# Deploy function
supabase functions deploy hello
# Invoke from client
const { data, error } = await supabase.functions.invoke('hello', {
body: { name: 'World' },
})
6.2 データベース Webhook
-- Send webhook on new post
CREATE OR REPLACE FUNCTION send_post_webhook()
RETURNS TRIGGER AS $$
BEGIN
PERFORM
net.http_post(
url := 'https://your-api.com/webhook',
headers := '{"Content-Type": "application/json"}'::jsonb,
body := jsonb_build_object(
'event', 'new_post',
'post_id', NEW.id,
'title', NEW.title
)
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER on_post_created
AFTER INSERT ON posts
FOR EACH ROW
EXECUTE FUNCTION send_post_webhook();
6.3 ベクトル検索(pgvector)
-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Add embedding column
ALTER TABLE posts ADD COLUMN embedding vector(1536);
-- Create vector index
CREATE INDEX ON posts USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Search by similarity
SELECT *
FROM posts
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
// Generate and store embeddings
import OpenAI from 'openai'
const openai = new OpenAI()
async function addEmbedding(postId: string, text: string) {
// Generate embedding
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text
})
const embedding = response.data[0].embedding
// Store in Supabase
await supabase.from('posts').update({ embedding }).eq('id', postId)
}
// Semantic search
async function semanticSearch(query: string) {
// Generate query embedding
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query
})
const queryEmbedding = response.data[0].embedding
// Search
const { data } = await supabase.rpc('match_posts', {
query_embedding: queryEmbedding,
match_threshold: 0.7,
match_count: 10
})
return data
}
データベースマイグレーションの管理
ローカル開発ワークフロー
# Initialize Supabase locally
supabase init
supabase start
# Create new migration
supabase migration new add_posts_table
# Edit migration file in supabase/migrations/
# Apply migrations
supabase db reset
# Generate TypeScript types
supabase gen types typescript --local > types/supabase.ts
本番環境へのデプロイ
# Link to remote project
supabase link --project-ref your-project-ref
# Push migrations to production
supabase db push
# Or apply specific migration
supabase db remote commit
セキュリティのベストプラクティス
1. サービスロールキーを決して公開しない
// ❌ WRONG - Never on client side
const supabase = createClient(url, SERVICE_ROLE_KEY)
// ✅ CORRECT - Use anon key on client
const supabase = createClient(url, ANON_KEY)
// ✅ Service role only on server
// app/api/admin/route.ts
const supabase = createClient(url, SERVICE_ROLE_KEY)
2. 常に RLS を使用する
-- ❌ WRONG - Table without RLS
CREATE TABLE sensitive_data (
id UUID PRIMARY KEY,
secret TEXT
);
-- ✅ CORRECT - RLS enabled
CREATE TABLE sensitive_data (
id UUID PRIMARY KEY,
user_id UUID REFERENCES auth.users(id),
secret TEXT
);
ALTER TABLE sensitive_data ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can only access their data"
ON sensitive_data FOR ALL
USING (auth.uid() = user_id);
3. 入力を検証する
// ❌ WRONG - No validation
await supabase.from('posts').insert({ title: userInput })
// ✅ CORRECT - Validate first
import { z } from 'zod'
const schema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(10).max(10000)
})
const validated = schema.parse(userInput)
await supabase.from('posts').insert(validated)
4. レート制限
// Use Edge Functions for rate limiting
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
// Check rate limit
const { count } = await supabase
.from('api_calls')
.select('*', { count: 'exact', head: true })
.eq('user_id', userId)
.gte('created_at', oneHourAgo)
if (count >= 100) {
return new Response('Rate limit exceeded', { status: 429 })
}
パフォーマンスの最適化
1. インデックスを使用する
-- Add indexes on frequently queried columns
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
-- Composite indexes for multi-column queries
CREATE INDEX posts_user_published_idx ON posts(user_id, published);
-- Full-text search indexes
CREATE INDEX posts_content_fts_idx ON posts USING gin(to_tsvector('english', content));
2. 必要なデータだけを選択する
// ❌ WRONG - Select everything
const { data } = await supabase.from('posts').select('*')
// ✅ CORRECT - Select specific columns
const { data } = await supabase.from('posts').select('id, title, created_at')
3. ページネーションを使用する
// Offset pagination
const { data } = await supabase.from('posts').select('*').range(0, 9)
// Cursor pagination (better for large datasets)
const { data } = await supabase
.from('posts')
.select('*')
.gt('created_at', lastCreatedAt)
.order('created_at', { ascending: false })
.limit(10)
4. 静的データをキャッシュする
// Use React Query or SWR
import { useQuery } from '@tanstack/react-query'
function usePosts() {
return useQuery({
queryKey: ['posts'],
queryFn: async () => {
const { data } = await supabase.from('posts').select('*')
return data
},
staleTime: 5 * 60 * 1000 // 5 minutes
})
}
テスト
単体テスト
// Mock Supabase client
import { createClient } from '@supabase/supabase-js'
jest.mock('@supabase/supabase-js', () => ({
createClient: jest.fn(() => ({
from: jest.fn(() => ({
select: jest.fn(() => ({
eq: jest.fn(() => ({
single: jest.fn(() =>
Promise.resolve({
data: { id: '1', title: 'Test' },
error: null
})
)
}))
}))
}))
}))
}))
test('fetches post by id', async () => {
const post = await getPostById('1')
expect(post.title).toBe('Test')
})
統合テスト
// Use test database
const testSupabase = createClient(
process.env.TEST_SUPABASE_URL!,
process.env.TEST_SUPABASE_ANON_KEY!
)
beforeEach(async () => {
// Clean database
await testSupabase.from('posts').delete().neq('id', '00000000-0000-0000-0000-000000000000')
})
test('creates post', async () => {
const { data, error } = await testSupabase
.from('posts')
.insert({ title: 'Test Post', content: 'Content' })
.select()
.single()
expect(error).toBeNull()
expect(data.title).toBe('Test Post')
})
一般的なパターン
1. 楽観的な UI 更新
import { useMutation, useQueryClient } from '@tanstack/react-query'
function useCreatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async newPost => {
const { data } = await supabase.from('posts').insert(newPost).select().single()
return data
},
onMutate: async newPost => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts'] })
// Snapshot previous value
const previousPosts = queryClient.getQueryData(['posts'])
// Optimistically update
queryClient.setQueryData(['posts'], old => [...old, newPost])
return { previousPosts }
},
onError: (err, newPost, context) => {
// Rollback on error
queryClient.setQueryData(['posts'], context.previousPosts)
},
onSettled: () => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ['posts'] })
}
})
}
2. ソフトデリート
-- Add deleted_at column
ALTER TABLE posts ADD COLUMN deleted_at TIMESTAMP WITH TIME ZONE;
-- Update RLS to exclude deleted
CREATE POLICY "Only show non-deleted posts"
ON posts FOR SELECT
USING (deleted_at IS NULL);
-- Soft delete function
CREATE OR REPLACE FUNCTION soft_delete_post(post_id UUID)
RETURNS void AS $$
BEGIN
UPDATE posts
SET deleted_at = NOW()
WHERE id = post_id AND user_id = auth.uid();
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
3. 監査ログ
-- Create audit log table
CREATE TABLE audit_logs (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
table_name TEXT NOT NULL,
record_id UUID NOT NULL,
action TEXT NOT NULL,
old_data JSONB,
new_data JSONB,
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Audit trigger function
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_logs (table_name, record_id, action, old_data, new_data, user_id)
VALUES (
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
TG_OP,
CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW) ELSE NULL END,
auth.uid()
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Apply to tables
CREATE TRIGGER audit_posts
AFTER INSERT OR UPDATE OR DELETE ON posts
FOR EACH ROW
EXECUTE FUNCTION audit_trigger();
トラブルシューティング
問題:RLS ポリシーが機能しない
症状:正しいポリシーを設定してもデータをクエリできません
解決策:
-- Check if RLS is enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
-- Check policies
SELECT * FROM pg_policies WHERE tablename = 'your_table';
-- Test policy as user
SET LOCAL ROLE authenticated;
SET LOCAL request.jwt.claims.sub TO 'user-uuid';
SELECT * FROM your_table;
問題:「JWT expired」エラー
解決策:
// Auto-refresh tokens
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'TOKEN_REFRESHED') {
// Token refreshed automatically
}
})
// Manual refresh
const { data, error } = await supabase.auth.refreshSession()
問題:ストレージへのアップロードに失敗する
解決策:
// Check file size (default: 50MB)
// Check MIME type restrictions
// Verify bucket exists
const { data: buckets } = await supabase.storage.listBuckets()
// Check RLS policies on storage.objects
問題:リアルタイム機能が動作しない
解決策:
-- Enable replication for table
ALTER PUBLICATION supabase_realtime ADD TABLE posts;
-- Check if table is in publication
SELECT * FROM pg_publication_tables WHERE pubname = 'supabase_realtime';
クイックリファレンス
基本的なコマンド
# Local development
supabase init
supabase start
supabase stop
supabase status
# Migrations
supabase migration new migration_name
supabase db reset
supabase db push
# Type generation
supabase gen types typescript --local > types/supabase.ts
# Edge Functions
supabase functions new function_name
supabase functions serve
supabase functions deploy function_name
# Link to remote
supabase link --project-ref your-ref
よく使うクエリ
// CRUD operations
const { data } = await supabase.from('table').select('*')
const { data } = await supabase.from('table').insert({ ... })
const { data } = await supabase.from('table').update({ ... }).eq('id', id)
const { data } = await supabase.from('table').delete().eq('id', id)
// Filters
.eq('column', value)
.neq('column', value)
.gt('column', value)
.gte('column', value)
.lt('column', value)
.lte('column', value)
.like('column', '%pattern%')
.ilike('column', '%pattern%')
.is('column', null)
.in('column', [1, 2, 3])
.contains('array_column', ['value'])
.textSearch('column', 'query')
// Modifiers
.order('column', { ascending: false })
.limit(10)
.range(0, 9)
.single()
.maybeSingle()
統合例
Next.js 13+ App Router
クライアント/サーバーの設定パターンについては、フェーズ 1 を参照してください。
React + Vite
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
)
// src/hooks/useAuth.ts
export function useAuth() {
const [user, setUser] = useState(null)
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null)
})
const {
data: { subscription }
} = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null)
})
return () => subscription.unsubscribe()
}, [])
return { user }
}
まとめ
このスキルでは、Supabase 開発のライフサイクル全体を扱います。
- ✅ セットアップ:プロジェクトの初期化とクライアントの設定
- ✅ 認証:セッション管理を含む複数の認証戦略
- ✅ データベース:行レベルセキュリティを使用した PostgreSQL スキーマ設計
- ✅ ストレージ:アクセス制御を備えたファイル管理
- ✅ リアルタイム:ライブサブスクリプション、ブロードキャスト、プレゼンス
- ✅ エッジ関数:サーバーレス TypeScript 関数
- ✅ セキュリティ:本番アプリケーション向けのベストプラクティス
- ✅ パフォーマンス:大規模な運用に向けた最適化戦略
- ✅ テスト:単体テストと統合テストのパターン
- ✅ 移行:データベースの変更管理
重要なポイント:Supabase は PostgreSQL を中核とする完全なバックエンドプラットフォームを提供します。行レベルセキュリティは主要なセキュリティ層です。デフォルトでデータを安全に保てるよう、RLS ポリシーを慎重に設計してください。
複雑なシナリオでは、このスキルを次のものと組み合わせてください。
- api-designer:カスタム API エンドポイント向け
- security-engineer:高度なセキュリティレビュー向け
- performance-optimizer:大規模アプリケーションのスケーリング向け
- data-engineer:複雑なデータパイプライン向け