# Supabaseの使い方

Firebaseのオープンソース代替として使っているSupabaseのメモ。PostgreSQLデータベース、認証、リアルタイム機能、ストレージなどを提供。

# セットアップ

# プロジェクト作成

  1. Supabase公式サイト (opens new window)でアカウント作成
  2. 「New Project」でプロジェクト作成
  3. 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
)

# 参考リンク

同じタグを持つ記事をピックアップしました。