mcpField
S
supabase-expert

supabase-expert

github.com/timelesscoデータ・データベース79 回インストール
リポジトリ

supabase-expert について

Supabase でアプリケーションを構築する開発者向けのドキュメントベースのスキルです。データベース、認証、リアルタイム機能、ストレージ、Edge Functions、ベクトル検索、プラットフォーム連携を扱います。

主な機能

  1. 01Supabase の各製品領域に関するガイダンス
  2. 02データベースと Row Level Security のパターン
  3. 03認証とリアルタイム機能の例
  4. 04ストレージと Edge Functions のガイダンス
  5. 05ベクトル検索とフレームワーク連携

活用シーン

  1. 01アプリケーションへの Supabase 認証の追加
  2. 02データベースのアクセス方針設計
  3. 03リアルタイムデータとファイルストレージの実装
  4. 04pgvector によるベクトル検索の構築

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/timelessco/recollect --skill supabase-expert

インストール後、Claude Code で/supabase-expertを入力して読み込まれたか確認します。

詳細

Supabase 統合エキスパート

目的

2,616 件以上の公式ドキュメントファイルに基づき、Supabase を使用したアプリケーションの構築に関する包括的で正確なガイダンスを提供します。データベース操作、認証、リアルタイム機能、ファイルストレージ、エッジ関数、ベクトル検索、プラットフォーム統合のあらゆる側面を網羅します。

ドキュメントの対象範囲

公式 Supabase ドキュメントに完全にアクセスできます(利用可能な場合):

  • 場所: docs/supabase_com/
  • ファイル: 2,616 個の Markdown ファイル
  • 対象範囲: 完全なガイド、API リファレンス、クライアントライブラリ、プラットフォームのドキュメント

注: ドキュメントは別途取得する必要があります。

pipx install docpull
docpull https://supabase.com/docs -o .claude/skills/supabase/docs

主な分野:

  • データベース: PostgreSQL、行レベルセキュリティ(RLS)、マイグレーション、関数、トリガー
  • 認証: メールアドレス/パスワード、OAuth、マジックリンク、SSO、MFA、電話番号認証
  • リアルタイム: データベース変更、ブロードキャスト、プレゼンス、チャネル
  • ストレージ: ファイルアップロード、画像変換、CDN、バケット
  • エッジ関数: Deno ランタイム、サーバーレス、グローバルデプロイ
  • ベクトル/AI: pgvector、埋め込み、セマンティック検索、RAG
  • クライアントライブラリ: JavaScript、Python、Dart(Flutter)、Swift、Kotlin
  • プラットフォーム: CLI、ローカル開発、ブランチ、オブザーバビリティ
  • 統合: Next.js、React、Vue、Svelte、React Native、Expo

使用するタイミング

ユーザーが次の内容に言及した場合に呼び出します。

  • データベース: PostgreSQL、Postgres、SQL、データベース、テーブル、クエリ、マイグレーション
  • 認証: 認証、ログイン、サインアップ、OAuth、SSO、多要素認証、マジックリンク
  • リアルタイム: リアルタイム、サブスクリプション、WebSocket、ライブデータ、プレゼンス、ブロードキャスト
  • ストレージ: ファイルアップロード、ファイルストレージ、画像、S3、CDN、バケット
  • 関数: エッジ関数、サーバーレス、API、Deno、クラウド関数
  • セキュリティ: 行レベルセキュリティ、RLS、ポリシー、権限、アクセス制御
  • AI/ML: ベクトル検索、埋め込み、pgvector、セマンティック検索、AI、RAG
  • フレームワーク統合: Next.js、React、Supabase クライアント、フック

ドキュメントの使用方法

質問に回答する際は、次の手順に従います。

  1. 特定のトピックを検索します:

    # Use Grep to find relevant docs
    grep -r "row level security" docs/supabase_com/ --include="*.md"
    
  2. ガイドを探します:

    # Guides are organized by feature
    ls docs/supabase_com/guides_*
    
  3. リファレンスドキュメントを確認します:

    # Reference docs for client libraries
    ls docs/supabase_com/reference_*
    

クイックスタート

インストール
npm install @supabase/supabase-js
クライアントの初期化
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
	process.env.NEXT_PUBLIC_SUPABASE_URL!,
	process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

環境変数:

  • NEXT_PUBLIC_SUPABASE_URL - プロジェクトの URL(クライアントで安全に使用できます)
  • NEXT_PUBLIC_SUPABASE_ANON_KEY - 匿名/公開キー(クライアントで安全に使用できます)
  • SUPABASE_SERVICE_ROLE_KEY - 管理者キー(サーバー側専用。RLS をバイパスします)

データベース操作

CRUD 操作
// Insert
const { data, error } = await supabase
	.from("posts")
	.insert({
		title: "Hello World",
		content: "My first post",
		user_id: user.id,
	})
	.select()
	.single();

// Read (with filters)
const { data: posts } = await supabase
	.from("posts")
	.select("*")
	.eq("published", true)
	.order("created_at", { ascending: false })
	.limit(10);

// Update
const { data, error } = await supabase
	.from("posts")
	.update({ published: true })
	.eq("id", postId)
	.select()
	.single();

// Delete
const { error } = await supabase.from("posts").delete().eq("id", postId);

// Upsert (insert or update)
const { data, error } = await supabase
	.from("profiles")
	.upsert({
		id: user.id,
		name: "John Doe",
		updated_at: new Date().toISOString(),
	})
	.select();
高度なクエリ
// Joins
const { data } = await supabase
	.from("posts")
	.select(
		`
    *,
    author:profiles(name, avatar),
    comments(count)
  `,
	)
	.eq("published", true);

// Full-text search
const { data } = await supabase
	.from("posts")
	.select("*")
	.textSearch("title", `'nextjs' & 'supabase'`);

// Range queries
const { data } = await supabase
	.from("posts")
	.select("*")
	.gte("created_at", "2024-01-01")
	.lt("created_at", "2024-12-31");

// JSON queries
const { data } = await supabase
	.from("posts")
	.select("*")
	.contains("metadata", { tags: ["tutorial"] });
データベース関数
// Call stored procedure
const { data, error } = await supabase.rpc("get_user_stats", {
	user_id: userId,
});

// Call with filters
const { data } = await supabase
	.rpc("search_posts", { search_term: "supabase" })
	.limit(10);

認証

サインアップ/サインイン
// Email/password signup
const { data, error } = await supabase.auth.signUp({
	email: "[email protected]",
	password: "secure-password",
	options: {
		data: {
			first_name: "John",
			last_name: "Doe",
		},
	},
});

// Email/password sign in
const { data, error } = await supabase.auth.signInWithPassword({
	email: "[email protected]",
	password: "secure-password",
});

// Magic link (passwordless)
const { data, error } = await supabase.auth.signInWithOtp({
	email: "[email protected]",
	options: {
		emailRedirectTo: "https://example.com/auth/callback",
	},
});

// Phone/SMS
const { data, error } = await supabase.auth.signInWithOtp({
	phone: "+1234567890",
});
OAuth プロバイダー
// Google sign in
const { data, error } = await supabase.auth.signInWithOAuth({
	provider: "google",
	options: {
		redirectTo: "http://localhost:3000/auth/callback",
		scopes: "profile email",
	},
});

対応プロバイダー:

  • Google、GitHub、GitLab、Bitbucket
  • Azure、Apple、Discord、Facebook
  • Slack、Spotify、Twitch、Twitter/X
  • Linear、Notion、Figma など
ユーザーセッション管理
// Get current user
const {
	data: { user },
} = await supabase.auth.getUser();

// Get session
const {
	data: { session },
} = await supabase.auth.getSession();

// Sign out
const { error } = await supabase.auth.signOut();

// Listen to auth changes
supabase.auth.onAuthStateChange((event, session) => {
	if (event === "SIGNED_IN") {
		console.log("User signed in:", session.user);
	}
	if (event === "SIGNED_OUT") {
		console.log("User signed out");
	}
	if (event === "TOKEN_REFRESHED") {
		console.log("Token refreshed");
	}
});
多要素認証(MFA)
// Enroll MFA
const { data, error } = await supabase.auth.mfa.enroll({
	factorType: "totp",
	friendlyName: "My Authenticator App",
});

// Verify MFA
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
	factorId: data.id,
	code: "123456",
});

// List factors
const { data: factors } = await supabase.auth.mfa.listFactors();

行レベルセキュリティ(RLS)

RLS を有効にする
-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ポリシーを作成する
-- Public read access
CREATE POLICY "Posts are viewable by everyone"
  ON posts FOR SELECT
  USING (true);

-- Users can insert their own posts
CREATE POLICY "Users can create posts"
  ON posts FOR INSERT
  WITH CHECK (auth.uid() = user_id);

-- Users can update only their posts
CREATE POLICY "Users can update own posts"
  ON posts FOR UPDATE
  USING (auth.uid() = user_id);

-- Users can delete only their posts
CREATE POLICY "Users can delete own posts"
  ON posts FOR DELETE
  USING (auth.uid() = user_id);

-- Conditional access (e.g., premium users)
CREATE POLICY "Premium content for premium users"
  ON posts FOR SELECT
  USING (
    NOT premium OR
    (auth.uid() IN (
      SELECT user_id FROM subscriptions
      WHERE status = 'active'
    ))
  );
ヘルパー関数
-- Get current user ID
auth.uid()

-- Get current JWT
auth.jwt()

-- Access JWT claims
(auth.jwt()->>'role')::text
(auth.jwt()->>'email')::text

リアルタイムサブスクリプション

データベースの変更をリッスンする
const channel = supabase
	.channel("posts-changes")
	.on(
		"postgres_changes",
		{
			event: "*", // or 'INSERT', 'UPDATE', 'DELETE'
			schema: "public",
			table: "posts",
		},
		(payload) => {
			console.log("Change received:", payload);
		},
	)
	.subscribe();

// Cleanup
channel.unsubscribe();
リアルタイムイベントをフィルタリングする
// Only listen to specific user's posts
const channel = supabase
	.channel("my-posts")
	.on(
		"postgres_changes",
		{
			event: "INSERT",
			schema: "public",
			table: "posts",
			filter: `user_id=eq.${userId}`,
		},
		(payload) => {
			console.log("New post:", payload.new);
		},
	)
	.subscribe();
ブロードキャスト(一時メッセージ)
const channel = supabase.channel("chat-room");

// Send message
await channel.send({
	type: "broadcast",
	event: "message",
	payload: { text: "Hello!", user: "John" },
});

// Receive messages
channel.on("broadcast", { event: "message" }, (payload) => {
	console.log("Message:", payload.payload);
});

await channel.subscribe();
プレゼンスの追跡
const channel = supabase.channel("room-1");

// Track presence
channel
	.on("presence", { event: "sync" }, () => {
		const state = channel.presenceState();
		console.log("Online users:", Object.keys(state).length);
	})
	.on("presence", { event: "join" }, ({ key, newPresences }) => {
		console.log("User joined:", newPresences);
	})
	.on("presence", { event: "leave" }, ({ key, leftPresences }) => {
		console.log("User left:", leftPresences);
	})
	.subscribe(async (status) => {
		if (status === "SUBSCRIBED") {
			await channel.track({
				user_id: userId,
				online_at: new Date().toISOString(),
			});
		}
	});

ストレージ

ファイルをアップロードする
const file = event.target.files[0];

const { data, error } = await supabase.storage
	.from("avatars")
	.upload(`public/${userId}/avatar.png`, file, {
		cacheControl: "3600",
		upsert: true,
	});

// Upload from base64
const { data, error } = await supabase.storage
	.from("avatars")
	.upload("file.png", decode(base64String), {
		contentType: "image/png",
	});
ファイルをダウンロードする
// Download as blob
const { data, error } = await supabase.storage
	.from("avatars")
	.download("public/avatar.png");

const url = URL.createObjectURL(data);
公開 URL
// Get public URL (for public buckets)
const { data } = supabase.storage
	.from("avatars")
	.getPublicUrl("public/avatar.png");

console.log(data.publicUrl);
署名付き URL(プライベートファイル)
// Create temporary access URL
const { data, error } = await supabase.storage
	.from("private-files")
	.createSignedUrl("document.pdf", 3600); // 1 hour

console.log(data.signedUrl);
画像変換
const { data } = supabase.storage.from("avatars").getPublicUrl("avatar.png", {
	transform: {
		width: 400,
		height: 400,
		resize: "cover", // 'contain', 'cover', 'fill'
		quality: 80,
	},
});
ファイルを一覧表示する
const { data, error } = await supabase.storage.from("avatars").list("public", {
	limit: 100,
	offset: 0,
	sortBy: { column: "created_at", order: "desc" },
});

エッジ関数

関数を作成する
# Install Supabase CLI
npm install -g supabase

# Initialize project
supabase init

# Create function
supabase functions new my-function
関数の例
// supabase/functions/my-function/index.ts
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

Deno.serve(async (req) => {
	try {
		// Initialize Supabase client
		const supabase = createClient(
			Deno.env.get("SUPABASE_URL")!,
			Deno.env.get("SUPABASE_ANON_KEY")!,
			{
				global: {
					headers: { Authorization: req.headers.get("Authorization")! },
				},
			},
		);

		// Get authenticated user
		const {
			data: { user },
			error: authError,
		} = await supabase.auth.getUser();

		if (authError || !user) {
			return new Response(JSON.stringify({ error: "Unauthorized" }), {
				status: 401,
				headers: { "Content-Type": "application/json" },
			});
		}

		// Query database
		const { data: posts, error } = await supabase
			.from("posts")
			.select("*")
			.eq("user_id", user.id);

		if (error) {
			throw error;
		}

		return new Response(JSON.stringify({ posts }), {
			headers: { "Content-Type": "application/json" },
		});
	} catch (error) {
		return new Response(JSON.stringify({ error: error.message }), {
			status: 500,
			headers: { "Content-Type": "application/json" },
		});
	}
});
関数をデプロイする
# Deploy single function
supabase functions deploy my-function

# Deploy all functions
supabase functions deploy
関数を呼び出す
const { data, error } = await supabase.functions.invoke("my-function", {
	body: { name: "World" },
});

console.log(data);

ベクトル検索(AI/ML)

pgvector を有効にする
-- Enable extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Create table with vector column
CREATE TABLE documents (
  id BIGSERIAL PRIMARY KEY,
  content TEXT,
  embedding VECTOR(1536) -- OpenAI ada-002 dimensions
);

-- Create HNSW index for fast similarity search
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
埋め込みを保存する
import OpenAI from "openai";

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// Generate embedding
const response = await openai.embeddings.create({
	model: "text-embedding-ada-002",
	input: "Supabase is awesome",
});

const embedding = response.data[0].embedding;

// Store in database
const { data, error } = await supabase.from("documents").insert({
	content: "Supabase is awesome",
	embedding,
});
類似性検索
// Find similar documents
const { data, error } = await supabase.rpc("match_documents", {
	query_embedding: embedding,
	match_threshold: 0.78,
	match_count: 10,
});

類似性検索関数:

CREATE FUNCTION match_documents (
  query_embedding VECTOR(1536),
  match_threshold FLOAT,
  match_count INT
)
RETURNS TABLE (
  id BIGINT,
  content TEXT,
  similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
  RETURN QUERY
  SELECT
    documents.id,
    documents.content,
    1 - (documents.embedding <=> query_embedding) AS similarity
  FROM documents
  WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
  ORDER BY documents.embedding <=> query_embedding
  LIMIT match_count;
END;
$$;

Next.js 統合

サーバーコンポーネント
// app/posts/page.tsx
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export default async function PostsPage() {
	const cookieStore = cookies();
	const supabase = createServerClient(
		process.env.NEXT_PUBLIC_SUPABASE_URL!,
		process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
		{
			cookies: {
				getAll() {
					return cookieStore.getAll();
				},
				setAll(cookiesToSet) {
					try {
						cookiesToSet.forEach(({ name, value, options }) =>
							cookieStore.set(name, value, options),
						);
					} catch {
						// Called from Server Component - ignore
					}
				},
			},
		},
	);
}
クライアントコンポーネント
// app/new-post/page.tsx
'use client';

import { createBrowserClient } from '@supabase/ssr';
import { useState } from 'react';

export default function NewPostPage() {
  const supabase = createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
  const [title, setTitle] = useState('');

  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();

    const { error } = await supabase
      .from('posts')
      .insert({ title });

    if (error) console.error(error);
  };

  return <form onSubmit={handleSubmit}>...</form>;
}
ミドルウェア(認証保護)
// middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
	let response = NextResponse.next({
		request,
	});

	const supabase = createServerClient(
		process.env.NEXT_PUBLIC_SUPABASE_URL!,
		process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
		{
			cookies: {
				getAll() {
					return request.cookies.getAll();
				},
				setAll(cookiesToSet) {
					for (const { name, value } of cookiesToSet) {
						request.cookies.set(name, value);
					}

					response = NextResponse.next({
						request,
					});
					for (const { name, value, options } of cookiesToSet) {
						response.cookies.set(name, value, options);
					}
				},
			},
		},
	);

	const { data, error } = await supabase.auth.getClaims();

	if (error) {
		throw error;
	}

	const user = data?.claims;

	// Redirect to login if not authenticated
	if (!user && request.nextUrl.pathname.startsWith("/dashboard")) {
		return NextResponse.redirect(new URL("/login", request.url));
	}

	return response;
}

export const config = {
	matcher: ["/dashboard/:path*"],
};
ルートハンドラー
// app/api/posts/route.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

async function createClient() {
	const cookieStore = await cookies();

	return createServerClient(
		process.env.NEXT_PUBLIC_SUPABASE_URL!,
		process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
		{
			cookies: {
				getAll() {
					return cookieStore.getAll();
				},
				setAll(cookiesToSet) {
					try {
						for (const { name, value, options } of cookiesToSet) {
							cookieStore.set(name, value, options);
						}
					} catch {
						// Called from Server Component - ignore
					}
				},
			},
		},
	);
}

export async function GET() {
	const supabase = await createClient();

	const { data: posts } = await supabase.from("posts").select("*");

	return NextResponse.json({ posts });
}

export async function POST(request: Request) {
	const supabase = await createClient();
	const body = await request.json();

	const { data, error } = await supabase
		.from("posts")
		.insert(body)
		.select()
		.single();

	if (error) {
		return NextResponse.json({ error: error.message }, { status: 400 });
	}

	return NextResponse.json({ post: data });
}

データベースマイグレーション

マイグレーションを作成する
supabase migration new create_posts_table
マイグレーションファイルの例
-- supabase/migrations/20241116000000_create_posts_table.sql

-- Create table
CREATE TABLE posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES auth.users NOT NULL,
  title TEXT NOT NULL,
  content TEXT,
  published BOOLEAN DEFAULT false,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);

-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Create policies
CREATE POLICY "Public posts are viewable by everyone"
  ON posts FOR SELECT
  USING (published = true);

CREATE POLICY "Users can view their own posts"
  ON posts FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "Users can create posts"
  ON posts FOR INSERT
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can update own posts"
  ON posts FOR UPDATE
  USING (auth.uid() = user_id);

-- Create indexes
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_published_idx ON posts(published) WHERE published = true;

-- Create updated_at trigger
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_updated_at
  BEFORE UPDATE ON posts
  FOR EACH ROW
  EXECUTE FUNCTION handle_updated_at();
マイグレーションの実行
# Apply migrations locally
supabase db reset

# Push to remote (production)
supabase db push

TypeScript との統合

データベース型の生成
# Generate types from your database schema
supabase gen types typescript --project-id YOUR_PROJECT_ID > types/supabase.ts

# Or from local development
supabase gen types typescript --local > types/supabase.ts
型安全なクライアント
// lib/supabase/types.ts
export type Json =
	| string
	| number
	| boolean
	| null
	| { [key: string]: Json | undefined }
	| Json[];

export interface Database {
	public: {
		Tables: {
			posts: {
				Row: {
					id: string;
					created_at: string;
					title: string;
					content: string | null;
					user_id: string;
					published: boolean;
				};
				Insert: {
					id?: string;
					created_at?: string;
					title: string;
					content?: string | null;
					user_id: string;
					published?: boolean;
				};
				Update: {
					id?: string;
					created_at?: string;
					title?: string;
					content?: string | null;
					user_id?: string;
					published?: boolean;
				};
			};
			profiles: {
				Row: {
					id: string;
					name: string | null;
					avatar_url: string | null;
					created_at: string;
				};
				Insert: {
					id: string;
					name?: string | null;
					avatar_url?: string | null;
					created_at?: string;
				};
				Update: {
					id?: string;
					name?: string | null;
					avatar_url?: string | null;
					created_at?: string;
				};
			};
		};
		Views: {
			[_ in never]: never;
		};
		Functions: {
			[_ in never]: never;
		};
		Enums: {
			[_ in never]: never;
		};
	};
}

// lib/supabase/client.ts
import { createClient } from "@supabase/supabase-js";
import { Database } from "./types";

export const supabase = createClient<Database>(
	process.env.NEXT_PUBLIC_SUPABASE_URL!,
	process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);

// Now you get full type safety!
const { data } = await supabase
	.from("posts") // ✅ TypeScript knows this table exists
	.select("title, content, profiles(name)") // ✅ TypeScript validates columns
	.eq("published", true); // ✅ TypeScript validates types

// data is typed as:
// Array<{ title: string; content: string | null; profiles: { name: string | null } }>
サーバー側とクライアント側の Supabase
// lib/supabase/client.ts - Client-side (respects RLS)
import { createBrowserClient } from "@supabase/ssr";
import { Database } from "./types";

export function createClient() {
	return createBrowserClient<Database>(
		process.env.NEXT_PUBLIC_SUPABASE_URL!,
		process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
	);
}

// lib/supabase/server.ts - Server-side (Next.js App Router)
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { Database } from "./types";

export async function createClient() {
	const cookieStore = await cookies();

	return createServerClient<Database>(
		process.env.NEXT_PUBLIC_SUPABASE_URL!,
		process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
		{
			cookies: {
				getAll() {
					return cookieStore.getAll();
				},
				setAll(cookiesToSet) {
					try {
						for (const { name, value, options } of cookiesToSet) {
							cookieStore.set(name, value, options);
						}
					} catch {
						// Called from Server Component - ignore
					}
				},
			},
		},
	);
}

// lib/supabase/admin.ts - Admin client (bypasses RLS)
import { createClient } from "@supabase/supabase-js";
import { Database } from "./types";

export const supabaseAdmin = createClient<Database>(
	process.env.NEXT_PUBLIC_SUPABASE_URL!,
	process.env.SUPABASE_SERVICE_ROLE_KEY!, // ⚠️ Server-side only!
	{
		auth: {
			autoRefreshToken: false,
			persistSession: false,
		},
	},
);

Next.js App Router のパターン

サーバーコンポーネント(推奨)
// app/posts/page.tsx
import { createClient } from '@/lib/supabase/server'

export default async function PostsPage() {
  const supabase = createClient()

  // Fetch data on server (no loading state needed!)
  const { data: posts } = await supabase
    .from('posts')
    .select('*, profiles(*)')
    .eq('published', true)
    .order('created_at', { ascending: false })

  return (
    <div>
      {posts?.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>By {post.profiles?.name}</p>
          <div>{post.content}</div>
        </article>
      ))}
    </div>
  )
}
ミューテーション用のサーバーアクション
// app/actions/posts.ts
"use server";

import { createClient } from "@/lib/supabase/server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export async function createPost(formData: FormData) {
	const supabase = createClient();

	const {
		data: { user },
	} = await supabase.auth.getUser();
	if (!user) {
		redirect("/login");
	}

	const title = formData.get("title") as string;
	const content = formData.get("content") as string;

	const { error } = await supabase.from("posts").insert({
		title,
		content,
		user_id: user.id,
	});

	if (error) {
		throw new Error(error.message);
	}

	revalidatePath("/posts");
	redirect("/posts");
}

export async function updatePost(id: string, formData: FormData) {
	const supabase = createClient();

	const {
		data: { user },
	} = await supabase.auth.getUser();
	if (!user) {
		throw new Error("Unauthorized");
	}

	const { error } = await supabase
		.from("posts")
		.update({
			title: formData.get("title") as string,
			content: formData.get("content") as string,
		})
		.eq("id", id)
		.eq("user_id", user.id); // Ensure user owns the post

	if (error) {
		throw new Error(error.message);
	}

	revalidatePath("/posts");
}

export async function deletePost(id: string) {
	const supabase = createClient();

	const {
		data: { user },
	} = await supabase.auth.getUser();
	if (!user) {
		throw new Error("Unauthorized");
	}

	const { error } = await supabase
		.from("posts")
		.delete()
		.eq("id", id)
		.eq("user_id", user.id);

	if (error) {
		throw new Error(error.message);
	}

	revalidatePath("/posts");
}
リアルタイム機能を備えたクライアントコンポーネント
// app/components/PostsList.tsx
'use client'

import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { Database } from '@/lib/supabase/types'

type Post = Database['public']['Tables']['posts']['Row']

export function PostsList({ initialPosts }: { initialPosts: Post[] }) {
  const [posts, setPosts] = useState(initialPosts)
  const supabase = createClient()

  useEffect(() => {
    const channel = supabase
      .channel('posts-changes')
      .on(
        'postgres_changes',
        {
          event: '*',
          schema: 'public',
          table: 'posts',
          filter: 'published=eq.true',
        },
        (payload) => {
          if (payload.eventType === 'INSERT') {
            setPosts(prev => [payload.new as Post, ...prev])
          } else if (payload.eventType === 'UPDATE') {
            setPosts(prev =>
              prev.map(post =>
                post.id === payload.new.id ? (payload.new as Post) : post
              )
            )
          } else if (payload.eventType === 'DELETE') {
            setPosts(prev => prev.filter(post => post.id !== payload.old.id))
          }
        }
      )
      .subscribe()

    return () => {
      supabase.removeChannel(channel)
    }
  }, [supabase])

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  )
}
ルートハンドラー
// app/api/posts/route.ts
import { createClient } from "@/lib/supabase/server";
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
	const supabase = createClient();

	const { searchParams } = new URL(request.url);
	const limit = parseInt(searchParams.get("limit") || "10");

	const { data, error } = await supabase
		.from("posts")
		.select("*")
		.eq("published", true)
		.order("created_at", { ascending: false })
		.limit(limit);

	if (error) {
		return NextResponse.json({ error: error.message }, { status: 500 });
	}

	return NextResponse.json(data);
}

export async function POST(request: NextRequest) {
	const supabase = createClient();

	const {
		data: { user },
	} = await supabase.auth.getUser();
	if (!user) {
		return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
	}

	const body = await request.json();

	const { data, error } = await supabase
		.from("posts")
		.insert({
			...body,
			user_id: user.id,
		})
		.select()
		.single();

	if (error) {
		return NextResponse.json({ error: error.message }, { status: 500 });
	}

	return NextResponse.json(data);
}

高度な認証

メール確認付きのメールアドレス/パスワード認証
// app/actions/auth.ts
"use server";

import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";

export async function signUp(formData: FormData) {
	const supabase = createClient();

	const email = formData.get("email") as string;
	const password = formData.get("password") as string;
	const name = formData.get("name") as string;

	const { error } = await supabase.auth.signUp({
		email,
		password,
		options: {
			data: {
				name, // Stored in auth.users.raw_user_meta_data
			},
			emailRedirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
		},
	});

	if (error) {
		return { error: error.message };
	}

	return { success: true, message: "Check your email to confirm your account" };
}

export async function signIn(formData: FormData) {
	const supabase = createClient();

	const email = formData.get("email") as string;
	const password = formData.get("password") as string;

	const { error } = await supabase.auth.signInWithPassword({
		email,
		password,
	});

	if (error) {
		return { error: error.message };
	}

	redirect("/dashboard");
}

export async function signOut() {
	const supabase = createClient();
	await supabase.auth.signOut();
	redirect("/");
}
OAuth(Google、GitHub など)
// app/actions/auth.ts
export async function signInWithGoogle() {
	const supabase = createClient();

	const { data, error } = await supabase.auth.signInWithOAuth({
		provider: "google",
		options: {
			redirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
			queryParams: {
				access_type: "offline",
				prompt: "consent",
			},
		},
	});

	if (data?.url) {
		redirect(data.url);
	}
}

export async function signInWithGitHub() {
	const supabase = createClient();

	const { data } = await supabase.auth.signInWithOAuth({
		provider: "github",
		options: {
			redirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
			scopes: "read:user user:email",
		},
	});

	if (data?.url) {
		redirect(data.url);
	}
}
マジックリンク
export async function sendMagicLink(email: string) {
	const supabase = createClient();

	const { error } = await supabase.auth.signInWithOtp({
		email,
		options: {
			emailRedirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
		},
	});

	if (error) {
		return { error: error.message };
	}

	return { success: true, message: "Check your email for the login link" };
}
電話番号認証(SMS)
export async function sendPhoneOTP(phone: string) {
	const supabase = createClient();

	const { error } = await supabase.auth.signInWithOtp({
		phone,
	});

	if (error) {
		return { error: error.message };
	}

	return { success: true };
}

export async function verifyPhoneOTP(phone: string, token: string) {
	const supabase = createClient();

	const { error } = await supabase.auth.verifyOtp({
		phone,
		token,
		type: "sms",
	});

	if (error) {
		return { error: error.message };
	}

	redirect("/dashboard");
}
多要素認証(MFA)
// Enable MFA for user
export async function enableMFA() {
	const supabase = createClient();

	const { data, error } = await supabase.auth.mfa.enroll({
		factorType: "totp",
		friendlyName: "Authenticator App",
	});

	if (error) {
		throw error;
	}

	// data.totp.qr_code - QR code to scan
	// data.totp.secret - Secret to enter manually
	return data;
}

// Verify MFA
export async function verifyMFA(factorId: string, code: string) {
	const supabase = createClient();

	const { data, error } = await supabase.auth.mfa.challengeAndVerify({
		factorId,
		code,
	});

	if (error) {
		throw error;
	}
	return data;
}
認証コールバックハンドラー
// app/auth/callback/route.ts
import { createClient } from "@/lib/supabase/server";
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
	const requestUrl = new URL(request.url);
	const code = requestUrl.searchParams.get("code");

	if (code) {
		const supabase = createClient();
		await supabase.auth.exchangeCodeForSession(code);
	}

	// Redirect to dashboard or wherever
	return NextResponse.redirect(new URL("/dashboard", request.url));
}
保護されたルート
// middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
	let response = NextResponse.next({
		request,
	});

	const supabase = createServerClient(
		process.env.NEXT_PUBLIC_SUPABASE_URL!,
		process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
		{
			cookies: {
				getAll() {
					return request.cookies.getAll();
				},
				setAll(cookiesToSet) {
					for (const { name, value } of cookiesToSet) {
						request.cookies.set(name, value);
					}

					response = NextResponse.next({
						request,
					});
					for (const { name, value, options } of cookiesToSet) {
						response.cookies.set(name, value, options);
					}
				},
			},
		},
	);

	const { data, error } = await supabase.auth.getClaims();

	if (error) {
		throw error;
	}

	const user = data?.claims;

	// Protect dashboard routes
	if (request.nextUrl.pathname.startsWith("/dashboard") && !user) {
		return NextResponse.redirect(new URL("/login", request.url));
	}

	// Redirect authenticated users away from auth pages
	if (request.nextUrl.pathname.startsWith("/login") && user) {
		return NextResponse.redirect(new URL("/dashboard", request.url));
	}

	return response;
}

export const config = {
	matcher: ["/dashboard/:path*", "/login", "/signup"],
};

高度な行レベルセキュリティ

複雑な RLS ポリシー
-- Users can only see published posts or their own drafts
CREATE POLICY "Users can read appropriate posts"
  ON posts FOR SELECT
  USING (
    published = true
    OR
    auth.uid() = user_id
  );

-- Users can update only their own posts
CREATE POLICY "Users can update own posts"
  ON posts FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

-- Team-based access
CREATE TABLE teams (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name TEXT NOT NULL
);

CREATE TABLE team_members (
  team_id UUID REFERENCES teams,
  user_id UUID REFERENCES auth.users,
  role TEXT CHECK (role IN ('owner', 'admin', 'member')),
  PRIMARY KEY (team_id, user_id)
);

CREATE TABLE team_documents (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  team_id UUID REFERENCES teams,
  title TEXT,
  content TEXT
);

-- Only team members can see team documents
CREATE POLICY "Team members can view documents"
  ON team_documents FOR SELECT
  USING (
    team_id IN (
      SELECT team_id
      FROM team_members
      WHERE user_id = auth.uid()
    )
  );

-- Only team owners/admins can delete
CREATE POLICY "Team admins can delete documents"
  ON team_documents FOR DELETE
  USING (
    team_id IN (
      SELECT team_id
      FROM team_members
      WHERE user_id = auth.uid()
        AND role IN ('owner', 'admin')
    )
  );
関数ベースの RLS
-- Create helper function
CREATE OR REPLACE FUNCTION is_team_admin(team_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
  RETURN EXISTS (
    SELECT 1
    FROM team_members
    WHERE team_members.team_id = is_team_admin.team_id
      AND team_members.user_id = auth.uid()
      AND team_members.role IN ('owner', 'admin')
  );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Use in policy
CREATE POLICY "Admins can update team settings"
  ON teams FOR UPDATE
  USING (is_team_admin(id))
  WITH CHECK (is_team_admin(id));
JWT クレームを使用した RLS
-- Access custom JWT claims
CREATE POLICY "Premium users can view premium content"
  ON premium_content FOR SELECT
  USING (
    (auth.jwt() -> 'user_metadata' ->> 'subscription_tier') = 'premium'
  );

-- Role-based access
CREATE POLICY "Admins have full access"
  ON sensitive_data FOR ALL
  USING (
    (auth.jwt() -> 'user_metadata' ->> 'role') = 'admin'
  );

高度なリアルタイム機能

プレゼンス(オンラインユーザー)
'use client'

import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'

export function OnlineUsers() {
  const [onlineUsers, setOnlineUsers] = useState<any[]>([])
  const supabase = createClient()

  useEffect(() => {
    const channel = supabase.channel('online-users')

    channel
      .on('presence', { event: 'sync' }, () => {
        const state = channel.presenceState()
        const users = Object.values(state).flat()
        setOnlineUsers(users)
      })
      .on('presence', { event: 'join' }, ({ newPresences }) => {
        console.log('Users joined:', newPresences)
      })
      .on('presence', { event: 'leave' }, ({ leftPresences }) => {
        console.log('Users left:', leftPresences)
      })
      .subscribe(async (status) => {
        if (status === 'SUBSCRIBED') {
          // Track this user
          const { data: { user } } = await supabase.auth.getUser()
          if (user) {
            await channel.track({
              user_id: user.id,
              email: user.email,
              online_at: new Date().toISOString(),
            })
          }
        }
      })

    return () => {
      supabase.removeChannel(channel)
    }
  }, [])

  return (
    <div>
      <h3>{onlineUsers.length} users online</h3>
      <ul>
        {onlineUsers.map((user, i) => (
          <li key={i}>{user.email}</li>
        ))}
      </ul>
    </div>
  )
}
ブロードキャスト(メッセージの送信)
// Cursor tracking
export function CollaborativeCanvas() {
  const supabase = createClient()

  useEffect(() => {
    const channel = supabase.channel('canvas')

    channel
      .on('broadcast', { event: 'cursor' }, (payload) => {
        // Update cursor position
        updateCursor(payload.payload)
      })
      .subscribe()

    // Send cursor position
    const handleMouseMove = (e: MouseEvent) => {
      channel.send({
        type: 'broadcast',
        event: 'cursor',
        payload: { x: e.clientX, y: e.clientY },
      })
    }

    window.addEventListener('mousemove', handleMouseMove)

    return () => {
      window.removeEventListener('mousemove', handleMouseMove)
      supabase.removeChannel(channel)
    }
  }, [])

  return <canvas />
}
Postgres の変更(データベースイベント)
// Listen to specific columns
const channel = supabase
	.channel("post-changes")
	.on(
		"postgres_changes",
		{
			event: "UPDATE",
			schema: "public",
			table: "posts",
			filter: "id=eq.123", // Specific row
		},
		(payload) => {
			console.log("Post updated:", payload);
		},
	)
	.subscribe();

// Listen to multiple tables
const channel = supabase
	.channel("changes")
	.on(
		"postgres_changes",
		{ event: "*", schema: "public", table: "posts" },
		handlePostChange,
	)
	.on(
		"postgres_changes",
		{ event: "*", schema: "public", table: "comments" },
		handleCommentChange,
	)
	.subscribe();

高度なストレージ

画像変換
// Upload with transformation
export async function uploadAvatar(file: File, userId: string) {
	const supabase = createClient();

	const fileExt = file.name.split(".").pop();
	const fileName = `${userId}-${Date.now()}.${fileExt}`;
	const filePath = `avatars/${fileName}`;

	const { error: uploadError } = await supabase.storage
		.from("avatars")
		.upload(filePath, file, {
			cacheControl: "3600",
			upsert: false,
		});

	if (uploadError) {
		throw uploadError;
	}

	// Get transformed image URL
	const { data } = supabase.storage.from("avatars").getPublicUrl(filePath, {
		transform: {
			width: 200,
			height: 200,
			resize: "cover",
			quality: 80,
		},
	});

	return data.publicUrl;
}
署名付き URL(プライベートファイル)
// Generate signed URL (expires after 1 hour)
export async function getPrivateFileUrl(path: string) {
	const supabase = createClient();

	const { data, error } = await supabase.storage
		.from("private-files")
		.createSignedUrl(path, 3600); // 1 hour

	if (error) {
		throw error;
	}

	return data.signedUrl;
}

// Upload to private bucket
export async function uploadPrivateFile(file: File, userId: string) {
	const supabase = createClient();

	const filePath = `${userId}/${file.name}`;

	const { error } = await supabase.storage
		.from("private-files")
		.upload(filePath, file);

	if (error) {
		throw error;
	}

	return filePath;
}
ストレージ RLS
-- Enable RLS on storage.objects
CREATE POLICY "Users can upload to their own folder"
  ON storage.objects FOR INSERT
  WITH CHECK (
    bucket_id = 'avatars' AND
    (storage.foldername(name))[1] = auth.uid()::text
  );

CREATE POLICY "Users can view their own files"
  ON storage.objects FOR SELECT
  USING (
    bucket_id = 'avatars' AND
    (storage.foldername(name))[1] = auth.uid()::text
  );

CREATE POLICY "Users can update their own files"
  ON storage.objects FOR UPDATE
  USING (
    bucket_id = 'avatars' AND
    (storage.foldername(name))[1] = auth.uid()::text
  );

CREATE POLICY "Users can delete their own files"
  ON storage.objects FOR DELETE
  USING (
    bucket_id = 'avatars' AND
    (storage.foldername(name))[1] = auth.uid()::text
  );

エッジ関数

基本的なエッジ関数
// supabase/functions/hello/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";

serve(async (req) => {
	const { name } = await req.json();

	return new Response(JSON.stringify({ message: `Hello ${name}!` }), {
		headers: { "Content-Type": "application/json" },
	});
});
Supabase クライアントを使用したエッジ関数
// supabase/functions/create-post/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

serve(async (req) => {
	try {
		const supabaseClient = createClient(
			Deno.env.get("SUPABASE_URL") ?? "",
			Deno.env.get("SUPABASE_ANON_KEY") ?? "",
			{
				global: {
					headers: { Authorization: req.headers.get("Authorization")! },
				},
			},
		);

		// Get authenticated user
		const {
			data: { user },
			error: userError,
		} = await supabaseClient.auth.getUser();
		if (userError || !user) {
			return new Response(JSON.stringify({ error: "Unauthorized" }), {
				status: 401,
			});
		}

		const { title, content } = await req.json();

		const { data, error } = await supabaseClient
			.from("posts")
			.insert({
				title,
				content,
				user_id: user.id,
			})
			.select()
			.single();

		if (error) {
			throw error;
		}

		return new Response(JSON.stringify(data), {
			headers: { "Content-Type": "application/json" },
		});
	} catch (error) {
		return new Response(JSON.stringify({ error: error.message }), {
			status: 500,
		});
	}
});
スケジュールされたエッジ関数(Cron)
// supabase/functions/cleanup-old-data/index.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

serve(async (req) => {
	// Verify request is from Supabase Cron
	const authHeader = req.headers.get("Authorization");
	if (authHeader !== `Bearer ${Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")}`) {
		return new Response("Unauthorized", { status: 401 });
	}

	const supabase = createClient(
		Deno.env.get("SUPABASE_URL")!,
		Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
	);

	// Delete old data
	const thirtyDaysAgo = new Date();
	thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

	const { error } = await supabase
		.from("temporary_data")
		.delete()
		.lt("created_at", thirtyDaysAgo.toISOString());

	if (error) {
		return new Response(JSON.stringify({ error: error.message }), {
			status: 500,
		});
	}

	return new Response(JSON.stringify({ success: true }));
});

// Configure in Dashboard: Database > Cron Jobs
// Schedule: 0 2 * * * (2am daily)
// HTTP Request: https://your-project.supabase.co/functions/v1/cleanup-old-data
クライアントからエッジ関数を呼び出す
// Client-side
const { data, error } = await supabase.functions.invoke("hello", {
	body: { name: "World" },
});

// With auth headers automatically included
const {
	data: { session },
} = await supabase.auth.getSession();

const { data, error } = await supabase.functions.invoke("create-post", {
	body: {
		title: "My Post",
		content: "Content here",
	},
	headers: {
		Authorization: `Bearer ${session?.access_token}`,
	},
});

ベクトル検索(AI/RAG)

pgvector を有効にする
-- Enable vector extension
CREATE EXTENSION IF NOT EXISTS vector;

-- Create table with embedding column
CREATE TABLE documents (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  content TEXT NOT NULL,
  metadata JSONB,
  embedding vector(1536),  -- For OpenAI ada-002 (1536 dimensions)
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Create index for fast similarity search
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

-- Or use HNSW for better performance (Postgres 15+)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
埋め込みを生成して保存する
// lib/embeddings.ts
import { OpenAI } from "openai";

const openai = new OpenAI({
	apiKey: process.env.OPENAI_API_KEY!,
});

export async function generateEmbedding(text: string): Promise<number[]> {
	const response = await openai.embeddings.create({
		model: "text-embedding-ada-002",
		input: text,
	});

	return response.data[0].embedding;
}

// Store document with embedding
export async function storeDocument(content: string, metadata: any) {
	const supabase = createClient();

	const embedding = await generateEmbedding(content);

	const { data, error } = await supabase
		.from("documents")
		.insert({
			content,
			metadata,
			embedding,
		})
		.select()
		.single();

	if (error) {
		throw error;
	}

	return data;
}
セマンティック検索
// Search similar documents
export async function searchSimilarDocuments(query: string, limit = 5) {
	const supabase = createClient();

	// Generate embedding for query
	const queryEmbedding = await generateEmbedding(query);

	// Search with RPC function
	const { data, error } = await supabase.rpc("match_documents", {
		query_embedding: queryEmbedding,
		match_threshold: 0.78, // Minimum similarity
		match_count: limit,
	});

	if (error) {
		throw error;
	}

	return data;
}

// Create the RPC function
CREATE OR REPLACE FUNCTION match_documents (
  query_embedding vector(1536),
  match_threshold float,
  match_count int
)
RETURNS TABLE (
  id UUID,
  content TEXT,
  metadata JSONB,
  similarity float
)
LANGUAGE SQL STABLE
AS $$
  SELECT
    documents.id,
    documents.content,
    documents.metadata,
    1 - (documents.embedding <=> query_embedding) AS similarity
  FROM documents
  WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
  ORDER BY documents.embedding <=> query_embedding
  LIMIT match_count;
$$;
RAG(検索拡張生成)
export async function ragQuery(question: string) {
	// 1. Search for relevant documents
	const relevantDocs = await searchSimilarDocuments(question, 5);

	// 2. Build context from relevant documents
	const context = relevantDocs.map((doc) => doc.content).join("\n\n");

	// 3. Generate answer with GPT
	const completion = await openai.chat.completions.create({
		model: "gpt-4",
		messages: [
			{
				role: "system",
				content:
					"You are a helpful assistant. Answer questions based on the provided context.",
			},
			{
				role: "user",
				content: `Context:\n${context}\n\nQuestion: ${question}`,
			},
		],
	});

	return {
		answer: completion.choices[0].message.content,
		sources: relevantDocs,
	};
}

データベース関数とトリガー

カスタム関数
-- Get user's post count
CREATE OR REPLACE FUNCTION get_user_post_count(user_id UUID)
RETURNS INTEGER AS $$
  SELECT COUNT(*)::INTEGER
  FROM posts
  WHERE posts.user_id = get_user_post_count.user_id;
$$ LANGUAGE SQL STABLE;

-- Call from TypeScript
const { data, error } = await supabase.rpc('get_user_post_count', {
  user_id: userId,
})
トリガー
-- Auto-update updated_at column
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_updated_at
  BEFORE UPDATE ON posts
  FOR EACH ROW
  EXECUTE FUNCTION handle_updated_at();

-- Update post count when post is created/deleted
CREATE OR REPLACE FUNCTION update_post_count()
RETURNS TRIGGER AS $$
BEGIN
  IF TG_OP = 'INSERT' THEN
    UPDATE profiles
    SET post_count = post_count + 1
    WHERE id = NEW.user_id;
    RETURN NEW;
  ELSIF TG_OP = 'DELETE' THEN
    UPDATE profiles
    SET post_count = post_count - 1
    WHERE id = OLD.user_id;
    RETURN OLD;
  END IF;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_post_count_trigger
  AFTER INSERT OR DELETE ON posts
  FOR EACH ROW
  EXECUTE FUNCTION update_post_count();

パフォーマンスの最適化

クエリの最適化
// Bad: N+1 query problem
const { data: posts } = await supabase.from("posts").select("*");
for (const post of posts) {
	const { data: author } = await supabase
		.from("profiles")
		.select("*")
		.eq("id", post.user_id)
		.single();
}

// Good: Join in single query
const { data: posts } = await supabase.from("posts").select(`
    *,
    profiles (
      id,
      name,
      avatar_url
    )
  `);

// Good: Use specific columns
const { data: posts } = await supabase
	.from("posts")
	.select("id, title, created_at, profiles(name)") // Only what you need
	.eq("published", true)
	.order("created_at", { ascending: false })
	.limit(20);
インデックス
-- Index on frequently filtered columns
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);

-- Partial index (filtered)
CREATE INDEX posts_published_idx ON posts(published)
WHERE published = true;

-- Composite index
CREATE INDEX posts_user_published_idx ON posts(user_id, published, created_at DESC);

-- Full-text search index
CREATE INDEX posts_content_idx ON posts
USING GIN (to_tsvector('english', content));
コネクションプーリング
// Use connection pooler for serverless (Supavisor)
// Connection string: postgresql://postgres.[project-ref]:[password]@aws-0-us-east-1.pooler.supabase.com:6543/postgres

import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
	process.env.SUPABASE_URL!,
	process.env.SUPABASE_ANON_KEY!,
	{
		db: {
			schema: "public",
		},
		global: {
			headers: { "x-my-custom-header": "my-app-name" },
		},
	},
);
キャッシュ
// Next.js cache with revalidation
import { unstable_cache } from "next/cache";
import { createClient } from "@/lib/supabase/server";

export const getCachedPosts = unstable_cache(
	async () => {
		const supabase = createClient();
		const { data } = await supabase
			.from("posts")
			.select("*")
			.eq("published", true);
		return data;
	},
	["posts"],
	{
		revalidate: 300, // 5 minutes
		tags: ["posts"],
	},
);

// Revalidate on mutation
import { revalidateTag } from "next/cache";

export async function createPost(data: any) {
	const supabase = createClient();
	await supabase.from("posts").insert(data);
	revalidateTag("posts");
}

ローカル開発

ローカル Supabase のセットアップ
# Install Supabase CLI
brew install supabase/tap/supabase

# Initialize project
supabase init

# Start local Supabase (Docker required)
supabase start

# This starts:
# - PostgreSQL
# - GoTrue (Auth)
# - Realtime
# - Storage
# - Kong (API Gateway)
# - Studio (Dashboard)
ローカル開発用 URL
# After supabase start:
API URL: http://localhost:54321
Studio URL: http://localhost:54323
Inbucket URL: http://localhost:54324 # Email testing
マイグレーションのワークフロー
# Create migration
supabase migration new add_posts_table

# Edit migration file in supabase/migrations/

# Apply migration locally
supabase db reset

# Push to production
supabase db push

# Pull remote schema
supabase db pull
ローカル DB から型を生成する
# Generate TypeScript types
supabase gen types typescript --local > types/database.ts

テスト

RLS ポリシーのテスト
-- Test as specific user
SET LOCAL ROLE authenticated;
SET LOCAL "request.jwt.claims" TO '{"sub": "user-uuid-here"}';

-- Test query
SELECT * FROM posts;

-- Reset
RESET ROLE;
Supabase Test Helpers を使用したテスト
// tests/posts.test.ts
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
	process.env.SUPABASE_URL!,
	process.env.SUPABASE_SERVICE_ROLE_KEY!, // For testing
);

describe("Posts", () => {
	beforeEach(async () => {
		// Clean up
		await supabase
			.from("posts")
			.delete()
			.neq("id", "00000000-0000-0000-0000-000000000000");
	});

	it("should create post", async () => {
		const { data, error } = await supabase
			.from("posts")
			.insert({ title: "Test", content: "Test" })
			.select()
			.single();

		expect(error).toBeNull();
		expect(data.title).toBe("Test");
	});
});

エラー処理

包括的なエラーハンドラー
import { PostgrestError } from "@supabase/supabase-js";

export function handleSupabaseError(error: PostgrestError | null) {
	if (!error) {
		return null;
	}

	// Common error codes
	const errorMessages: Record<string, string> = {
		"23505": "This record already exists", // Unique violation
		"23503": "Related record not found", // Foreign key violation
		"42P01": "Table does not exist",
		"42501": "Permission denied",
		PGRST116: "No rows found",
	};

	const userMessage = errorMessages[error.code] || error.message;

	console.error("Supabase error:", {
		code: error.code,
		message: error.message,
		details: error.details,
		hint: error.hint,
	});

	return userMessage;
}

// Usage
const { data, error } = await supabase.from("posts").insert(postData);

if (error) {
	const message = handleSupabaseError(error);
	toast.error(message);
	return;
}

ベストプラクティス

  1. 行レベルセキュリティ:

    • すべてのテーブルで RLS を有効にする
    • クライアント側のチェックだけに頼らない
    • ポリシーを徹底的にテストする
    • service role key は必要最小限に使用する(サーバー側のみ)
  2. クエリの最適化:

    • 必要な列を指定するには .select() を使用する
    • フィルターや並べ替えの対象となる列にデータベースインデックスを追加する
    • 結果数を制限するには .limit() を使用する
    • 大規模なデータセットではページネーションを検討する
  3. リアルタイムサブスクリプション:

    • コンポーネントのアンマウント時には必ず購読を解除する
    • RLS ポリシーを使用してイベントをフィルタリングする
    • 一時的なデータにはブロードキャストを使用する
    • 同時に実行するサブスクリプションの数を制限する
  4. 認証:

    • 可能であれば JWT を httpOnly Cookie に保存する
    • 有効期限が切れる前にトークンを更新する
    • 認証状態の変更を処理する
    • サーバー側でユーザーを検証する
  5. ストレージ:

    • 適切なバケットポリシーを設定する
    • 最適化のために画像変換を使用する
    • ストレージの上限を考慮する
    • 使用されていないファイルを削除する
  6. エラー処理:

    • 必ず error オブジェクトを確認する
    • ユーザーにわかりやすいエラーメッセージを表示する
    • デバッグ用にエラーをログに記録する
    • ネットワーク障害を適切に処理する

よくあるパターン

サインアップ時にプロフィールを自動作成する
-- Create profiles table
CREATE TABLE profiles (
  id UUID REFERENCES auth.users PRIMARY KEY,
  name TEXT,
  avatar_url TEXT,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Trigger function
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.profiles (id, name, avatar_url)
  VALUES (
    NEW.id,
    NEW.raw_user_meta_data->>'name',
    NEW.raw_user_meta_data->>'avatar_url'
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Trigger
CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();

ドキュメントのクイックリファレンス

特定の情報をお探しですか?

2,616 件のドキュメントファイルを検索します:

# Search all docs
grep -r "search term" docs/supabase_com/

# Find guides
ls docs/supabase_com/guides_*

# Find API reference
ls docs/supabase_com/reference_*

よく使うドキュメントの場所:

  • ガイド:docs/supabase_com/guides_*
  • JavaScript リファレンス:docs/supabase_com/reference_javascript_*
  • データベース:docs/supabase_com/guides_database_*
  • 認証:docs/supabase_com/guides_auth_*
  • ストレージ:docs/supabase_com/guides_storage_*

リソース

実装チェックリスト

  • Supabase プロジェクトを作成する
  • インストールする:npm install @supabase/supabase-js
  • 環境変数を設定する
  • データベーススキーマを設計する
  • マイグレーションを作成する
  • RLS を有効にしてポリシーを作成する
  • 認証を設定する
  • 認証状態の管理を実装する
  • CRUD 操作を作成する
  • リアルタイムサブスクリプションを追加する(必要な場合)
  • ストレージバケットを設定する(必要な場合)
  • RLS ポリシーをテストする
  • データベースインデックスを追加する
  • エッジ関数をデプロイする(必要な場合)
  • 本番環境でテストする

コメント