Svelte + Supabase
SupabaseはオープンソースのFirebase代替として人気のBaaSです。PostgreSQLベース、リアルタイム機能、Row Level Security(RLS)など、エンタープライズレベルの機能を提供します。
Supabaseの特徴
ダイアグラムを読み込み中...
主な機能
- 🐘 PostgreSQL - 完全なSQLデータベース
- 🔐 認証 - Email/Password、OAuth、マジックリンク
- ⚡ リアルタイム - データベースの変更をリアルタイムで同期
- 🛡️ RLS - Row Level Securityでセキュアなアクセス制御
- 📦 ストレージ - ファイルアップロード・管理
- 🚀 Edge Functions - Deno Deployベースのサーバーレス関数
セットアップ
1. プロジェクト作成
npm create vite@latest my-svelte-supabase -- --template svelte-ts
cd my-svelte-supabase
npm install @supabase/supabase-js bash
2. Supabaseクライアント設定
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
import type { Database } from './database.types';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
export const supabase = createClient<Database>(
supabaseUrl,
supabaseAnonKey,
{
auth: {
persistSession: true,
autoRefreshToken: true
}
}
); typescript
3. 型定義の生成
# Supabase CLIで型定義を自動生成
npx supabase gen types typescript --project-id your-project-id > src/lib/database.types.ts bash
認証の実装
認証ストア(Svelte 5 Runes)
// src/lib/stores/auth.svelte.ts
import { supabase } from '$lib/supabase';
import type { User, Session } from '@supabase/supabase-js';
class AuthStore {
user = $state<User | null>(null);
session = $state<Session | null>(null);
loading = $state(true);
constructor() {
// 初期セッション取得
supabase.auth.getSession().then(({ data: { session } }) => {
this.session = session;
this.user = session?.user ?? null;
this.loading = false;
});
// 認証状態の監視
supabase.auth.onAuthStateChange((_event, session) => {
this.session = session;
this.user = session?.user ?? null;
});
}
async signInWithEmail(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
});
if (error) throw error;
return data;
}
async signInWithOAuth(provider: 'google' | 'github') {
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) throw error;
return data;
}
async signOut() {
const { error } = await supabase.auth.signOut();
if (error) throw error;
}
get isAuthenticated() {
return !!this.user;
}
}
export const authStore = new AuthStore(); typescript
認証コンポーネント
<!-- src/lib/components/AuthForm.svelte -->
<script lang="ts">
import { authStore } from '$lib/stores/auth.svelte';
let email = $state('');
let password = $state('');
let isSignUp = $state(false);
let error = $state<string | null>(null);
let loading = $state(false);
async function handleSubmit(e: SubmitEvent) {
e.preventDefault();
loading = true;
error = null;
try {
if (isSignUp) {
await supabase.auth.signUp({ email, password });
} else {
await authStore.signInWithEmail(email, password);
}
} catch (err) {
error = err instanceof Error ? err.message : 'エラーが発生しました';
} finally {
loading = false;
}
}
async function handleOAuth(provider: 'google' | 'github') {
try {
await authStore.signInWithOAuth(provider);
} catch (err) {
error = err instanceof Error ? err.message : 'エラーが発生しました';
}
}
</script>
{#if authStore.user}
<div class="user-profile">
<p>ログイン中: {authStore.user.email}</p>
<button onclick={() => authStore.signOut()}>ログアウト</button>
</div>
{:else}
<form onsubmit={handleSubmit}>
<input
type="email"
bind:value={email}
placeholder="メールアドレス"
required
/>
<input
type="password"
bind:value={password}
placeholder="パスワード"
required
/>
{#if error}
<p class="error">{error}</p>
{/if}
<button type="submit" disabled={loading}>
{isSignUp ? 'サインアップ' : 'ログイン'}
</button>
<button type="button" onclick={() => isSignUp = !isSignUp}>
{isSignUp ? 'ログインに切り替え' : 'サインアップに切り替え'}
</button>
</form>
<div class="oauth-buttons">
<button onclick={() => handleOAuth('google')}>
Googleでログイン
</button>
<button onclick={() => handleOAuth('github')}>
GitHubでログイン
</button>
</div>
{/if} svelte
データベース操作
CRUDの実装
// src/lib/stores/todos.svelte.ts
import { supabase } from '$lib/supabase';
import { authStore } from './auth.svelte';
interface Todo {
id: string;
text: string;
completed: boolean;
user_id: string;
created_at: string;
}
class TodoStore {
todos = $state<Todo[]>([]);
loading = $state(false);
constructor() {
// リアルタイム購読の設定
this.subscribeToChanges();
}
async fetchTodos() {
if (!authStore.user) return;
this.loading = true;
const { data, error } = await supabase
.from('todos')
.select('*')
.eq('user_id', authStore.user.id)
.order('created_at', { ascending: false });
if (!error && data) {
this.todos = data;
}
this.loading = false;
}
async addTodo(text: string) {
if (!authStore.user) return;
const { data, error } = await supabase
.from('todos')
.insert({
text,
user_id: authStore.user.id,
completed: false
})
.select()
.single();
if (!error && data) {
this.todos = [data, ...this.todos];
}
}
async updateTodo(id: string, updates: Partial<Todo>) {
const { error } = await supabase
.from('todos')
.update(updates)
.eq('id', id);
if (!error) {
this.todos = this.todos.map(todo =>
todo.id === id ? { ...todo, ...updates } : todo
);
}
}
async deleteTodo(id: string) {
const { error } = await supabase
.from('todos')
.delete()
.eq('id', id);
if (!error) {
this.todos = this.todos.filter(todo => todo.id !== id);
}
}
private subscribeToChanges() {
supabase
.channel('todos_changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'todos',
filter: `user_id=eq.${authStore.user?.id}`
},
(payload) => {
// リアルタイム更新の処理
if (payload.eventType === 'INSERT' && payload.new) {
this.todos = [payload.new as Todo, ...this.todos];
} else if (payload.eventType === 'UPDATE' && payload.new) {
this.todos = this.todos.map(todo =>
todo.id === payload.new!.id ? payload.new as Todo : todo
);
} else if (payload.eventType === 'DELETE' && payload.old) {
this.todos = this.todos.filter(todo =>
todo.id !== (payload.old as Todo).id
);
}
}
)
.subscribe();
}
}
export const todoStore = new TodoStore(); typescript
Row Level Security (RLS)
データベースポリシーの設定
-- todos テーブルのRLSを有効化
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
-- ユーザーは自分のTODOのみ参照可能
CREATE POLICY "Users can view own todos" ON todos
FOR SELECT USING (auth.uid() = user_id);
-- ユーザーは自分のTODOのみ作成可能
CREATE POLICY "Users can insert own todos" ON todos
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- ユーザーは自分のTODOのみ更新可能
CREATE POLICY "Users can update own todos" ON todos
FOR UPDATE USING (auth.uid() = user_id);
-- ユーザーは自分のTODOのみ削除可能
CREATE POLICY "Users can delete own todos" ON todos
FOR DELETE USING (auth.uid() = user_id); sql
ファイルストレージ
画像アップロードの実装
// src/lib/utils/storage.ts
import { supabase } from '$lib/supabase';
export async function uploadAvatar(file: File, userId: string) {
const fileExt = file.name.split('.').pop();
const fileName = `${userId}/${Date.now()}.${fileExt}`;
const { data, error } = await supabase.storage
.from('avatars')
.upload(fileName, file, {
cacheControl: '3600',
upsert: false
});
if (error) throw error;
// 公開URLの取得
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(fileName);
return publicUrl;
}
// 画像削除
export async function deleteAvatar(path: string) {
const { error } = await supabase.storage
.from('avatars')
.remove([path]);
if (error) throw error;
} typescript
Edge Functions
Deno Edge Function の作成
// supabase/functions/send-email/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
serve(async (req: Request) => {
const { email, subject, message } = await req.json();
// Supabaseクライアントの初期化
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
// メール送信処理(例:Resend API)
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: 'noreply@example.com',
to: email,
subject,
html: message
})
});
return new Response(
JSON.stringify({ success: response.ok }),
{ headers: { 'Content-Type': 'application/json' } }
);
}); typescript
Edge Functionの呼び出し
// src/lib/utils/functions.ts
import { supabase } from '$lib/supabase';
export async function sendEmail(
email: string,
subject: string,
message: string
) {
const { data, error } = await supabase.functions.invoke('send-email', {
body: { email, subject, message }
});
if (error) throw error;
return data;
} typescript
ベストプラクティス
1. 環境変数の管理
# .env.local
VITE_SUPABASE_URL=https://xxxxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... bash
2. エラーハンドリング
// src/lib/utils/error-handler.ts
import { PostgrestError } from '@supabase/supabase-js';
export function handleSupabaseError(error: PostgrestError | null): string {
if (!error) return '';
// 一般的なエラーコード
const errorMessages: Record<string, string> = {
'23505': '既に登録されています',
'23503': '参照エラー:関連データが存在しません',
'22P02': '不正な入力形式です',
'PGRST301': '認証が必要です',
'PGRST204': 'データが見つかりません'
};
return errorMessages[error.code] || error.message;
} typescript
3. リアルタイム接続の管理
// src/lib/stores/realtime.svelte.ts
import { supabase } from '$lib/supabase';
import type { RealtimeChannel } from '@supabase/supabase-js';
class RealtimeManager {
channels = new Map<string, RealtimeChannel>();
subscribe(name: string, channel: RealtimeChannel) {
this.channels.set(name, channel);
return channel.subscribe();
}
unsubscribe(name: string) {
const channel = this.channels.get(name);
if (channel) {
supabase.removeChannel(channel);
this.channels.delete(name);
}
}
unsubscribeAll() {
this.channels.forEach(channel => {
supabase.removeChannel(channel);
});
this.channels.clear();
}
}
export const realtimeManager = new RealtimeManager(); typescript
Supabaseの強み
- PostgreSQLの全機能が使える(トリガー、関数、ビュー)
- RLSによる強力なセキュリティ
- リアルタイムが標準機能
- セルフホスティング可能
まとめ
Svelte + Supabaseの組み合わせは以下のケースに最適です。
- ✅ 本格的なデータベース - PostgreSQLの全機能
- ✅ セキュアなアプリ - RLSによる細かいアクセス制御
- ✅ リアルタイム機能 - WebSocketベースの同期
- ✅ オープンソース - ベンダーロックインなし
次のステップとして、 GraphQL統合 も検討してみてください。