Supabaseとは
Supabaseは、オープンソースのFirebase代替として注目されているBaaS(Backend as a Service)プラットフォームです。PostgreSQLをベースに、認証、リアルタイム、ストレージなどの機能を提供します。
flowchart TB
subgraph Client["Client Application"]
Apps["React, Next.js, Vue, Flutter, etc."]
end
subgraph Supabase["Supabase Platform"]
subgraph Services["Services Layer"]
Auth["Auth<br/>(GoTrue)"]
Realtime["Realtime<br/>(Elixir)"]
Storage["Storage<br/>(S3互換)"]
end
subgraph API["API Layer"]
Edge["Edge Functions<br/>(Deno)"]
PostgREST["PostgREST<br/>(Auto-generated API)"]
end
subgraph DB["Database Layer"]
PostgreSQL["PostgreSQL<br/>(Row Level Security, Extensions)"]
end
end
Client --> Services
Client --> API
Services --> DB
API --> DB
セットアップ
# Supabase CLIのインストール
npm install -g supabase
# ローカル開発環境の起動
supabase init
supabase start
# クライアントライブラリのインストール
npm install @supabase/supabase-js
クライアント初期化
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
import type { Database } from './database.types';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);
// サーバーサイド用(Service Role Key)
export const supabaseAdmin = createClient<Database>(
supabaseUrl,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
}
);
型生成
# データベーススキーマから型を生成
supabase gen types typescript --local > lib/database.types.ts
// lib/database.types.ts (自動生成)
export type Database = {
public: {
Tables: {
users: {
Row: {
id: string;
email: string;
name: string;
avatar_url: string | null;
created_at: string;
};
Insert: {
id?: string;
email: string;
name: string;
avatar_url?: string | null;
created_at?: string;
};
Update: {
id?: string;
email?: string;
name?: string;
avatar_url?: string | null;
created_at?: string;
};
};
posts: {
Row: {
id: number;
title: string;
content: string;
author_id: string;
published: boolean;
created_at: string;
};
Insert: Omit<Posts['Row'], 'id' | 'created_at'>;
Update: Partial<Posts['Insert']>;
};
};
};
};
認証
メール/パスワード認証
// サインアップ
async function signUp(email: string, password: string, name: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
name,
},
},
});
if (error) throw error;
return data;
}
// ログイン
async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
return data;
}
// ログアウト
async function signOut() {
const { error } = await supabase.auth.signOut();
if (error) throw error;
}
// セッション取得
async function getSession() {
const { data: { session } } = await supabase.auth.getSession();
return session;
}
OAuth認証
// Google認証
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
},
});
if (error) throw error;
return data;
}
// GitHub認証
async function signInWithGitHub() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
scopes: 'read:user user:email',
},
});
if (error) throw error;
return data;
}
認証状態の監視
// hooks/useAuth.ts
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
import type { User, Session } from '@supabase/supabase-js';
export function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 初期セッション取得
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
setUser(session?.user ?? null);
setLoading(false);
});
// 認証状態の変更を監視
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
setSession(session);
setUser(session?.user ?? null);
}
);
return () => subscription.unsubscribe();
}, []);
return { user, session, loading };
}
データベース操作
CRUD操作
// 取得
async function getPosts() {
const { data, error } = await supabase
.from('posts')
.select('*, author:users(name, avatar_url)')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(10);
if (error) throw error;
return data;
}
// 単一レコード取得
async function getPost(id: number) {
const { data, error } = await supabase
.from('posts')
.select('*, author:users(name, avatar_url), comments(*, author:users(name))')
.eq('id', id)
.single();
if (error) throw error;
return data;
}
// 作成
async function createPost(post: { title: string; content: string }) {
const { data: { user } } = await supabase.auth.getUser();
const { data, error } = await supabase
.from('posts')
.insert({
title: post.title,
content: post.content,
author_id: user!.id,
})
.select()
.single();
if (error) throw error;
return data;
}
// 更新
async function updatePost(id: number, updates: Partial<Post>) {
const { data, error } = await supabase
.from('posts')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
}
// 削除
async function deletePost(id: number) {
const { error } = await supabase
.from('posts')
.delete()
.eq('id', id);
if (error) throw error;
}
フィルタリング
// 複合条件
const { data } = await supabase
.from('products')
.select('*')
.gte('price', 100)
.lte('price', 500)
.in('category', ['electronics', 'books'])
.ilike('name', '%phone%')
.not('deleted_at', 'is', null);
// OR条件
const { data } = await supabase
.from('posts')
.select('*')
.or('status.eq.published,author_id.eq.123');
// 全文検索
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', 'TypeScript React');
// 範囲クエリ
const { data } = await supabase
.from('events')
.select('*')
.gte('start_date', '2024-01-01')
.lte('end_date', '2024-12-31');
Row Level Security (RLS)
-- テーブルにRLSを有効化
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- ポリシー: 公開記事は誰でも閲覧可能
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true);
-- ポリシー: 自分の記事は全て閲覧可能
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = author_id);
-- ポリシー: 認証ユーザーは記事を作成可能
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- ポリシー: 自分の記事のみ更新可能
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);
-- ポリシー: 自分の記事のみ削除可能
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);
flowchart TB
Request["リクエスト<br/>SELECT * FROM posts WHERE id = 1"]
RLS["RLS Policy Check<br/>published = true OR auth.uid() = author_id"]
Match["ポリシー適合<br/>→ データ返却"]
NoMatch["ポリシー不適合<br/>→ 空の結果"]
Request --> RLS
RLS --> Match
RLS --> NoMatch
リアルタイム
リアルタイムサブスクリプション
// テーブルの変更を監視
const subscription = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE, または *
schema: 'public',
table: 'posts',
},
(payload) => {
console.log('Change received:', payload);
if (payload.eventType === 'INSERT') {
console.log('New post:', payload.new);
} else if (payload.eventType === 'UPDATE') {
console.log('Updated:', payload.old, '->', payload.new);
} else if (payload.eventType === 'DELETE') {
console.log('Deleted:', payload.old);
}
}
)
.subscribe();
// フィルター付きサブスクリプション
const subscription = supabase
.channel('user-posts')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts',
filter: `author_id=eq.${userId}`,
},
(payload) => {
console.log('New post by user:', payload.new);
}
)
.subscribe();
// クリーンアップ
subscription.unsubscribe();
プレゼンス(オンライン状態)
// チャットルームのプレゼンス
const room = supabase.channel('chat-room');
room
.on('presence', { event: 'sync' }, () => {
const state = room.presenceState();
console.log('Online users:', Object.keys(state));
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key);
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await room.track({
user_id: userId,
online_at: new Date().toISOString(),
});
}
});
ストレージ
// ファイルアップロード
async function uploadFile(file: File, path: string) {
const { data, error } = await supabase.storage
.from('avatars')
.upload(path, file, {
cacheControl: '3600',
upsert: true,
});
if (error) throw error;
// 公開URLを取得
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(path);
return publicUrl;
}
// ファイルダウンロード
async function downloadFile(path: string) {
const { data, error } = await supabase.storage
.from('documents')
.download(path);
if (error) throw error;
return data;
}
// 署名付きURL(有効期限付き)
async function getSignedUrl(path: string) {
const { data, error } = await supabase.storage
.from('private-files')
.createSignedUrl(path, 3600); // 1時間有効
if (error) throw error;
return data.signedUrl;
}
// ファイル削除
async function deleteFile(path: string) {
const { error } = await supabase.storage
.from('avatars')
.remove([path]);
if (error) throw error;
}
// フォルダ内のファイル一覧
async function listFiles(folder: string) {
const { data, error } = await supabase.storage
.from('documents')
.list(folder, {
limit: 100,
sortBy: { column: 'created_at', order: 'desc' },
});
if (error) throw error;
return data;
}
Edge Functions
// supabase/functions/hello/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
serve(async (req) => {
try {
// Supabaseクライアント作成
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{
global: {
headers: { Authorization: req.headers.get('Authorization')! },
},
}
);
// 認証ユーザー取得
const { data: { user } } = await supabaseClient.auth.getUser();
if (!user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
const { name } = await req.json();
return new Response(
JSON.stringify({ message: `Hello ${name}!`, userId: user.id }),
{ headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
});
// クライアントからの呼び出し
const { data, error } = await supabase.functions.invoke('hello', {
body: { name: 'World' },
});
Next.js App Router統合
// app/auth/callback/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get('code');
if (code) {
const supabase = createRouteHandlerClient({ cookies });
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(new URL('/', requestUrl.origin));
}
// 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();
// 保護されたルートへのアクセスチェック
if (req.nextUrl.pathname.startsWith('/dashboard') && !session) {
return NextResponse.redirect(new URL('/login', req.url));
}
return res;
}
export const config = {
matcher: ['/dashboard/:path*'],
};