supabase-expert について
supabase-expertは、Supabase連携の構築とトラブルシューティングを支援する開発者向けスキルです。データベース設計、Row Level Security、認証、Edge Functions、リアルタイム機能、TypeScript型を扱います。
主な機能
- 01データベーススキーマとリレーションの設計
- 02RLSポリシーの作成とデバッグ
- 03認証とミドルウェアの設定
- 04Edge Functionsとリアルタイム機能の構築
- 05スキーマからのTypeScript型生成
- 06クエリとアクセス問題の調査
活用シーン
- 01アクセスルールを含むSupabaseスキーマの設計
- 02RLSによる読み書き拒否のデバッグ
- 03アプリケーションの認証設定
- 04リアルタイムサブスクリプションやEdge Functionsの追加
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ jmsktm/ claude-settings --skill supabase-expertインストール後、Claude Code で/supabase-expertを入力して読み込まれたか確認します。
詳細
Supabase 統合エキスパートスキル
概要
このスキルを使うと、安全でスケーラブルな Supabase 統合を構築できます。データベース設計、Row Level Security(RLS)ポリシー、認証、Edge Functions、リアルタイム機能に使用してください。
基本原則
1. セキュリティを最優先する
- ユーザーデータを含むテーブルでは、必ず RLS を有効にします
- service role キーは、安全なサーバーコンテキストでのみ使用します
- クライアント側の操作には anon キーを使用します
- ポリシーを十分にテストします
2. 型安全性
- スキーマから TypeScript 型を生成します
- アプリケーションでは生成した型を使用します
- スキーマの変更に合わせて型を最新の状態に保ちます
3. パフォーマンス
- 頻繁にクエリされる列にはインデックスを使用します
- 大規模なデータセットにはページネーションを実装します
- 返却するフィールドを制限するには select() を使用します
- 適切な場合はキャッシュします
データベーススキーマの設計
基本的なテーブル作成
-- Create a table with standard fields
create table public.items (
id uuid default gen_random_uuid() primary key,
created_at timestamp with time zone default timezone('utc'::text, now()) not null,
updated_at timestamp with time zone default timezone('utc'::text, now()) not null,
user_id uuid references auth.users(id) on delete cascade not null,
title text not null,
description text,
status text default 'draft' check (status in ('draft', 'published', 'archived'))
);
-- Create updated_at trigger
create or replace function public.handle_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
create trigger set_updated_at
before update on public.items
for each row
execute function public.handle_updated_at();
-- Create index
create index items_user_id_idx on public.items(user_id);
create index items_status_idx on public.items(status);
外部キーとリレーション
-- One-to-many relationship
create table public.comments (
id uuid default gen_random_uuid() primary key,
created_at timestamp with time zone default now() not null,
item_id uuid references public.items(id) on delete cascade not null,
user_id uuid references auth.users(id) on delete cascade not null,
content text not null
);
-- Many-to-many relationship
create table public.item_tags (
item_id uuid references public.items(id) on delete cascade,
tag_id uuid references public.tags(id) on delete cascade,
primary key (item_id, tag_id)
);
Row Level Security(RLS)
基本的な RLS パターン
-- Enable RLS
alter table public.items enable row level security;
-- Users can read their own items
create policy "Users can read own items"
on public.items for select
using (auth.uid() = user_id);
-- Users can insert their own items
create policy "Users can insert own items"
on public.items for insert
with check (auth.uid() = user_id);
-- Users can update their own items
create policy "Users can update own items"
on public.items for update
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
-- Users can delete their own items
create policy "Users can delete own items"
on public.items for delete
using (auth.uid() = user_id);
高度な RLS パターン
-- Public read, authenticated write
create policy "Anyone can read published items"
on public.items for select
using (status = 'published');
create policy "Authenticated users can insert"
on public.items for insert
to authenticated
with check (true);
-- Role-based access
create policy "Admins can do everything"
on public.items for all
using (
exists (
select 1 from public.user_roles
where user_id = auth.uid()
and role = 'admin'
)
);
-- Shared access
create policy "Users can read shared items"
on public.items for select
using (
auth.uid() = user_id
or exists (
select 1 from public.item_shares
where item_id = items.id
and shared_with = auth.uid()
)
);
匿名/ゲストアクセス
-- Allow anonymous reads
create policy "Anonymous can read public content"
on public.items for select
to anon
using (status = 'published');
-- Allow anonymous inserts (for guest mode)
create policy "Anonymous can create items"
on public.items for insert
to anon
with check (true);
クライアント統合
クライアントのセットアップ(Next.js)
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createServerClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
},
}
)
}
CRUD 操作
// Query data
const { data, error } = await supabase
.from('items')
.select('*')
.eq('status', 'published')
.order('created_at', { ascending: false })
.limit(10)
// Insert data
const { data, error } = await supabase
.from('items')
.insert({ title: 'New Item', user_id: userId })
.select()
.single()
// Update data
const { data, error } = await supabase
.from('items')
.update({ title: 'Updated Title' })
.eq('id', itemId)
.select()
.single()
// Delete data
const { error } = await supabase
.from('items')
.delete()
.eq('id', itemId)
// Complex joins
const { data, error } = await supabase
.from('items')
.select(`
*,
comments (
id,
content,
user:user_id (
email
)
)
`)
.eq('user_id', userId)
リアルタイムサブスクリプション
// Subscribe to changes
const channel = supabase
.channel('items-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'items',
filter: `user_id=eq.${userId}`,
},
(payload) => {
console.log('Change received!', payload)
// Update local state
}
)
.subscribe()
// Cleanup
channel.unsubscribe()
認証
メールアドレス/パスワード認証
// Sign up
const { data, error } = await supabase.auth.signUp({
email: '[email protected]',
password: 'password123',
options: {
data: {
display_name: 'User Name',
},
},
})
// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'password123',
})
// Sign out
const { error } = await supabase.auth.signOut()
// Get current user
const { data: { user } } = await supabase.auth.getUser()
OAuth プロバイダー
// Google OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
})
// Handle callback
// app/auth/callback/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const code = searchParams.get('code')
if (code) {
const supabase = createServerClient()
await supabase.auth.exchangeCodeForSession(code)
}
return NextResponse.redirect(new URL('/dashboard', request.url))
}
認証ミドルウェア
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
const response = NextResponse.next()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: any) {
response.cookies.set(name, value, options)
},
remove(name: string, options: any) {
response.cookies.set(name, '', { ...options, maxAge: 0 })
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
// Redirect to login if not authenticated
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return response
}
export const config = {
matcher: ['/dashboard/:path*'],
}
Edge Functions
基本的な Edge Function
// supabase/functions/hello/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
try {
// Get Supabase client
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
}
)
// Get user from auth header
const authHeader = req.headers.get('Authorization')
const token = authHeader?.replace('Bearer ', '')
const { data: { user } } = await supabase.auth.getUser(token)
if (!user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
}
// Your logic here
const { data, error } = await supabase
.from('items')
.select('*')
.eq('user_id', user.id)
return new Response(
JSON.stringify({ data }),
{ headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
)
}
})
型の生成
# Generate TypeScript types
npx supabase gen types typescript --project-id your-project-id > types/supabase.ts
# Use in code
import { Database } from '@/types/supabase'
type Item = Database['public']['Tables']['items']['Row']
type ItemInsert = Database['public']['Tables']['items']['Insert']
type ItemUpdate = Database['public']['Tables']['items']['Update']
一般的なパターン
ソフトデリート
alter table public.items add column deleted_at timestamp with time zone;
create policy "Users cannot see deleted items"
on public.items for select
using (deleted_at is null);
-- Soft delete function
create or replace function soft_delete_item(item_id uuid)
returns void as $$
begin
update public.items
set deleted_at = now()
where id = item_id;
end;
$$ language plpgsql security definer;
監査ログ
create table public.audit_logs (
id uuid default gen_random_uuid() primary key,
created_at timestamp with time zone default now() not null,
user_id uuid references auth.users(id),
table_name text not null,
record_id uuid not null,
action text not null,
changes jsonb
);
-- Trigger function
create or replace function public.audit_trigger()
returns trigger as $$
begin
insert into public.audit_logs (user_id, table_name, record_id, action, changes)
values (
auth.uid(),
TG_TABLE_NAME,
NEW.id,
TG_OP,
to_jsonb(NEW) - to_jsonb(OLD)
);
return NEW;
end;
$$ language plpgsql security definer;
トラブルシューティング
よくある問題
- 401 エラー:RLS ポリシーを確認し、ユーザーが認証済みであることを確認します
- 403 エラー:RLS ポリシーによって操作がブロックされています
- 行が見つからない:ポリシーによって除外されている可能性があります
- 接続の問題:URL と API キーを確認します
- 型の不一致:スキーマの変更後に型を再生成します
RLS のデバッグ
-- Test as specific user
set request.jwt.claims = '{"sub": "user-uuid-here"}';
-- Check what policies apply
select * from pg_policies where tablename = 'items';
-- Disable RLS temporarily (for testing only!)
alter table public.items disable row level security;
ベストプラクティスのチェックリスト
- ユーザーデータを含むすべてのテーブルで RLS を有効にする
- 外部キーと頻繁にクエリされる列にインデックスを作成する
- 主キーには UUID を使用する
- created_at と updated_at のタイムスタンプを追加する
- 重要なデータにはソフトデリートを実装する
- 列挙型のフィールドには CHECK 制約を使用する
- TypeScript 型を生成して使用する
- RLS ポリシーを十分にテストする
- service role キーはサーバー側でのみ使用する
- 適切なエラーハンドリングを実装する
- 機密性の高い操作には監査ログを追加する
- 複数ステップの操作にはトランザクションを使用する
このスキルを使用する場面
次の作業では、このスキルを呼び出してください。
- データベーススキーマの設計
- RLS ポリシーの作成またはデバッグ
- 認証のセットアップ
- Edge Functions の構築
- リアルタイム機能の実装
- Supabase の問題のトラブルシューティング
- データベースクエリの最適化
- 型生成のセットアップ
コメント
Markdown 対応