# Supabaseの使い方
Firebaseのオープンソース代替として使っているSupabaseのメモ。PostgreSQLデータベース、認証、リアルタイム機能、ストレージなどを提供。
# セットアップ
# プロジェクト作成
- Supabase公式サイト (opens new window)でアカウント作成
- 「New Project」でプロジェクト作成
- Project URLとAPIキーを取得
セキュリティ
service_role keyは絶対にフロントエンドに公開しない。RLSをバイパスできる。
ローカル開発環境についてはSupabase CLIの使い方を参照。
# クライアントライブラリのインストール
npm install @supabase/supabase-js
# クライアントの初期化
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = 'YOUR_SUPABASE_URL'
const supabaseAnonKey = 'YOUR_SUPABASE_ANON_KEY'
const supabase = createClient(supabaseUrl, supabaseAnonKey)
環境変数で管理する場合:
// .env.local
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
// コード内
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)
# 認証機能
# ユーザー登録
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
})
# ログイン
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password123',
})
# ログアウト
const { error } = await supabase.auth.signOut()
# 現在のユーザーを取得
const { data: { user } } = await supabase.auth.getUser()
# 認証状態の監視
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
console.log('ログインしました')
} else if (event === 'SIGNED_OUT') {
console.log('ログアウトしました')
}
})
# OAuth認証(Google)
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://your-app.com/auth/callback'
}
})
# データベース操作
# テーブルの作成
SupabaseダッシュボードのSQL Editorで実行:
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL
);
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "誰でも読み取り可能" ON posts
FOR SELECT USING (true);
CREATE POLICY "認証済みユーザーのみ書き込み可能" ON posts
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
# データの取得(SELECT)
// 全件取得
const { data, error } = await supabase
.from('posts')
.select('*')
// 条件付き取得
const { data, error } = await supabase
.from('posts')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false })
// 単一レコード取得
const { data, error } = await supabase
.from('posts')
.select('*')
.eq('id', postId)
.single()
// リレーションを含む取得
const { data, error } = await supabase
.from('posts')
.select(`
*,
profiles:user_id (
username,
avatar_url
)
`)
# データの挿入(INSERT)
const { data, error } = await supabase
.from('posts')
.insert([
{
title: '新しい投稿',
content: 'これは投稿の内容です',
user_id: userId
}
])
.select()
# データの更新(UPDATE)
const { data, error } = await supabase
.from('posts')
.update({ title: '更新されたタイトル' })
.eq('id', postId)
.select()
# データの削除(DELETE)
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
# フィルタリング
.eq('status', 'published') // 等しい
.neq('status', 'draft') // 等しくない
.gt('views', 100) // より大きい
.lt('price', 1000) // より小さい
.gte('age', 18) // 以上
.lte('age', 65) // 以下
.in('status', ['published', 'archived']) // 含む(配列)
.not('status', 'in', ['draft']) // 含まない(配列)
.like('title', '%JavaScript%') // 部分一致(LIKE)
.ilike('title', '%javascript%') // 大文字小文字を区別しない部分一致
.is('deleted_at', null) // NULLチェック
.not('deleted_at', 'is', null) // NOT NULLチェック
# ページネーション
// オフセットベース
const { data, error } = await supabase
.from('posts')
.select('*')
.range(0, 9) // 最初の10件
// カーソルベース(推奨)
const { data, error } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
.limit(10)
# リアルタイム機能
# チャンネルへの購読
const channel = supabase
.channel('posts-channel')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('変更:', payload)
}
)
.subscribe()
// 購読を解除
channel.unsubscribe()
# 特定のイベントのみ購読
const channel = supabase
.channel('posts-channel')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts',
filter: 'user_id=eq.' + userId
},
(payload) => {
console.log('新しい投稿:', payload.new)
}
)
.subscribe()
# ストレージ機能
# ファイルのアップロード
const { data, error } = await supabase.storage
.from('avatars')
.upload('user-avatar.jpg', file)
# ファイルのダウンロード
const { data, error } = await supabase.storage
.from('avatars')
.download('user-avatar.jpg')
// dataはBlobオブジェクト
const url = URL.createObjectURL(data)
# 公開URLの取得
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('user-avatar.jpg')
console.log('公開URL:', data.publicUrl)
# ファイルの削除
const { error } = await supabase.storage
.from('avatars')
.remove(['user-avatar.jpg'])
# ファイル一覧の取得
const { data, error } = await supabase.storage
.from('avatars')
.list()
# Row Level Security (RLS)
RLSは、データベースレベルで行単位のアクセス制御を実現する機能。
-- 自分の投稿のみ更新可能
CREATE POLICY "自分の投稿のみ更新可能" ON posts
FOR UPDATE USING (auth.uid() = user_id);
-- 自分の投稿のみ削除可能
CREATE POLICY "自分の投稿のみ削除可能" ON posts
FOR DELETE USING (auth.uid() = user_id);
-- 公開された投稿のみ読み取り可能
CREATE POLICY "公開された投稿のみ読み取り可能" ON posts
FOR SELECT USING (is_public = true OR auth.uid() = user_id);
# エラーハンドリング
const { data, error } = await supabase
.from('posts')
.select('*')
if (error) {
switch (error.code) {
case 'PGRST116':
console.error('データが見つかりません')
break
case '23505':
console.error('重複エラー: 既に存在するレコードです')
break
case '42501':
console.error('権限エラー: この操作を実行する権限がありません')
break
default:
console.error('エラー:', error.message)
}
}
# よくあるエラーと対処法
# RLSエラーが発生する
エラー: new row violates row-level security policy
原因: RLSポリシーが適切に設定されていない、またはユーザーが認証されていない
対処:
- RLSポリシーを確認し、適切な条件を設定
- ユーザーがログインしているか確認
- ポリシーで
auth.uid()を使用している場合、認証状態を確認
# CORSエラーが発生する
エラー: Access to fetch at '...' from origin '...' has been blocked by CORS policy
原因: Supabaseの設定で許可されたオリジンが設定されていない
対処: Supabaseダッシュボードの「Settings」→「API」で許可されたオリジンを追加
# リアルタイムが動作しない
原因: チャンネルの購読が正しく設定されていない、またはRLSポリシーでブロックされている
対処:
- チャンネルの購読状態を確認
- RLSポリシーでリアルタイムイベントが許可されているか確認
- ブラウザのコンソールでエラーを確認
# ストレージへのアクセスが拒否される
原因: ストレージバケットのポリシーが適切に設定されていない
対処: Supabaseダッシュボードの「Storage」→「Policies」で適切なポリシーを設定
ローカル開発環境やSupabase CLIに関する問題はSupabase CLIの使い方を参照。
# 実装時の注意点
# 環境変数の使用
APIキーやURLは環境変数で管理し、Gitにコミットしない。
// .env.local
NEXT_PUBLIC_SUPABASE_URL=your-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-key
// .gitignore
.env.local
# クライアントのシングルトン化
アプリケーション全体で同じSupabaseクライアントインスタンスを使用。
// lib/supabase.js
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)
# RLSの適切な設定
データベースのセキュリティを確保するため、必ずRLSを有効化し、適切なポリシーを設定。
# リアルタイム購読のクリーンアップ
コンポーネントのアンマウント時にリアルタイム購読を解除。
useEffect(() => {
const channel = supabase
.channel('posts-channel')
.on('postgres_changes', { ... }, (payload) => {
// 処理
})
.subscribe()
return () => {
channel.unsubscribe()
}
}, [])
# 型安全性の確保(TypeScript)
Supabase CLIを使用して型定義を生成。
supabase gen types typescript --local > types/supabase.ts
詳細はSupabase CLIの使い方を参照。
import { Database } from './types/supabase'
const supabase = createClient<Database>(
supabaseUrl,
supabaseAnonKey
)