mcpField
S
Supabase Developer

Supabase Developer

github.com/daffy0208開発38184 回インストール44 週間前に更新
リポジトリ

Supabase Developer について

Supabase でフルスタックアプリを構築するチーム向けの開発スキルです。データベース設計とセキュリティ、認証、ストレージ、リアルタイム機能、Edge Functions、マイグレーションを扱います。

主な機能

  1. 01PostgreSQL スキーマと RLS ポリシーの設計
  2. 02認証とセッション管理の実装
  3. 03ファイルストレージとアクセス制御の管理
  4. 04リアルタイム購読とブロードキャストの構築
  5. 05Edge Functions の作成とデプロイ
  6. 06データベースマイグレーションの管理

活用シーン

  1. 01アプリへの登録とログインの追加
  2. 02Row Level Security によるユーザーデータ保護
  3. 03リアルタイム共同作業機能の構築
  4. 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 開発のライフサイクル全体を扱います。

  1. ✅ セットアップ:プロジェクトの初期化とクライアントの設定
  2. ✅ 認証:セッション管理を含む複数の認証戦略
  3. ✅ データベース:行レベルセキュリティを使用した PostgreSQL スキーマ設計
  4. ✅ ストレージ:アクセス制御を備えたファイル管理
  5. ✅ リアルタイム:ライブサブスクリプション、ブロードキャスト、プレゼンス
  6. ✅ エッジ関数:サーバーレス TypeScript 関数
  7. ✅ セキュリティ:本番アプリケーション向けのベストプラクティス
  8. ✅ パフォーマンス:大規模な運用に向けた最適化戦略
  9. ✅ テスト:単体テストと統合テストのパターン
  10. ✅ 移行:データベースの変更管理

重要なポイント:Supabase は PostgreSQL を中核とする完全なバックエンドプラットフォームを提供します。行レベルセキュリティは主要なセキュリティ層です。デフォルトでデータを安全に保てるよう、RLS ポリシーを慎重に設計してください。

複雑なシナリオでは、このスキルを次のものと組み合わせてください。

  • api-designer:カスタム API エンドポイント向け
  • security-engineer:高度なセキュリティレビュー向け
  • performance-optimizer:大規模アプリケーションのスケーリング向け
  • data-engineer:複雑なデータパイプライン向け

コメント