Supabase

データベースPostgreSQLオープンソースリアルタイム認証ストレージFirebase代替フルスタック

データベースプラットフォーム

Supabase

概要

Supabaseは、PostgreSQLをベースとしたオープンソースのFirebase代替プラットフォームです。リアルタイムデータベース、認証、ストレージ、Edge Functions、Row Level Security (RLS) を統合し、モダンなWebアプリケーション開発に必要な機能を一元的に提供します。標準SQLサポートと透明性の高いオープンソースアプローチで、開発者コミュニティから強い支持を獲得しています。

詳細

PostgreSQLベースのデータベース

SupabaseはPostgreSQLをコアとして、リレーショナルデータベースの堅牢性と標準SQLの完全サポートを提供します。NoSQLライクな柔軟性も併せ持ち、JSON/JSONBサポートにより複雑なデータ構造も効率的に処理できます。

リアルタイム機能

データベースの変更をリアルタイムで監視し、WebSocketを通じてクライアントに即座に通知します。チャットアプリケーション、ライブダッシュボード、コラボレーションツールの構築が容易になります。

統合認証システム

OAuth プロバイダー(Google、GitHub、Discord など)、メール認証、マジックリンク、多要素認証をサポートします。Row Level Security と組み合わせることで、セキュアなマルチテナントアプリケーションを構築できます。

Edge Functions

Deno ランタイムベースのサーバーレス関数により、グローバルに分散されたエッジでカスタムロジックを実行できます。データベースに近い場所での処理により、低レイテンシを実現します。

メリット・デメリット

メリット

  • オープンソース: 完全にオープンソースで、ベンダーロックインのリスクが低い
  • PostgreSQL互換: 標準SQLとPostgreSQLの豊富な機能をフル活用可能
  • リアルタイム機能: データベース変更の即座な同期とライブアップデート
  • 統合プラットフォーム: データベース、認証、ストレージ、Functions を一元管理
  • Row Level Security: きめ細かいアクセス制御でセキュアなアプリケーション構築
  • 自由度の高いホスティング: セルフホストまたはクラウドホスト選択可能
  • 豊富なSDK: JavaScript、TypeScript、Python、Go など多言語サポート

デメリット

  • 相対的に新しいプラットフォーム: Firebaseと比較して歴史が浅い
  • PostgreSQL学習コスト: NoSQLに慣れた開発者にはSQL学習が必要
  • リアルタイム制限: 大規模なリアルタイム機能では性能限界がある可能性
  • コンプライアンス: 一部の規制要件では追加設定が必要
  • 複雑な設定: 高度な機能利用時にはPostgreSQLの深い知識が必要

参考ページ

実装の例

セットアップ

# Supabase CLIをインストール
npm install -g supabase

# プロジェクトを初期化
supabase init my-project
cd my-project

# ローカル環境を開始
supabase start

# JavaScriptクライアントをインストール
npm install @supabase/supabase-js

スキーマ設計

-- ユーザープロファイルテーブル
CREATE TABLE profiles (
  id UUID REFERENCES auth.users NOT NULL PRIMARY KEY,
  updated_at TIMESTAMP WITH TIME ZONE,
  username TEXT UNIQUE,
  avatar_url TEXT,
  website TEXT,
  CONSTRAINT username_length CHECK (char_length(username) >= 3)
);

-- RLSを有効化
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

-- ポリシーを設定
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 own profile." 
  ON profiles FOR UPDATE 
  USING ( auth.uid() = id );

-- 投稿テーブル
CREATE TABLE posts (
  id BIGSERIAL PRIMARY KEY,
  user_id UUID NOT NULL REFERENCES auth.users(id),
  title TEXT NOT NULL,
  content TEXT,
  published BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- リアルタイム機能を有効化
ALTER PUBLICATION supabase_realtime ADD TABLE posts;

データ操作

import { createClient } from '@supabase/supabase-js'

const supabaseUrl = 'https://your-project.supabase.co'
const supabaseKey = process.env.SUPABASE_ANON_KEY
const supabase = createClient(supabaseUrl, supabaseKey)

// ユーザー登録
async function signUp(email, password) {
  const { data, error } = await supabase.auth.signUp({
    email: email,
    password: password,
  })
  
  if (error) {
    throw new Error(error.message)
  }
  
  return data
}

// データの挿入
async function createPost(title, content, published = false) {
  const { data, error } = await supabase
    .from('posts')
    .insert([
      { 
        title, 
        content, 
        published,
        user_id: (await supabase.auth.getUser()).data.user?.id
      }
    ])
    .select()
  
  if (error) {
    throw new Error(error.message)
  }
  
  return data[0]
}

// データの取得
async function getPosts(limit = 10) {
  const { data, error } = await supabase
    .from('posts')
    .select(`
      *,
      profiles (
        username,
        avatar_url
      )
    `)
    .eq('published', true)
    .order('created_at', { ascending: false })
    .limit(limit)
  
  if (error) {
    throw new Error(error.message)
  }
  
  return data
}

// リアルタイム購読
function subscribeToPostChanges(callback) {
  const subscription = supabase
    .channel('posts')
    .on(
      'postgres_changes',
      {
        event: 'INSERT',
        schema: 'public',
        table: 'posts'
      },
      (payload) => {
        console.log('New post:', payload.new)
        callback(payload.new)
      }
    )
    .subscribe()
  
  return subscription
}

スケーリング

// データベース接続プールの設定
const supabase = createClient(supabaseUrl, supabaseKey, {
  db: {
    schema: 'public',
  },
  auth: {
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: true
  },
  realtime: {
    params: {
      eventsPerSecond: 10,
    },
  },
})

// 読み取り専用レプリカの活用
async function getAnalytics() {
  const { data, error } = await supabase
    .from('posts')
    .select('created_at, published')
    .gte('created_at', new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString())
  
  if (error) throw error
  
  // データ集計処理
  const analytics = data.reduce((acc, post) => {
    const date = post.created_at.split('T')[0]
    if (!acc[date]) {
      acc[date] = { total: 0, published: 0 }
    }
    acc[date].total++
    if (post.published) {
      acc[date].published++
    }
    return acc
  }, {})
  
  return analytics
}

// バッチ処理の最適化
async function batchUpdatePosts(postIds, updates) {
  const { data, error } = await supabase
    .from('posts')
    .update(updates)
    .in('id', postIds)
    .select()
  
  if (error) throw error
  return data
}

バックアップ・復旧

# データベースのバックアップ
supabase db dump > backup.sql

# 特定テーブルのバックアップ
supabase db dump --table posts > posts_backup.sql

# マイグレーションの管理
supabase migration new create_posts_table
supabase migration new add_posts_rls

# マイグレーションの適用
supabase db push

# 本番環境との同期
supabase link --project-ref your-project-ref
supabase db push --dry-run  # プレビュー
supabase db push  # 適用

統合

// Next.js App Router での統合
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'

export async function GET() {
  const cookieStore = cookies()
  const supabase = createRouteHandlerClient({ cookies: () => cookieStore })
  
  const { data: posts, error } = await supabase
    .from('posts')
    .select('*')
    .eq('published', true)
  
  if (error) {
    return Response.json({ error: error.message }, { status: 500 })
  }
  
  return Response.json(posts)
}

// React での認証状態管理
import { useEffect, useState } from 'react'
import { useSupabaseClient, useUser } from '@supabase/auth-helpers-react'

function AuthComponent() {
  const supabase = useSupabaseClient()
  const user = useUser()
  const [loading, setLoading] = useState(false)

  const handleSignIn = async (email, password) => {
    setLoading(true)
    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    })
    if (error) {
      alert(error.message)
    }
    setLoading(false)
  }

  const handleSignOut = async () => {
    const { error } = await supabase.auth.signOut()
    if (error) {
      alert(error.message)
    }
  }

  if (user) {
    return (
      <div>
        <p>Welcome, {user.email}!</p>
        <button onClick={handleSignOut}>Sign Out</button>
      </div>
    )
  }

  return (
    <form onSubmit={(e) => {
      e.preventDefault()
      const formData = new FormData(e.target)
      handleSignIn(
        formData.get('email'),
        formData.get('password')
      )
    }}>
      <input name="email" type="email" placeholder="Email" required />
      <input name="password" type="password" placeholder="Password" required />
      <button type="submit" disabled={loading}>
        {loading ? 'Loading...' : 'Sign In'}
      </button>
    </form>
  )
}

// Edge Functions の例
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  const { name } = await req.json()
  
  const supabaseClient = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? '',
  )
  
  const { data } = await supabaseClient
    .from('profiles')
    .select('*')
    .eq('username', name)
    .single()
  
  return new Response(
    JSON.stringify({ user: data }),
    { headers: { 'Content-Type': 'application/json' } },
  )
})