
Supabase
- 50 installs
- 2 repo stars
- Updated August 3, 2026
- fandhe-ai/agent-reference-skills
Helps with ai & agent building tasks.
About
supabase is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- supabase
- AI & Agent Building
- AI-coding skill
Supabase by the numbers
- 50 all-time installs (skills.sh)
- Ranked #7,298 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fandhe-ai/agent-reference-skills --skill supabaseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 50 |
|---|---|
| repo stars | ★ 2 |
| Last updated | August 3, 2026 |
| Repository | fandhe-ai/agent-reference-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
埋め込み生成
テキストや画像をベクトル表現に変換するための各種埋め込みモデルと Supabase での利用方法。
概要
埋め込み(Embedding)は、テキストや画像などの非構造化データを固定長の数値ベクトルに変換したもの。意味的に近いデータは近いベクトルになる。Supabase では以下の方法で埋め込みを生成できる。
OpenAI Embeddings
最も広く使われる埋め込みモデル。text-embedding-3-small(1536次元)と text-embedding-3-large(3072次元)が主要モデル。
Hugging Face
オープンソースモデルを利用可能。Transformers.js を使って Edge Functions 内で直接実行することもできる。
Supabase AI(Edge Functions 内蔵モデル)
Edge Functions 内で Supabase.ai セッションを使い、ビルトインの gte-small モデル(384次元)でサーバーサイド埋め込み生成が可能。外部 API コール不要。
自動埋め込み(Automatic Embeddings)
テーブルにデータが挿入・更新されたときに自動的に埋め込みを生成するトリガーベースの機能。手動での埋め込み管理が不要になる。
コード例
// === OpenAI で埋め込み生成 ===
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: 'The quick brown fox jumps over the lazy dog',
});
const embedding = response.data[0].embedding; // number[] (1536次元)
// Supabase に格納
const { error } = await supabase
.from('documents')
.insert({
content: 'The quick brown fox jumps over the lazy dog',
embedding: JSON.stringify(embedding),
});// === Supabase AI(Edge Functions 内蔵モデル) ===
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
serve(async (req) => {
const { input } = await req.json();
// ビルトインの gte-small モデルを使用(外部 API 不要)
const session = new Supabase.ai.Session('gte-small');
const embedding = await session.run(input, {
mean_pool: true,
normalize: true,
});
return new Response(JSON.stringify({ embedding }), {
headers: { 'Content-Type': 'application/json' },
});
});// === Edge Functions で OpenAI 埋め込み + Supabase 格納 ===
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
import OpenAI from 'https://esm.sh/openai@4';
serve(async (req) => {
const { content } = await req.json();
const openai = new OpenAI({ apiKey: Deno.env.get('OPENAI_API_KEY') });
const embeddingResponse = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: content,
});
const embedding = embeddingResponse.data[0].embedding;
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
const { error } = await supabase
.from('documents')
.insert({ content, embedding });
if (error) throw error;
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
});
});-- === 自動埋め込み(トリガーベース) ===
-- ai.automatic_embeddings を使用してテーブルの変更を自動でベクトル化
-- 自動埋め込みの設定(ダッシュボードから設定推奨)
-- 対象テーブル、ソースカラム、埋め込みモデル、出力カラムを指定
select ai.create_vectorizer(
'public.documents'::regclass,
destination => 'documents_embeddings',
embedding => ai.embedding_openai('text-embedding-3-small', 1536),
chunking => ai.chunking_recursive_character_text_splitter('body')
);注意点
- OpenAI の埋め込みモデルは API 呼び出しごとに課金される。大量データの処理にはコスト計算が必要
gte-small(384次元)は軽量だが精度は OpenAI モデルより劣る場合がある。用途に応じて選択- 埋め込みモデルを変更すると、既存のベクトルとの互換性がなくなる。全データの再埋め込みが必要
- 同じモデル・同じバージョンで生成した埋め込み同士でのみ距離計算が有効
- バッチ処理で大量の埋め込みを生成する場合、API のレート制限に注意
- Edge Functions 内蔵の
Supabase.aiは Deno ランタイムでのみ利用可能
関連
- AI & Vectors 概要
- ベクトルカラム
- セマンティック検索
- RAG パイプライン
ハイブリッド検索
ベクトル検索(セマンティック検索)と全文検索(キーワード検索)を組み合わせた高精度な検索手法。
概要
ハイブリッド検索は、セマンティック検索とキーワード検索それぞれの長所を組み合わせる手法。セマンティック検索は意味的な類似性を捉えるが、固有名詞や専門用語のマッチが弱い。キーワード検索は正確な語句マッチに強いが、同義語や言い換えに対応できない。両者を組み合わせることで、より高精度な検索を実現する。
RRF(Reciprocal Rank Fusion)
複数の検索結果のランキングを統合するアルゴリズム。各結果のランク順位に基づいてスコアを計算し、統合する。
RRF スコア = Σ 1 / (k + rank_i)k は定数(通常 60)で、上位ランクの結果に過度に重みが偏るのを防ぐ。
PostgreSQL の全文検索
PostgreSQL 組み込みの tsvector / tsquery を使用した全文検索。to_tsvector() でテキストをトークン化し、to_tsquery() でクエリを構成する。GIN インデックスで高速化。
コード例
-- 全文検索用のカラムとインデックスを追加
alter table documents add column fts tsvector
generated always as (to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))) stored;
create index on documents using gin (fts);
-- キーワード検索関数
create or replace function keyword_search(
query text,
match_count int default 10
)
returns table (
id bigint,
content text,
rank real
)
language sql stable
as $$
select
documents.id,
documents.content,
ts_rank(documents.fts, websearch_to_tsquery('english', query)) as rank
from documents
where documents.fts @@ websearch_to_tsquery('english', query)
order by rank desc
limit match_count;
$$;
-- セマンティック検索関数
create or replace function semantic_search(
query_embedding vector(1536),
match_count int default 10
)
returns table (
id bigint,
content text,
similarity float
)
language sql stable
as $$
select
documents.id,
documents.content,
1 - (documents.embedding <=> query_embedding) as similarity
from documents
order by documents.embedding <=> query_embedding
limit match_count;
$$;
-- ハイブリッド検索関数(RRF による統合)
create or replace function hybrid_search(
query_text text,
query_embedding vector(1536),
match_count int default 10,
full_text_weight float default 1.0,
semantic_weight float default 1.0,
rrf_k int default 60
)
returns table (
id bigint,
content text,
score float
)
language sql stable
as $$
with
-- 全文検索の結果にランク付け
full_text as (
select
id,
row_number() over (order by ts_rank(fts, websearch_to_tsquery('english', query_text)) desc) as rank_ix
from documents
where fts @@ websearch_to_tsquery('english', query_text)
order by rank_ix
limit least(match_count, 30) * 2
),
-- セマンティック検索の結果にランク付け
semantic as (
select
id,
row_number() over (order by embedding <=> query_embedding) as rank_ix
from documents
order by rank_ix
limit least(match_count, 30) * 2
)
-- RRF で統合
select
documents.id,
documents.content,
coalesce(1.0 / (rrf_k + full_text.rank_ix), 0.0) * full_text_weight +
coalesce(1.0 / (rrf_k + semantic.rank_ix), 0.0) * semantic_weight as score
from
full_text
full outer join semantic on full_text.id = semantic.id
join documents on documents.id = coalesce(full_text.id, semantic.id)
order by score desc
limit match_count;
$$;// クライアント側の実装
import { createClient } from '@supabase/supabase-js';
import OpenAI from 'openai';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function hybridSearch(query: string) {
// クエリをベクトル化
const embeddingResponse = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query,
});
const queryEmbedding = embeddingResponse.data[0].embedding;
// ハイブリッド検索を実行
const { data, error } = await supabase.rpc('hybrid_search', {
query_text: query,
query_embedding: queryEmbedding,
match_count: 10,
full_text_weight: 1.0,
semantic_weight: 1.0,
rrf_k: 60,
});
if (error) throw error;
return data;
}注意点
full_text_weightとsemantic_weightのバランスはデータと用途に応じて調整する- 全文検索には適切な言語設定(
'english','japanese'等)が必要。日本語はpgroonga等の追加 Extension が必要な場合がある websearch_to_tsqueryは Google 検索のような自然な構文(AND/OR/NOT、引用符)をサポート- RRF の
k値(デフォルト 60)を大きくすると上位と下位のスコア差が小さくなる - 両方の検索で一方にしか出てこない結果は、
full outer joinによりもう一方のスコアが 0 として統合される - パフォーマンスのため、各検索の中間結果を
match_count * 2程度に制限する
関連
- セマンティック検索
- ベクトルインデックス
- RAG パイプライン
外部連携
LangChain、LlamaIndex、Amazon Bedrock など主要 AI フレームワークとの Supabase ベクトルストア連携。
概要
Supabase の pgvector は、主要な AI/LLM フレームワークからベクトルストアとして利用できる。各フレームワークは専用のインテグレーションモジュールを提供しており、少ない設定コードで Supabase をベクトルデータベースとして組み込める。
対応フレームワーク
| フレームワーク | パッケージ | 言語 |
|---|---|---|
| LangChain | @langchain/community / langchain (Python) | TypeScript / Python |
| LlamaIndex | llama-index-vector-stores-supabase | Python |
| Amazon Bedrock | Knowledge Base 連携 | - |
コード例
// === LangChain (TypeScript) ===
import { SupabaseVectorStore } from '@langchain/community/vectorstores/supabase';
import { OpenAIEmbeddings } from '@langchain/openai';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
modelName: 'text-embedding-3-small',
});
// ベクトルストアの作成とドキュメント追加
const vectorStore = await SupabaseVectorStore.fromTexts(
[
'Supabase is an open source Firebase alternative.',
'PostgreSQL is a powerful relational database.',
'pgvector adds vector operations to PostgreSQL.',
],
[
{ source: 'supabase-docs' },
{ source: 'postgres-docs' },
{ source: 'pgvector-docs' },
],
embeddings,
{
client: supabase,
tableName: 'documents',
queryName: 'match_documents',
}
);
// 類似検索
const results = await vectorStore.similaritySearch(
'What is Supabase?',
5 // match_count
);
console.log(results);
// 既存のベクトルストアに接続
const existingStore = new SupabaseVectorStore(embeddings, {
client: supabase,
tableName: 'documents',
queryName: 'match_documents',
});
// フィルタ付き検索
const filteredResults = await existingStore.similaritySearch(
'database features',
5,
{ source: 'postgres-docs' }
);# === LangChain (Python) ===
from langchain_community.vectorstores import SupabaseVectorStore
from langchain_openai import OpenAIEmbeddings
from supabase import create_client
supabase = create_client(
"https://[REF].supabase.co",
"[SERVICE_ROLE_KEY]"
)
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
openai_api_key="[OPENAI_API_KEY]"
)
# ドキュメントからベクトルストアを作成
vector_store = SupabaseVectorStore.from_texts(
texts=[
"Supabase is an open source Firebase alternative.",
"PostgreSQL is a powerful relational database.",
],
metadatas=[
{"source": "supabase-docs"},
{"source": "postgres-docs"},
],
embedding=embeddings,
client=supabase,
table_name="documents",
query_name="match_documents",
)
# 類似検索
results = vector_store.similarity_search("What is Supabase?", k=5)
# スコア付き検索
results_with_scores = vector_store.similarity_search_with_relevance_scores(
"What is Supabase?", k=5
)
for doc, score in results_with_scores:
print(f"Score: {score:.4f} - {doc.page_content[:100]}")
# Retriever として使用(LangChain の chain に組み込み)
retriever = vector_store.as_retriever(search_kwargs={"k": 5})# === LlamaIndex ===
# pip install llama-index-vector-stores-supabase
from llama_index.vector_stores.supabase import SupabaseVectorStore
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, StorageContext
# Supabase ベクトルストアの設定
vector_store = SupabaseVectorStore(
postgres_connection_string="postgresql://postgres:[PASSWORD]@db.[REF].supabase.co:5432/postgres",
collection_name="documents",
dimension=1536,
)
# ストレージコンテキストの作成
storage_context = StorageContext.from_defaults(vector_store=vector_store)
# ドキュメントの読み込みとインデックス作成
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(
documents,
storage_context=storage_context,
)
# クエリエンジンの作成
query_engine = index.as_query_engine()
response = query_engine.query("What is Supabase?")
print(response)
# 既存のインデックスに接続
index = VectorStoreIndex.from_vector_store(vector_store=vector_store)# === Amazon Bedrock Knowledge Base ===
# Supabase を Amazon Bedrock Knowledge Base のベクトルストアとして使用
# AWS コンソールまたは AWS SDK で設定
import boto3
bedrock_agent = boto3.client("bedrock-agent", region_name="us-east-1")
# Knowledge Base の作成(Supabase をストレージとして指定)
# 実際の設定は AWS コンソールから行うのが推奨
# 必要な情報:
# - Supabase Database URL
# - テーブル名
# - ベクトルカラム名
# - テキストカラム名
# - メタデータカラム名-- LangChain / LlamaIndex で使用するテーブルとマッチ関数
-- LangChain のデフォルトスキーマ
create table documents (
id bigserial primary key,
content text,
metadata jsonb,
embedding vector(1536)
);
create or replace function match_documents(
query_embedding vector(1536),
filter jsonb default '{}',
match_count int default 10
)
returns table (
id bigint,
content text,
metadata jsonb,
similarity float
)
language plpgsql
as $$
begin
return query
select
documents.id,
documents.content,
documents.metadata,
1 - (documents.embedding <=> query_embedding) as similarity
from documents
where documents.metadata @> filter
order by documents.embedding <=> query_embedding
limit match_count;
end;
$$;注意点
- LangChain の
SupabaseVectorStoreはmatch_documentsという名前の RPC 関数を期待する。テーブル名・関数名はカスタマイズ可能 - LlamaIndex は
vecsライブラリを内部的に使用し、vecsスキーマにテーブルを作成する service_role keyを使うと RLS がバイパスされるため、サーバーサイドでのみ使用する- LangChain のメタデータフィルタは JSONB の
@>演算子でマッチするため、metadataカラムに GIN インデックスを作成すると高速化できる - フレームワークのバージョンアップにより API が変更される場合がある。公式ドキュメントを確認
- Amazon Bedrock 連携は AWS 側の設定も必要。IAM ロール、VPC 設定等が必要な場合がある
関連
- AI & Vectors 概要
- セマンティック検索
- RAG パイプライン
- Python クライアント
AI & Vectors 概要
Supabase における AI・ベクトル検索機能の全体像。pgvector Extension を中心としたベクトルデータの格納・検索基盤。
概要
Supabase は PostgreSQL の pgvector Extension を利用して、ベクトルデータの格納・インデックス作成・類似度検索を提供する。これにより、セマンティック検索、RAG(Retrieval-Augmented Generation)、レコメンデーションなどの AI ワークフローを PostgreSQL 上で直接実現できる。
pgvector Extension
pgvector は PostgreSQL にベクトル型とベクトル演算を追加する Extension。Supabase では全プロジェクトでデフォルト利用可能。
ベクトル型
vector(n) 型で n 次元のベクトルを格納する。n は埋め込みモデルの出力次元数に合わせる(例: OpenAI text-embedding-3-small は 1536 次元)。
距離関数
| 演算子 | 距離関数 | 説明 |
|---|---|---|
<-> | L2 距離(ユークリッド距離) | 値が小さいほど類似 |
<#> | 内積(Inner Product)の負値 | 値が小さいほど類似(正規化済みベクトル向き) |
<=> | コサイン距離 | 値が小さいほど類似(0〜2 の範囲) |
Supabase 上での AI ワークフロー
1. 埋め込み生成: OpenAI / Hugging Face / Supabase AI でテキストをベクトル化 2. ベクトル格納: vector 型カラムに INSERT 3. インデックス作成: HNSW または IVFFlat でパフォーマンス向上 4. 類似度検索: 距離演算子で近傍検索 5. アプリケーション統合: supabase-js / REST API / Edge Functions から利用
検索の種類
| 種別 | 説明 | 用途 |
|---|---|---|
| セマンティック検索 | ベクトル類似度で意味的に近いドキュメントを検索 | 自然言語クエリ、推薦 |
| キーワード検索 | PostgreSQL 全文検索で正確な語句にマッチ | 専門用語、固有名詞、精度重視 |
| ハイブリッド検索 | セマンティック + キーワードの組み合わせ | 両方の利点が必要な場合 |
コード例
-- pgvector Extension を有効化
create extension if not exists vector with schema extensions;
-- ベクトルカラムを持つテーブル作成
create table documents (
id bigserial primary key,
content text,
embedding vector(1536)
);
-- コサイン類似度で上位 5 件を検索
select id, content, 1 - (embedding <=> '[0.1, 0.2, ...]'::vector) as similarity
from documents
order by embedding <=> '[0.1, 0.2, ...]'::vector
limit 5;注意点
- pgvector は Supabase ダッシュボードの「Extensions」から有効化するか、SQL で
create extensionを実行する - ベクトルの次元数は最大 2000 次元まで対応(pgvector 0.7.0 以降)
- 大量のベクトルデータにはインデックスが必須。インデックスなしではフルスキャンとなり遅い
- RLS(Row Level Security)はベクトル検索にも適用される
vector型はextensionsスキーマに作成することが推奨される
関連
- ベクトルカラム
- ベクトルインデックス
- 埋め込み生成
- セマンティック検索
- ハイブリッド検索
- RAG パイプライン
- 外部連携
Python クライアント
vecs ライブラリを使った Python からのベクトル操作。コレクション管理、ベクトルの upsert・query。
概要
vecs は Supabase が提供する Python ライブラリで、pgvector を使ったベクトル操作を簡潔に行える。コレクション(ベクトルテーブル)の作成、ベクトルの追加・検索をシンプルな API で提供する。Google Colab や Jupyter Notebook での利用にも適している。
vecs の特徴
- コレクションベースの抽象化(内部的には PostgreSQL テーブル)
- 自動的にベクトルインデックスを作成
- メタデータフィルタリング対応
- バッチ upsert 対応
- 接続文字列ベースの簡単なセットアップ
コード例
# === インストール ===
# pip install vecs
import vecs
# === 接続 ===
# Supabase プロジェクトの Database URL を使用
vx = vecs.create_client("postgresql://postgres:[PASSWORD]@db.[PROJECT_REF].supabase.co:5432/postgres")
# === コレクション作成 ===
# 1536 次元のベクトルコレクションを作成
docs = vx.get_or_create_collection(name="documents", dimension=1536)
# === ベクトルの upsert ===
# (id, vector, metadata) のタプルリストで upsert
docs.upsert(
records=[
(
"doc_1",
[0.0013, -0.0245, 0.0112, ...], # 1536 次元のベクトル
{"title": "Introduction to AI", "category": "tech"}
),
(
"doc_2",
[0.0042, 0.0198, -0.0067, ...],
{"title": "Machine Learning Basics", "category": "tech"}
),
]
)
# === インデックス作成 ===
# コサイン距離でインデックスを作成
docs.create_index(
method=vecs.IndexMethod.hnsw,
measure=vecs.IndexMeasure.cosine_distance,
)
# IVFFlat インデックスの場合
docs.create_index(
method=vecs.IndexMethod.ivfflat,
measure=vecs.IndexMeasure.cosine_distance,
)
# === クエリ(類似検索) ===
query_vector = [0.0013, -0.0245, ...] # 1536 次元
results = docs.query(
data=query_vector,
limit=10,
include_value=True, # 距離値を含める
include_metadata=True, # メタデータを含める
)
for result in results:
print(result)
# ('doc_1', 0.123, {'title': 'Introduction to AI', 'category': 'tech'})
# === メタデータフィルタリング ===
results = docs.query(
data=query_vector,
limit=5,
filters={"category": {"$eq": "tech"}},
include_metadata=True,
)
# 複合フィルタ
results = docs.query(
data=query_vector,
limit=5,
filters={
"$and": [
{"category": {"$eq": "tech"}},
{"year": {"$gte": 2023}},
]
},
)# === OpenAI と組み合わせた完全な例 ===
import vecs
import openai
# 接続
vx = vecs.create_client("postgresql://postgres:[PASSWORD]@db.[REF].supabase.co:5432/postgres")
docs = vx.get_or_create_collection(name="documents", dimension=1536)
# ドキュメントの埋め込みと格納
def ingest_documents(texts: list[str], metadatas: list[dict]):
# OpenAI で埋め込み生成
response = openai.embeddings.create(
model="text-embedding-3-small",
input=texts,
)
records = [
(f"doc_{i}", emb.embedding, meta)
for i, (emb, meta) in enumerate(zip(response.data, metadatas))
]
docs.upsert(records=records)
# 検索
def search(query: str, limit: int = 5):
# クエリを埋め込み
response = openai.embeddings.create(
model="text-embedding-3-small",
input=query,
)
query_embedding = response.data[0].embedding
return docs.query(
data=query_embedding,
limit=limit,
include_value=True,
include_metadata=True,
)# === Google Colab での利用例 ===
# セルでインストール
# !pip install vecs openai
import vecs
import os
# 環境変数またはシークレットから接続情報を取得
# Google Colab のシークレット機能を使う場合:
# from google.colab import userdata
# db_url = userdata.get('SUPABASE_DB_URL')
vx = vecs.create_client(os.environ["SUPABASE_DB_URL"])
docs = vx.get_or_create_collection(name="my_collection", dimension=384)
# コレクション一覧
collections = vx.list_collections()
print(collections) # ['documents', 'my_collection']
# コレクション削除
# vx.delete_collection("my_collection")
# 接続を閉じる
vx.disconnect()# === supabase-py でのベクトル操作 ===
from supabase import create_client
supabase = create_client(
"https://[REF].supabase.co",
"[ANON_KEY]"
)
# RPC 関数を呼び出してセマンティック検索
result = supabase.rpc(
"match_documents",
{
"query_embedding": query_vector,
"match_threshold": 0.78,
"match_count": 10,
}
).execute()
print(result.data)注意点
vecsは内部的にvecsスキーマにテーブルを作成する(publicスキーマではない)- Database URL はダッシュボードの「Settings > Database > Connection string > URI」から取得
- Google Colab ではシークレット機能を使い、接続文字列をコードにハードコードしない
upsertは同じ ID のレコードがあれば更新する。ID はユニークである必要がある- インデックスは大量データ投入後に作成するのが効率的(特に IVFFlat)
vecsはsupabase-pyとは別のライブラリ。用途に応じて使い分ける- フィルタ演算子:
$eq,$ne,$gt,$gte,$lt,$lte,$in,$and,$or
関連
- AI & Vectors 概要
- ベクトルカラム
- セマンティック検索
- 外部連携
RAG パイプライン
Retrieval-Augmented Generation。ドキュメントの分割・埋め込み・検索・LLM への受け渡しによる、知識ベースを活用した AI 応答生成。
概要
RAG(Retrieval-Augmented Generation)は、LLM に外部知識を与えて正確な応答を生成する手法。Supabase を使った RAG パイプラインは以下のステップで構成される。
RAG パイプラインの流れ
1. ドキュメント分割(Chunking): 長いドキュメントを適切なサイズのチャンクに分割 2. 埋め込み生成: 各チャンクをベクトル化 3. ベクトル格納: pgvector でデータベースに格納 4. 検索(Retrieval): ユーザーのクエリに類似するチャンクを検索 5. 生成(Generation): 検索結果をコンテキストとして LLM に渡し、回答を生成
RLS による権限付き RAG
Supabase の RLS を活用することで、ユーザーごとにアクセス可能なドキュメントのみを検索対象にできる。これにより、マルチテナント環境でも安全な RAG を実現。
コード例
-- RAG 用テーブルの作成
create table documents (
id bigserial primary key,
content text not null,
metadata jsonb default '{}',
embedding vector(1536),
user_id uuid references auth.users(id),
created_at timestamptz default now()
);
-- RLS の有効化
alter table documents enable row level security;
-- ユーザーは自分のドキュメントのみアクセス可能
create policy "Users can access own documents"
on documents for select
using (auth.uid() = user_id);
-- ベクトルインデックスの作成
create index on documents
using hnsw (embedding vector_cosine_ops)
with (m = 16, ef_construction = 64);
-- RAG 用の検索関数
create or replace function match_documents(
query_embedding vector(1536),
match_threshold float default 0.78,
match_count int default 5
)
returns table (
id bigint,
content text,
metadata jsonb,
similarity float
)
language plpgsql
as $$
begin
return query
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;
end;
$$;// === Edge Functions で RAG パイプライン実装 ===
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
import OpenAI from 'https://esm.sh/openai@4';
const openai = new OpenAI({ apiKey: Deno.env.get('OPENAI_API_KEY') });
serve(async (req) => {
const { query } = await req.json();
// Authorization ヘッダーからユーザートークンを取得
const authHeader = req.headers.get('Authorization')!;
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
);
// 1. クエリをベクトル化
const embeddingResponse = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query,
});
const queryEmbedding = embeddingResponse.data[0].embedding;
// 2. 類似ドキュメントを検索(RLS が適用される)
const { data: documents, error } = await supabase.rpc('match_documents', {
query_embedding: queryEmbedding,
match_threshold: 0.78,
match_count: 5,
});
if (error) throw error;
// 3. 検索結果をコンテキストとして LLM に渡す
const context = documents.map((doc: any) => doc.content).join('\n\n');
const chatResponse = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `You are a helpful assistant. Use the following context to answer the user's question. If the context doesn't contain relevant information, say so.\n\nContext:\n${context}`,
},
{
role: 'user',
content: query,
},
],
max_tokens: 1024,
});
const answer = chatResponse.choices[0].message.content;
return new Response(
JSON.stringify({
answer,
sources: documents.map((doc: any) => ({
id: doc.id,
content: doc.content.substring(0, 200),
similarity: doc.similarity,
})),
}),
{ headers: { 'Content-Type': 'application/json' } }
);
});// === ドキュメントの分割と埋め込み格納 ===
import { createClient } from '@supabase/supabase-js';
import OpenAI from 'openai';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// ドキュメントをチャンクに分割
function splitDocument(text: string, chunkSize = 1000, overlap = 200): string[] {
const chunks: string[] = [];
let start = 0;
while (start < text.length) {
const end = Math.min(start + chunkSize, text.length);
chunks.push(text.slice(start, end));
start += chunkSize - overlap;
}
return chunks;
}
async function ingestDocument(content: string, metadata: Record<string, any>) {
const chunks = splitDocument(content);
// バッチで埋め込みを生成
const embeddingResponse = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: chunks,
});
// Supabase に格納
const rows = chunks.map((chunk, i) => ({
content: chunk,
metadata,
embedding: embeddingResponse.data[i].embedding,
}));
const { error } = await supabase.from('documents').insert(rows);
if (error) throw error;
}// === クライアント側から RAG Edge Function を呼び出す ===
const { data, error } = await supabase.functions.invoke('rag-search', {
body: { query: 'How do I set up authentication?' },
});
console.log(data.answer); // LLM の回答
console.log(data.sources); // 参照されたドキュメント注意点
- チャンクサイズはモデルのコンテキストウィンドウと検索精度のトレードオフ。500〜1500 文字が一般的
- オーバーラップ(重複)を設定すると、チャンク境界にまたがる情報の欠落を防げる
- RLS を適用した RAG では、
anon key+ ユーザートークンを使う。service_role keyを使うと RLS がバイパスされる - Edge Functions のタイムアウト(デフォルト 150 秒)に注意。大量のドキュメント処理はバックグラウンドジョブで行う
- LLM に渡すコンテキストが大きすぎるとトークン制限に達する。検索結果数を適切に制限する
- ハルシネーション防止のため、回答に「ソースに基づく情報がない」旨を含めるプロンプト設計が重要
関連
- セマンティック検索
- 埋め込み生成
- ハイブリッド検索
- ベクトルインデックス
ai
| Name | Description | Path |
|---|---|---|
| 埋め込み生成 | テキストや画像をベクトル表現に変換するための各種埋め込みモデルと Supabase での… | embeddings.md |
| ハイブリッド検索 | ベクトル検索(セマンティック検索)と全文検索(キーワード検索)を組み合わ… | hybrid-search.md |
| 外部連携 | LangChain、LlamaIndex、Amazon Bedrock など主要 AI フレームワークとの… | integrations.md |
| AI & Vectors 概要 | Supabase における AI・ベクトル検索機能の全体像。pgvector Extension を… | overview.md |
| Python クライアント | vecs ライブラリを使った Python からのベクトル操作。コレクション管理、… | python-clients.md |
| RAG パイプライン | Retrieval-Augmented Generation。ドキュメントの分割・埋め込み・検索・… | rag.md |
| セマンティック検索 | ベクトル類似度に基づく意味的な検索。match_documents パターンによるク… | semantic-search.md |
| ベクトルカラム | pgvector の vector(n) 型カラムの定義・操作方法と距離演算子の使い方。 | vector-columns.md |
| ベクトルインデックス | HNSW と IVFFlat の 2 種類のベクトルインデックスによる近似最近傍検索… | vector-indexes.md |
セマンティック検索
ベクトル類似度に基づく意味的な検索。match_documents パターンによるクエリベクトルと格納済みベクトルの近傍検索。
概要
セマンティック検索は、キーワードの完全一致ではなく、テキストの「意味」に基づいて検索結果を返す。クエリテキストをベクトル化し、データベースに格納済みのベクトルとの距離を計算して類似度の高い結果を返す。
検索フロー
1. ユーザーのクエリテキストを埋め込みモデルでベクトル化 2. PostgreSQL の距離演算子でベクトル間の類似度を計算 3. 類似度の高い順にソートして返却 4. match_threshold で最低類似度を設定し、無関係な結果を除外
match_documents 関数パターン
Supabase での標準的なセマンティック検索パターン。RPC 関数として定義し、supabase-js の rpc() から呼び出す。
コード例
-- match_documents 関数の作成
create or replace function match_documents(
query_embedding vector(1536),
match_threshold float default 0.78,
match_count int default 10
)
returns table (
id bigint,
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;
$$;// === クライアント側の実装 ===
import { createClient } from '@supabase/supabase-js';
import OpenAI from 'openai';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function semanticSearch(query: string) {
// 1. クエリをベクトル化
const embeddingResponse = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query,
});
const queryEmbedding = embeddingResponse.data[0].embedding;
// 2. RPC で match_documents を呼び出し
const { data, error } = await supabase.rpc('match_documents', {
query_embedding: queryEmbedding,
match_threshold: 0.78,
match_count: 10,
});
if (error) throw error;
return data;
}
// 使用例
const results = await semanticSearch('How to deploy a Next.js app?');
console.log(results);
// [
// { id: 42, content: 'Deploying Next.js...', metadata: {...}, similarity: 0.92 },
// { id: 17, content: 'Next.js deployment guide...', metadata: {...}, similarity: 0.87 },
// ...
// ]-- RLS を考慮した match_documents(認証ユーザーのデータのみ検索)
create or replace function match_user_documents(
query_embedding vector(1536),
match_threshold float default 0.78,
match_count int default 10
)
returns table (
id bigint,
content text,
similarity float
)
language plpgsql security definer
as $$
begin
return query
select
documents.id,
documents.content,
1 - (documents.embedding <=> query_embedding) as similarity
from documents
where
documents.user_id = auth.uid()
and 1 - (documents.embedding <=> query_embedding) > match_threshold
order by documents.embedding <=> query_embedding
limit match_count;
end;
$$;-- フィルタ付きセマンティック検索
create or replace function match_documents_by_category(
query_embedding vector(1536),
filter_category text,
match_threshold float default 0.78,
match_count int default 10
)
returns table (
id bigint,
content text,
category text,
similarity float
)
language sql stable
as $$
select
documents.id,
documents.content,
documents.category,
1 - (documents.embedding <=> query_embedding) as similarity
from documents
where
documents.category = filter_category
and 1 - (documents.embedding <=> query_embedding) > match_threshold
order by documents.embedding <=> query_embedding
limit match_count;
$$;注意点
match_thresholdは 0〜1 の範囲。0.78 はよく使われるデフォルト値だが、データとモデルに応じて調整が必要- コサイン類似度は
1 - コサイン距離で計算する。<=>演算子はコサイン距離を返す language sql stableを指定すると PostgreSQL のオプティマイザが最適化しやすくなる- RLS を適用する場合は
security definerを使い、関数内でフィルタ条件を明示するパターンが一般的 - クエリの埋め込みと格納済み埋め込みは同じモデル・同じ次元数である必要がある
order byの後にlimitを付けないと全件返却となりパフォーマンスが低下する
関連
- 埋め込み生成
- ベクトルインデックス
- ハイブリッド検索
- RAG パイプライン
ベクトルカラム
pgvector の vector(n) 型カラムの定義・操作方法と距離演算子の使い方。
概要
ベクトルカラムは vector(n) 型で定義する。n は次元数を指定し、利用する埋め込みモデルの出力次元数に合わせる。ベクトルデータは文字列リテラル '[0.1, 0.2, ...]' の形式で INSERT / SELECT する。
主要な埋め込みモデルと次元数
| モデル | 次元数 |
|---|---|
| OpenAI text-embedding-3-small | 1536 |
| OpenAI text-embedding-3-large | 3072 |
| OpenAI text-embedding-ada-002 | 1536 |
| Cohere embed-english-v3.0 | 1024 |
| Supabase gte-small | 384 |
距離演算子
| 演算子 | 名称 | インデックス ops クラス | 用途 |
|---|---|---|---|
<-> | L2 距離(ユークリッド距離) | vector_l2_ops | 一般的な類似度検索 |
<#> | 内積の負値(Negative Inner Product) | vector_ip_ops | 正規化済みベクトルでの高速検索 |
<=> | コサイン距離 | vector_cosine_ops | テキスト埋め込みで最も一般的 |
コサイン類似度は 1 - コサイン距離 で計算できる。
コード例
-- テーブル作成
create table documents (
id bigserial primary key,
title text not null,
body text,
embedding vector(1536)
);
-- ベクトルデータの INSERT
insert into documents (title, body, embedding)
values (
'Introduction to AI',
'Artificial intelligence is...',
'[0.0013, -0.0245, 0.0112, ...]'
);
-- コサイン距離で類似検索
select
id,
title,
1 - (embedding <=> query_embedding) as cosine_similarity
from documents, (select '[0.0013, -0.0245, ...]'::vector(1536) as query_embedding) q
order by embedding <=> query_embedding
limit 10;
-- L2 距離で類似検索
select id, title, embedding <-> '[0.0013, -0.0245, ...]'::vector as l2_distance
from documents
order by embedding <-> '[0.0013, -0.0245, ...]'::vector
limit 10;
-- 内積で類似検索(正規化済みベクトル向け)
select id, title, (embedding <#> '[0.0013, -0.0245, ...]'::vector) * -1 as inner_product
from documents
order by embedding <#> '[0.0013, -0.0245, ...]'::vector
limit 10;
-- 既存テーブルにベクトルカラムを追加
alter table documents
add column embedding vector(1536);
-- ベクトルカラムの更新
update documents
set embedding = '[0.0013, -0.0245, ...]'
where id = 1;// supabase-js でのベクトル挿入
const { data, error } = await supabase
.from('documents')
.insert({
title: 'Introduction to AI',
body: 'Artificial intelligence is...',
embedding: JSON.stringify([0.0013, -0.0245, 0.0112]),
});
// supabase-js ではベクトル検索に RPC(関数呼び出し)を使う
const { data, error } = await supabase.rpc('match_documents', {
query_embedding: [0.0013, -0.0245, 0.0112],
match_threshold: 0.78,
match_count: 10,
});注意点
- 次元数は最大 2000 次元まで(pgvector 0.7.0 以降。それ以前は 1536)
vector型はデフォルトでextensionsスキーマに作成される。テーブル定義時にextensions.vectorと明示する必要がある場合がある- ベクトルの次元数が異なるカラム同士で距離演算を行うとエラーになる
- NULL ベクトルは距離演算の結果も NULL になる
- supabase-js からベクトルを挿入する際は配列を JSON 文字列に変換する(
JSON.stringify())か、配列をそのまま渡す - 大量データの INSERT には
copyコマンドやバッチ処理を推奨
関連
- AI & Vectors 概要
- ベクトルインデックス
- セマンティック検索
ベクトルインデックス
HNSW と IVFFlat の 2 種類のベクトルインデックスによる近似最近傍検索(ANN)の高速化。
概要
ベクトル検索はインデックスなしではテーブル全行のフルスキャンとなり、データ量が増えると極めて遅くなる。pgvector は 2 種類の ANN(Approximate Nearest Neighbor)インデックスを提供する。
HNSW(Hierarchical Navigable Small World)
- 推奨: 精度が高く、検索速度も優れている
- 構築に時間がかかる(メモリ使用量も大きい)
- パラメータ:
m(グラフの接続数、デフォルト 16)、ef_construction(構築時の探索幅、デフォルト 64) - 検索時パラメータ:
hnsw.ef_search(デフォルト 40、大きくすると精度向上・速度低下) - インデックス構築にデータが不要(空テーブルでも作成可能)
IVFFlat(Inverted File with Flat Compression)
- 構築が高速で、メモリ使用量が少ない
- 精度は HNSW よりやや低い
- パラメータ:
lists(クラスタ数) - 検索時パラメータ:
ivfflat.probes(探索するクラスタ数、デフォルト 1) - インデックス構築前にデータが必要(データに基づいてクラスタを構成するため)
lists パラメータの目安
| 行数 | lists の推奨値 |
|---|---|
| 〜100,000 | rows / 1000 |
| 100,000〜1,000,000 | sqrt(rows) |
コード例
-- ============================================
-- HNSW インデックス(推奨)
-- ============================================
-- コサイン距離用 HNSW インデックス
create index on documents
using hnsw (embedding vector_cosine_ops)
with (m = 16, ef_construction = 64);
-- L2 距離用 HNSW インデックス
create index on documents
using hnsw (embedding vector_l2_ops)
with (m = 16, ef_construction = 64);
-- 内積用 HNSW インデックス
create index on documents
using hnsw (embedding vector_ip_ops)
with (m = 16, ef_construction = 64);
-- 検索時の ef_search を調整(セッション単位)
set hnsw.ef_search = 100;
-- ============================================
-- IVFFlat インデックス
-- ============================================
-- コサイン距離用 IVFFlat インデックス
create index on documents
using ivfflat (embedding vector_cosine_ops)
with (lists = 100);
-- L2 距離用 IVFFlat インデックス
create index on documents
using ivfflat (embedding vector_l2_ops)
with (lists = 100);
-- 検索時の probes を調整(セッション単位)
set ivfflat.probes = 10;
-- ============================================
-- インデックスの管理
-- ============================================
-- インデックスの再構築(データ分布が大きく変わった場合)
reindex index documents_embedding_idx;
-- インデックスの削除
drop index if exists documents_embedding_idx;
-- インデックスの構築進捗を確認
select phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) as "%"
from pg_stat_progress_create_index;-- RPC 関数内でのインデックスパラメータ設定
create or replace 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
-- 関数内で ef_search を設定
set local hnsw.ef_search = 100;
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;
$$;注意点
- HNSW は空テーブルでもインデックスを作成できるが、IVFFlat はデータがないと意味のあるインデックスを構築できない
- IVFFlat は
probesをlistsと同じ値にすると完全再現(Exact Search)になるが遅い - HNSW の
mを大きくするとインデックスサイズが増大する。通常は 16〜64 の範囲 ef_constructionは構築時のみの設定。大きくすると精度向上するが構築時間が増加- インデックス構築は CPU とメモリを大量に消費するため、本番環境ではメンテナンスウィンドウで実行を推奨
- 大量データ(100 万行以上)では、インデックス構築に
maintenance_work_memの増加が必要になる場合がある - 距離関数ごとに別の ops クラスが必要。コサイン距離のインデックスは L2 距離のクエリには使えない
関連
- ベクトルカラム
- セマンティック検索
- AI & Vectors 概要
匿名認証
匿名ユーザーの作成と、認証済みアカウントへの変換。
概要
匿名認証は、ユーザーがメールアドレスやパスワードを提供せずに一時的なアカウントを作成できる機能である。チェックアウト前のカート保存や、サインアップ前のアプリ体験など、一時的なアクセスを提供する場合に有用。
主な特徴
signInAnonymously()で即座にセッションが作成されるauth.usersテーブルにis_anonymous = trueのユーザーが作成される- 匿名ユーザーは後から
linkIdentity()やupdateUser()で認証済みアカウントに変換可能 - RLS ポリシーで
is_anonymousフラグを使って匿名ユーザーのアクセスを制御可能
ダッシュボード設定
Auth > Providers > Anonymous Sign-Ins で「Enable Anonymous Sign-Ins」を有効にする必要がある。
コード例
// === 匿名サインイン ===
const { data, error } = await supabase.auth.signInAnonymously()
if (data.user) {
console.log('Anonymous user:', data.user.id)
console.log('Is anonymous:', data.user.is_anonymous) // true
}
// 匿名ユーザーでもデータの読み書きが可能(RLS による制御)
const { data: todos, error: todoError } = await supabase
.from('todos')
.insert({ title: 'My todo', user_id: data.user?.id })
// === 匿名ユーザーを認証済みアカウントに変換 ===
// 方法 1: メールとパスワードを設定
const { data, error } = await supabase.auth.updateUser({
email: 'user@example.com',
password: 'securePassword123!',
})
// メール確認リンクが送信される
// 確認後、is_anonymous が false になる
// 方法 2: OAuth プロバイダをリンク
const { data, error } = await supabase.auth.linkIdentity({
provider: 'google',
})
// Google 認証後、is_anonymous が false になる
// 方法 3: 電話番号を設定
const { data, error } = await supabase.auth.updateUser({
phone: '+819012345678',
})
// OTP で確認後、is_anonymous が false になる
// === 匿名ユーザーの状態確認 ===
const { data: { user } } = await supabase.auth.getUser()
if (user?.is_anonymous) {
// サインアップを促すUIを表示
showSignUpPrompt()
} else {
// 認証済みユーザーの通常UI
showDashboard()
}
// === RLS での匿名ユーザー制御 ===
// SQL: 匿名ユーザーは読み取りのみ許可
// CREATE POLICY "Anonymous users can read" ON public.posts
// FOR SELECT
// TO authenticated
// USING (true);
//
// CREATE POLICY "Only non-anonymous users can insert" ON public.posts
// FOR INSERT
// TO authenticated
// WITH CHECK (
// (SELECT is_anonymous FROM auth.users WHERE id = auth.uid()) = false
// );
// SQL: JWT の is_anonymous クレームを使用(より効率的)
// CREATE POLICY "Only non-anonymous can insert" ON public.posts
// FOR INSERT
// TO authenticated
// WITH CHECK (
// (auth.jwt() ->> 'is_anonymous')::boolean = false
// );
// === onAuthStateChange での変換検知 ===
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'USER_UPDATED' && session?.user && !session.user.is_anonymous) {
// 匿名ユーザーが認証済みに変換された
console.log('User converted from anonymous to authenticated')
}
})注意点
- 匿名認証はダッシュボードで明示的に有効化する必要がある
- 匿名ユーザーのデータは、アカウント変換後もそのまま引き継がれる(user_id が同じ)
- 匿名ユーザーの JWT にも
authenticatedロールが付与されるため、RLS ポリシーでis_anonymousを使って明示的に制御すること - 匿名ユーザーがブラウザのストレージをクリアすると、そのアカウントにアクセスできなくなる
- 大量の匿名ユーザーが作成される可能性があるため、定期的なクリーンアップを検討すること
- CAPTCHA を有効にすることで、匿名アカウントの不正な大量作成を防止できる
関連
- Auth 概要
- ユーザー管理
- ID 管理・アカウントリンク
- CAPTCHA
Auth Hooks
サーバーサイドで認証フローをカスタマイズする 6 種類のフック。
概要
Auth Hooks は、認証フローの特定のポイントでカスタムロジックを実行するためのサーバーサイドフックである。PostgreSQL 関数または HTTP エンドポイントとして実装可能。
6 種類の Auth Hooks
| Hook | トリガータイミング | 主な用途 |
|---|---|---|
| Custom Access Token | JWT 発行時 | JWT にカスタムクレームを追加 |
| Send Email | メール送信時 | カスタム SMTP / メールテンプレート |
| Send SMS | SMS 送信時 | カスタム SMS プロバイダ |
| MFA Verification | MFA 検証時 | カスタム MFA 検証ロジック |
| Password Verification | パスワード検証時 | カスタムパスワードハッシュの移行 |
| Before User Created | ユーザー作成前 | ユーザー作成の許可/拒否 |
実装方法
1. PostgreSQL 関数: auth スキーマまたは public スキーマに関数を作成。同一データベース内で完結するため高速 2. HTTP エンドポイント: 外部サービスの API を呼び出す。Edge Function などで実装可能
コード例
// ============================================================
// 1. Custom Access Token Hook
// JWT にカスタムクレーム(ロール等)を追加
// ============================================================
// PostgreSQL 関数として実装:
// CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
// RETURNS jsonb
// LANGUAGE plpgsql
// STABLE
// AS $$
// DECLARE
// claims jsonb;
// user_role text;
// BEGIN
// -- ユーザーのロールを取得
// SELECT role INTO user_role
// FROM public.user_roles
// WHERE user_id = (event->>'user_id')::uuid;
//
// -- claims を取得
// claims := event->'claims';
//
// -- カスタムクレームを追加
// IF user_role IS NOT NULL THEN
// claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
// ELSE
// claims := jsonb_set(claims, '{user_role}', '"user"');
// END IF;
//
// -- 更新した claims を返す
// event := jsonb_set(event, '{claims}', claims);
// RETURN event;
// END;
// $$;
//
// -- Hook に必要な権限を付与
// GRANT USAGE ON SCHEMA public TO supabase_auth_admin;
// GRANT EXECUTE ON FUNCTION public.custom_access_token_hook TO supabase_auth_admin;
// REVOKE EXECUTE ON FUNCTION public.custom_access_token_hook FROM authenticated, anon, public;
// GRANT ALL ON TABLE public.user_roles TO supabase_auth_admin;
// RLS でカスタムクレームを使用:
// CREATE POLICY "Admins only" ON public.admin_data
// FOR ALL TO authenticated
// USING ((auth.jwt() ->> 'user_role') = 'admin');
// ============================================================
// 2. Send Email Hook
// カスタムメール送信(例: Resend, SendGrid)
// ============================================================
// Edge Function として実装:
// Deno.serve(async (req) => {
// const payload = await req.json()
// const { user, email_data } = payload
//
// // email_data.token: OTP トークン
// // email_data.token_hash: トークンハッシュ
// // email_data.redirect_to: リダイレクト先
// // email_data.email_action_type: 'signup' | 'magiclink' | 'recovery' | ...
//
// // カスタムメール送信
// 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: user.email,
// subject: 'Your verification code',
// html: `<p>Your code: ${email_data.token}</p>`,
// }),
// })
//
// return new Response(JSON.stringify({}), {
// headers: { 'Content-Type': 'application/json' },
// })
// })
// ============================================================
// 3. Send SMS Hook
// カスタム SMS 送信
// ============================================================
// PostgreSQL 関数(pg_net を使用):
// CREATE OR REPLACE FUNCTION public.custom_sms_hook(event jsonb)
// RETURNS jsonb
// LANGUAGE plpgsql
// AS $$
// DECLARE
// phone text := event->'user'->>'phone';
// otp text := event->'sms'->>'otp';
// BEGIN
// -- pg_net で外部 SMS API を呼び出し
// PERFORM net.http_post(
// url := 'https://api.sms-provider.com/send',
// headers := '{"Authorization": "Bearer YOUR_API_KEY"}'::jsonb,
// body := jsonb_build_object('to', phone, 'message', 'Your code: ' || otp)
// );
//
// RETURN event;
// END;
// $$;
// ============================================================
// 4. MFA Verification Hook
// カスタム MFA 検証ロジック
// ============================================================
// PostgreSQL 関数:
// CREATE OR REPLACE FUNCTION public.mfa_verification_hook(event jsonb)
// RETURNS jsonb
// LANGUAGE plpgsql
// AS $$
// DECLARE
// attempts int;
// BEGIN
// -- 失敗回数を確認
// SELECT count(*) INTO attempts
// FROM public.mfa_attempts
// WHERE user_id = (event->>'user_id')::uuid
// AND created_at > now() - interval '1 hour';
//
// IF attempts >= 5 THEN
// -- 5回以上失敗したらブロック
// RETURN jsonb_build_object(
// 'decision', 'reject',
// 'message', 'Too many attempts. Please try again later.'
// );
// END IF;
//
// RETURN jsonb_build_object('decision', 'continue');
// END;
// $$;
// ============================================================
// 5. Password Verification Hook
// カスタムパスワード検証(移行用途)
// ============================================================
// PostgreSQL 関数:
// CREATE OR REPLACE FUNCTION public.password_verification_hook(event jsonb)
// RETURNS jsonb
// LANGUAGE plpgsql
// AS $$
// DECLARE
// stored_hash text;
// BEGIN
// -- 旧システムのパスワードハッシュを確認
// SELECT password_hash INTO stored_hash
// FROM public.legacy_users
// WHERE email = (event->>'email');
//
// IF stored_hash IS NOT NULL AND
// public.verify_legacy_hash(event->>'password', stored_hash) THEN
// -- 旧ハッシュで認証成功 → bcrypt に移行
// RETURN jsonb_build_object(
// 'decision', 'continue',
// 'should_update_password', true
// );
// END IF;
//
// RETURN jsonb_build_object('decision', 'continue');
// END;
// $$;
// ============================================================
// 6. Before User Created Hook
// ユーザー作成前の検証
// ============================================================
// PostgreSQL 関数:
// CREATE OR REPLACE FUNCTION public.before_user_created_hook(event jsonb)
// RETURNS jsonb
// LANGUAGE plpgsql
// AS $$
// DECLARE
// email text := event->'user'->>'email';
// domain text;
// BEGIN
// -- メールドメインを取得
// domain := split_part(email, '@', 2);
//
// -- 許可されたドメインのみサインアップを許可
// IF domain NOT IN ('company.com', 'partner.com') THEN
// RETURN jsonb_build_object(
// 'decision', 'reject',
// 'message', 'Only company.com and partner.com emails are allowed.'
// );
// END IF;
//
// RETURN jsonb_build_object('decision', 'continue');
// END;
// $$;注意点
- Auth Hook の PostgreSQL 関数は
supabase_auth_adminロールで実行されるため、適切な権限を付与すること authenticatedやanonロールからの実行権限は REVOKE すること(セキュリティ対策)- Custom Access Token Hook で追加したクレームは JWT のペイロードサイズを増加させる。過度に大きなデータを入れないこと
- HTTP エンドポイント型の Hook はネットワーク遅延の影響を受けるため、パフォーマンスに注意
- Send Email / Send SMS Hook を設定すると、Supabase のデフォルトのメール / SMS 送信は無効化される
- Hook 関数がエラーを返すと、認証フロー全体が失敗する。適切なエラーハンドリングを実装すること
- ダッシュボードの Auth > Hooks で Hook を有効化し、実装を関連付ける
関連
- Auth 概要
- JWT 構造
- メール OTP / Magic Link
- 多要素認証
CAPTCHA 連携
hCaptcha と Cloudflare Turnstile による bot 対策。
概要
Supabase Auth は CAPTCHA プロバイダと連携して、自動化された不正なサインアップやサインインを防止できる。サポートされているプロバイダは以下の 2 つ:
| プロバイダ | 特徴 |
|---|---|
| hCaptcha | プライバシー重視の CAPTCHA サービス |
| Cloudflare Turnstile | ユーザーフレンドリーな非対話型チャレンジ |
ダッシュボード設定
1. Auth > Settings > Security で CAPTCHA プロバイダを選択 2. プロバイダの Site Key と Secret Key を設定 3. 保護するエンドポイントを選択(Sign Up, Sign In 等)
対応エンドポイント
- サインアップ(
signUp) - パスワードサインイン(
signInWithPassword) - パスワードレスサインイン(
signInWithOtp) - パスワードリセット(
resetPasswordForEmail)
コード例
// === hCaptcha の実装 ===
// 1. hCaptcha スクリプトをロード
// <script src="https://js.hcaptcha.com/1/api.js" async defer></script>
// 2. hCaptcha ウィジェットを配置
// <div id="hcaptcha" class="h-captcha" data-sitekey="your-site-key"></div>
// 3. トークンを取得してサインアップ
const captchaToken = (window as any).hcaptcha.getResponse()
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
options: {
captchaToken,
},
})
// hCaptcha をリセット
;(window as any).hcaptcha.reset()
// === Cloudflare Turnstile の実装 ===
// 1. Turnstile スクリプトをロード
// <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
// 2. Turnstile ウィジェットを配置
// <div class="cf-turnstile" data-sitekey="your-site-key"></div>
// 3. トークンを取得してサインイン
const turnstileToken = document.querySelector<HTMLInputElement>(
'[name="cf-turnstile-response"]'
)?.value
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password123',
options: {
captchaToken: turnstileToken,
},
})
// === React での hCaptcha 実装 ===
import HCaptcha from '@hcaptcha/react-hcaptcha'
import { useRef, useState } from 'react'
function SignUpForm() {
const captchaRef = useRef<HCaptcha>(null)
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
async function handleSignUp(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
if (!captchaToken) {
alert('Please complete the CAPTCHA')
return
}
const { data, error } = await supabase.auth.signUp({
email: formData.get('email') as string,
password: formData.get('password') as string,
options: {
captchaToken,
},
})
// CAPTCHA をリセット
captchaRef.current?.resetCaptcha()
setCaptchaToken(null)
if (error) {
console.error('Sign up error:', error.message)
}
}
return (
<form onSubmit={handleSignUp}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<HCaptcha
ref={captchaRef}
sitekey="your-site-key"
onVerify={(token) => setCaptchaToken(token)}
onExpire={() => setCaptchaToken(null)}
/>
<button type="submit">Sign Up</button>
</form>
)
}
// === OTP サインインでの CAPTCHA ===
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
captchaToken: captchaToken,
},
})
// === パスワードリセットでの CAPTCHA ===
const { error } = await supabase.auth.resetPasswordForEmail(
'user@example.com',
{
redirectTo: 'https://example.com/reset-password',
captchaToken: captchaToken,
}
)注意点
- CAPTCHA トークンは一度使用すると無効化される。リクエスト後にウィジェットをリセットすること
- hCaptcha のトークンは 2 分間有効。期限切れの場合は再チャレンジが必要
- Cloudflare Turnstile は非対話型のため、ユーザー体験がより良い(推奨)
- CAPTCHA を有効にしても、Admin API(service_role)からのリクエストには CAPTCHA は不要
- テスト環境では、hCaptcha のテスト用 Site Key(
10000000-ffff-ffff-ffff-000000000001)を使用できる - CAPTCHA はクライアントサイドでのみ実装が必要。サーバーサイド API では Supabase Auth が自動的にトークンを検証する
captchaTokenを渡さずにリクエストすると、CAPTCHA が有効な場合はcaptcha_failedエラーが返される
関連
- パスワード認証
- レート制限
- 匿名認証
- エラーコード
メール OTP / Magic Link
メールベースのパスワードレス認証(OTP コードとマジックリンク)。
概要
メールベースのパスワードレス認証は、パスワードを使わずにメールアドレスだけでログインする方式である。2 つの方式がある:
OTP(ワンタイムパスワード)コード
- メールに 6 桁の数字コードが送信される
- ユーザーがコードを入力して認証
signInWithOtp()+verifyOtp()の 2 ステップ- モバイルアプリやリダイレクトが困難な環境に適している
マジックリンク
- メールに認証リンクが送信される
- ユーザーがリンクをクリックして認証
signInWithOtp()のみで、リンククリック後に自動的にセッションが作成される- Web アプリに適している
メールテンプレート
ダッシュボードの Auth > Email Templates でカスタマイズ可能:
- Confirm signup: サインアップ確認メール
- Magic Link: マジックリンクメール
- Change Email Address: メールアドレス変更確認
- Reset Password: パスワードリセットメール
テンプレートで使用可能な変数:
{{ .ConfirmationURL }}: 確認 URL{{ .Token }}: OTP トークン{{ .TokenHash }}: トークンハッシュ{{ .SiteURL }}: サイト URL{{ .RedirectTo }}: リダイレクト先 URL
コード例
// === OTP コード方式 ===
// 1. OTP コードをメールで送信
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
shouldCreateUser: true, // 未登録ユーザーも自動作成(デフォルト: true)
},
})
// 2. ユーザーが入力した OTP コードで検証
const { data, error } = await supabase.auth.verifyOtp({
email: 'user@example.com',
token: '123456',
type: 'email',
})
if (data.session) {
console.log('Logged in:', data.user)
}
// === マジックリンク方式 ===
// マジックリンクをメールで送信
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'https://example.com/dashboard',
shouldCreateUser: true,
},
})
// マジックリンクをクリック後、リダイレクト先で:
// PKCE フローの場合
const code = new URL(window.location.href).searchParams.get('code')
if (code) {
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
}
// === サインアップ時の OTP 確認 ===
// サインアップ時に OTP で確認
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
})
// 送信された OTP コードで確認
const { data: verifyData, error: verifyError } = await supabase.auth.verifyOtp({
email: 'user@example.com',
token: '123456',
type: 'signup',
})
// === OTP の type パラメータ ===
// 'email' - メール OTP でのサインイン
// 'signup' - サインアップ確認
// 'recovery' - パスワードリカバリー
// 'invite' - 招待メール
// 'email_change' - メールアドレス変更確認
// === メール送信の無効化(テスト用) ===
// Admin API で OTP を直接生成(メール送信なし)
const { data, error } = await supabaseAdmin.auth.admin.generateLink({
type: 'magiclink',
email: 'user@example.com',
})
// data.properties.hashed_token が OTP トークンハッシュ注意点
- OTP コードのデフォルト有効期限は 5 分間
- マジックリンクはメール内のリンクが一度しか使えない
shouldCreateUser: falseに設定すると、未登録のメールアドレスでは OTP が送信されない- メール送信にはレート制限がある(デフォルト: 1 通/60 秒)
- PKCE フロー使用時は、マジックリンクのリダイレクト先で
exchangeCodeForSession()を呼ぶ必要がある - カスタムメールテンプレートで
{{ .Token }}を使用すると OTP コード方式、{{ .ConfirmationURL }}を使用するとマジックリンク方式になる - Auth Hook の Send Email Hook を使うと、メール送信をカスタムの SMTP サービスに委譲できる
関連
- パスワード認証
- 電話番号認証
- Auth Hooks
- レート制限
Auth エラーコード一覧
Supabase Auth が返すエラーコードの一覧と対処法。
概要
Supabase Auth のエラーは、HTTP ステータスコードと Auth 固有のエラーコードで構成される。supabase-js ではエラーオブジェクトの error.message、error.status、error.code でアクセスできる。
エラーオブジェクトの構造
{
message: string, // 人間が読めるエラーメッセージ
status: number, // HTTP ステータスコード
code: string, // Auth エラーコード(例: 'invalid_credentials')
}エラーコード一覧
400 Bad Request
| エラーコード | 説明 | 対処法 |
|---|---|---|
invalid_credentials | メールアドレスまたはパスワードが正しくない | 入力内容を確認。ユーザーが存在するか確認 |
user_already_exists | メールアドレスが既に登録済み | ログインを促すか、パスワードリセットを案内 |
weak_password | パスワードが要件を満たさない | パスワード要件(最小長、文字種等)を満たすよう案内 |
email_not_confirmed | メールアドレスが未確認 | 確認メールの再送信を案内 |
phone_not_confirmed | 電話番号が未確認 | SMS OTP の再送信を案内 |
same_password | 新旧パスワードが同一 | 異なるパスワードを設定するよう案内 |
signup_disabled | サインアップが無効化されている | ダッシュボードでサインアップを有効化 |
user_banned | ユーザーが BAN されている | Admin API で BAN を解除 |
captcha_failed | CAPTCHA 検証に失敗 | CAPTCHA トークンを再取得して再試行 |
flow_state_not_found | PKCE フロー状態が見つからない | 認証フローを最初からやり直す |
flow_state_expired | PKCE フロー状態が期限切れ | 認証フローを最初からやり直す |
otp_expired | OTP コードが期限切れ | 新しい OTP を送信 |
otp_disabled | OTP が無効化されている | ダッシュボードで OTP を有効化 |
email_provider_disabled | メールプロバイダが無効 | ダッシュボードでメール認証を有効化 |
phone_provider_disabled | 電話プロバイダが無効 | ダッシュボードで電話認証を有効化 |
provider_disabled | 指定のプロバイダが無効 | ダッシュボードで該当プロバイダを有効化 |
validation_failed | 入力バリデーションエラー | リクエストパラメータを確認 |
bad_json | JSON パースエラー | リクエストボディの JSON 形式を確認 |
bad_jwt | JWT の形式が不正 | 正しい JWT を使用 |
email_exists | メールアドレスが既に使用されている | 別のメールアドレスを使用 |
phone_exists | 電話番号が既に使用されている | 別の電話番号を使用 |
identity_already_exists | ID が既にリンクされている | 既存のリンクを解除してから再リンク |
identity_not_found | 指定の ID が見つからない | 正しい identity ID を指定 |
no_authorization | 認証ヘッダーがない | Authorization ヘッダーを付与 |
401 Unauthorized
| エラーコード | 説明 | 対処法 |
|---|---|---|
invalid_token | トークンが無効または期限切れ | 再ログインまたはトークンリフレッシュ |
session_not_found | セッションが見つからない | 再ログイン |
user_not_found | ユーザーが見つからない | ユーザーが削除されていないか確認 |
invalid_grant | リフレッシュトークンが無効 | 再ログイン |
403 Forbidden
| エラーコード | 説明 | 対処法 |
|---|---|---|
insufficient_aal | AAL レベルが不足(MFA 未完了) | MFA 検証を完了させる |
not_admin | Admin 権限が必要 | service_role キーを使用 |
422 Unprocessable Entity
| エラーコード | 説明 | 対処法 |
|---|---|---|
validation_failed | 入力値のバリデーションエラー | エラーメッセージの詳細を確認して修正 |
email_address_invalid | メールアドレスの形式が不正 | 正しいメール形式を使用 |
phone_number_invalid | 電話番号の形式が不正 | E.164 形式(+819012345678)を使用 |
429 Too Many Requests
| エラーコード | 説明 | 対処法 |
|---|---|---|
rate_limit_exceeded | レート制限超過 | 待機後に再試行。CAPTCHA の導入を検討 |
email_send_rate_limit | メール送信レート制限超過 | 60 秒以上待機してから再試行 |
sms_send_rate_limit | SMS 送信レート制限超過 | 60 秒以上待機してから再試行 |
over_request_rate_limit | 全体的なリクエスト制限超過 | 待機後に再試行 |
500 Internal Server Error
| エラーコード | 説明 | 対処法 |
|---|---|---|
unexpected_failure | 予期しないサーバーエラー | Supabase のステータスページを確認。再試行 |
コード例
// === エラーハンドリングの実装 ===
async function handleSignIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
switch (error.code) {
case 'invalid_credentials':
return { error: 'メールアドレスまたはパスワードが正しくありません。' }
case 'email_not_confirmed':
return { error: 'メールアドレスが確認されていません。確認メールを確認してください。' }
case 'user_banned':
return { error: 'このアカウントは停止されています。' }
case 'captcha_failed':
return { error: 'CAPTCHA の検証に失敗しました。再試行してください。' }
case 'rate_limit_exceeded':
case 'over_request_rate_limit':
return { error: 'リクエストが制限に達しました。しばらくしてから再試行してください。' }
default:
return { error: `ログインに失敗しました: ${error.message}` }
}
}
return { data }
}
// === サインアップのエラーハンドリング ===
async function handleSignUp(email: string, password: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
})
if (error) {
switch (error.code) {
case 'user_already_exists':
return { error: 'このメールアドレスは既に登録されています。' }
case 'weak_password':
return { error: 'パスワードが弱すぎます。8 文字以上で、大文字・小文字・数字を含めてください。' }
case 'signup_disabled':
return { error: '現在、新規登録は受け付けていません。' }
case 'email_provider_disabled':
return { error: 'メールでの登録は無効になっています。' }
case 'validation_failed':
return { error: '入力内容に問題があります。確認してください。' }
case 'email_send_rate_limit':
return { error: 'メール送信の制限に達しました。しばらくしてから再試行してください。' }
default:
return { error: `登録に失敗しました: ${error.message}` }
}
}
// identities が空の場合、既存ユーザー
if (data.user?.identities?.length === 0) {
return { error: 'このメールアドレスは既に登録されています。' }
}
return { data }
}
// === 共通エラーハンドラー ===
function getErrorMessage(error: { code?: string; message: string; status?: number }): string {
const errorMessages: Record<string, string> = {
invalid_credentials: 'ログイン情報が正しくありません。',
user_already_exists: 'このアカウントは既に存在します。',
weak_password: 'より強力なパスワードを設定してください。',
email_not_confirmed: 'メールアドレスを確認してください。',
rate_limit_exceeded: 'リクエスト制限に達しました。しばらくお待ちください。',
session_not_found: 'セッションが期限切れです。再ログインしてください。',
invalid_token: 'トークンが無効です。再ログインしてください。',
otp_expired: 'コードの有効期限が切れました。再送信してください。',
captcha_failed: 'CAPTCHA を完了してください。',
}
return errorMessages[error.code ?? ''] ?? error.message
}注意点
- エラーコードはバージョンによって追加・変更される可能性がある。
error.codeが undefined の場合のフォールバック処理を実装すること signUpで既存メールアドレスを使用した場合、セキュリティ上エラーではなくidentitiesが空のユーザーを返す(ユーザー列挙攻撃の防止)- レート制限エラー(429)はクライアントサイドでリトライロジックを実装すること。ただし、指数バックオフを使用すること
error.messageはユーザー向けの表示に使用できるが、英語で返されるため、上記のような日本語マッピングを用意すると良い- Admin API のエラーはクライアント API と異なるコードを返す場合がある
- 本番環境ではエラーの詳細をログに記録し、ユーザーには一般的なメッセージのみ表示すること
関連
- Auth 概要
- パスワード認証
- レート制限
- CAPTCHA
ID 管理・アカウントリンク
auth.identities テーブルを使った複数プロバイダのアカウントリンク機能。
概要
Supabase Auth では、1 人のユーザーが複数の認証プロバイダ(メール、Google、GitHub など)を紐付けることができる。各プロバイダの認証情報は auth.identities テーブルに格納され、user_id で auth.users テーブルと関連付けられる。
auth.identities テーブルの主要カラム
| カラム | 型 | 説明 |
|---|---|---|
id | uuid | ID の一意識別子 |
user_id | uuid | 関連する auth.users の ID |
provider | text | プロバイダ名(email, google, github 等) |
provider_id | text | プロバイダ側のユーザー ID |
identity_data | jsonb | プロバイダから取得したユーザー情報 |
created_at | timestamptz | 作成日時 |
updated_at | timestamptz | 更新日時 |
アカウントリンクの方式
Automatic Linking(自動リンク):
- 同じメールアドレスを持つアカウントが自動的にリンクされる
- ダッシュボードの Auth 設定で有効化
- 信頼できるプロバイダ(メール確認済み)のみが対象
- セキュリティリスクがあるため、メール確認を必須にすること
Manual Linking(手動リンク):
- ユーザーが明示的に
linkIdentity()を呼び出してリンクする - より安全だが、ユーザー操作が必要
- 推奨される方式
コード例
// === アカウントリンク ===
// 現在ログイン中のユーザーに OAuth プロバイダをリンク
const { data, error } = await supabase.auth.linkIdentity({
provider: 'google',
options: {
redirectTo: 'https://example.com/account/linked',
},
})
// GitHub をリンク
const { data, error } = await supabase.auth.linkIdentity({
provider: 'github',
})
// === アカウントリンク解除 ===
// 現在のユーザーの identities を取得
const { data: { user } } = await supabase.auth.getUser()
const identities = user?.identities
// 特定の identity をリンク解除
const googleIdentity = identities?.find((i) => i.provider === 'google')
if (googleIdentity) {
const { error } = await supabase.auth.unlinkIdentity(googleIdentity)
}
// === ユーザーの identities 確認 ===
const { data: { user } } = await supabase.auth.getUser()
if (user?.identities) {
user.identities.forEach((identity) => {
console.log(`Provider: ${identity.provider}`)
console.log(`Provider ID: ${identity.provider_id}`)
console.log(`Identity Data:`, identity.identity_data)
})
}
// === Admin API で identities を確認 ===
const { data: { user }, error } = await supabaseAdmin.auth.admin.getUserById(
'user-uuid-here'
)
console.log('Identities:', user?.identities)注意点
- Automatic Linking を有効にする場合、メール確認を必須に設定すること。確認なしだと、攻撃者が他人のメールアドレスでアカウントを作成し、そのアカウントにリンクされる危険がある
unlinkIdentity()でリンクを解除する際、最後の identity を削除することはできない(ログイン手段がなくなるため)- OAuth プロバイダをリンクする場合、ブラウザリダイレクトが発生する
identity_dataにはプロバイダから返される情報(名前、アバター等)が含まれる- 同一プロバイダで複数の identity をリンクすることはできない
関連
- ユーザー管理
- ソーシャルログイン
- 匿名認証
JWT 構造
Supabase Auth が発行する JWT のクレーム構造と RLS での活用。
概要
Supabase Auth は JWT(JSON Web Token)を使ってユーザーのセッション情報を管理する。JWT はアクセストークンとして使用され、Supabase のサービス(Database, Storage, Edge Functions 等)へのリクエスト時に Authorization: Bearer <token> ヘッダーで送信される。
JWT ペイロードの標準クレーム
| クレーム | 型 | 説明 |
|---|---|---|
sub | string | ユーザー ID(auth.users.id) |
aud | string | オーディエンス(authenticated) |
role | string | PostgreSQL ロール(anon / authenticated) |
email | string | ユーザーのメールアドレス |
phone | string | ユーザーの電話番号 |
app_metadata | object | アプリケーションメタデータ |
user_metadata | object | ユーザーメタデータ |
aal | string | AAL レベル(aal1 / aal2) |
amr | array | 認証メソッド参照 |
session_id | string | セッション ID |
is_anonymous | boolean | 匿名ユーザーフラグ |
iat | number | 発行日時(UNIX タイムスタンプ) |
exp | number | 有効期限(UNIX タイムスタンプ) |
iss | string | 発行者 URL |
JWT の有効期限
- デフォルト: 3600 秒(1 時間)
- ダッシュボードの Auth > Settings > JWT Expiry で変更可能
- 短すぎるとリフレッシュが頻繁になり、長すぎるとセキュリティリスクが増す
署名鍵
Supabase は 共有秘密鍵(HS256) と 非対称鍵(RSA / EC / OKP) の両方をサポートする。
| 方式 | アルゴリズム例 | JWKS エンドポイント | 推奨度 |
|---|---|---|---|
| 共有秘密鍵 | HS256 | 公開されない | 非推奨(ローカル検証はセキュリティリスク) |
| 非対称鍵 | RS256, ES256, Ed25519 | /.well-known/jwks.json で公開鍵を公開 | 推奨 |
- HS256 の場合、JWT Secret はダッシュボードの Settings > API > JWT Secret で確認可能
- 非対称鍵を使用する場合、JWKS エンドポイント(
https://<project>.supabase.co/auth/v1/.well-known/jwks.json)で公開鍵を取得して検証する - 非対称鍵を使用しない場合、JWKS エンドポイントはキーを返さない
- HS256 のローカル検証は推奨されない。代わりに Auth サーバーに問い合わせる(
GET /auth/v1/user)
コード例
// === JWT ペイロードの確認 ===
const { data: { session } } = await supabase.auth.getSession()
if (session) {
// JWT をデコード(検証はしない)
const payload = JSON.parse(
atob(session.access_token.split('.')[1])
)
console.log('User ID (sub):', payload.sub)
console.log('Role:', payload.role)
console.log('Email:', payload.email)
console.log('AAL:', payload.aal)
console.log('Session ID:', payload.session_id)
console.log('Is Anonymous:', payload.is_anonymous)
console.log('Expires at:', new Date(payload.exp * 1000))
}
// === RLS での auth.jwt() 使用 ===
// SQL: JWT のクレームを RLS ポリシーで参照
//
// -- ユーザー ID(sub)でフィルタ
// CREATE POLICY "Users can access own data" ON public.profiles
// FOR ALL TO authenticated
// USING (id = auth.uid());
// -- auth.uid() は (auth.jwt() ->> 'sub')::uuid のショートカット
//
// -- カスタムクレームでフィルタ
// CREATE POLICY "Admins only" ON public.admin_data
// FOR ALL TO authenticated
// USING ((auth.jwt() ->> 'user_role') = 'admin');
//
// -- AAL レベルでフィルタ
// CREATE POLICY "MFA required" ON public.sensitive_data
// FOR ALL TO authenticated
// USING ((auth.jwt() ->> 'aal') = 'aal2');
//
// -- 匿名ユーザーを除外
// CREATE POLICY "No anonymous" ON public.user_content
// FOR INSERT TO authenticated
// WITH CHECK ((auth.jwt() ->> 'is_anonymous')::boolean IS FALSE);
//
// -- app_metadata のプロバイダを確認
// CREATE POLICY "Google users" ON public.google_data
// FOR ALL TO authenticated
// USING (auth.jwt() -> 'app_metadata' ->> 'provider' = 'google');
// === Custom Access Token Hook でカスタムクレーム追加 ===
// PostgreSQL 関数:
// CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
// RETURNS jsonb
// LANGUAGE plpgsql STABLE
// AS $$
// DECLARE
// claims jsonb;
// user_role text;
// user_plan text;
// BEGIN
// claims := event->'claims';
//
// -- カスタムクレームを追加
// SELECT role, plan INTO user_role, user_plan
// FROM public.user_profiles
// WHERE user_id = (event->>'user_id')::uuid;
//
// claims := jsonb_set(claims, '{user_role}', to_jsonb(COALESCE(user_role, 'user')));
// claims := jsonb_set(claims, '{user_plan}', to_jsonb(COALESCE(user_plan, 'free')));
//
// event := jsonb_set(event, '{claims}', claims);
// RETURN event;
// END;
// $$;
// TypeScript でカスタムクレームにアクセス
const { data: { session } } = await supabase.auth.getSession()
const jwt = JSON.parse(atob(session!.access_token.split('.')[1]))
console.log('User role:', jwt.user_role)
console.log('User plan:', jwt.user_plan)
// === auth.uid() と auth.role() ===
// SQL: RLS で使用する便利関数
// auth.uid() → (auth.jwt() ->> 'sub')::uuid
// auth.role() → auth.jwt() ->> 'role'
// auth.email() → auth.jwt() ->> 'email'
// RLS ポリシーでの典型的な使用例
// CREATE POLICY "Own data" ON public.todos
// FOR ALL TO authenticated
// USING (user_id = auth.uid())
// WITH CHECK (user_id = auth.uid());
// === サーバーサイドでの JWT 検証 ===
import jwt from 'jsonwebtoken'
// JWT を手動で検証(非対称鍵の場合 — jose ライブラリ推奨)
import { createRemoteJWKSet, jwtVerify } from 'jose'
const JWKS = createRemoteJWKSet(
new URL('https://<project>.supabase.co/auth/v1/.well-known/jwks.json')
)
async function verifySupabaseJWT(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://<project>.supabase.co/auth/v1',
audience: 'authenticated',
})
return payload
}
// 共有秘密鍵(HS256)の場合 — ローカル検証は非推奨
// 代替: Auth サーバーに問い合わせて検証
async function verifyViaAuthServer(token: string) {
const res = await fetch('https://<project>.supabase.co/auth/v1/user', {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error('Invalid token')
return res.json()
}注意点
- JWT はデコード可能であるため、機密情報をカスタムクレームに含めないこと
- Custom Access Token Hook でクレームを追加する場合、JWT ペイロードのサイズが増加する。HTTP ヘッダーの制限(通常 8KB)に注意
auth.jwt()は PostgreSQL 関数であり、RLS ポリシー内でのみ使用可能- JWT の署名鍵(JWT Secret)は厳密に管理し、クライアントに露出させないこと
expクレームの有効期限を過ぎた JWT はすべてのサービスで拒否される- カスタムクレームは Custom Access Token Hook でのみ追加可能。
updateUser()では追加できない amr(Authentication Methods Reference)には認証に使用された方法(password,otp,oauth等)が配列で格納される- 非対称鍵への移行は推奨だが、移行後は HS256 を前提としたローカル検証コードをすべて jose 等のライブラリへ置き換えること
- Supabase は外部 JWT(サードパーティ Auth / 自己発行)も
accessTokenオプション経由で受け付ける
関連
- Auth 概要
- セッション管理
- Auth Hooks
- 多要素認証
多要素認証(MFA)
TOTP と Phone(SMS)を使った多要素認証の実装。
概要
Supabase Auth の MFA(Multi-Factor Authentication)は、パスワード認証に加えて追加の認証要素を要求することで、セキュリティを強化する機能である。
サポートされる MFA タイプ
| タイプ | 説明 | 認証アプリ |
|---|---|---|
| TOTP | 時間ベースのワンタイムパスワード | Google Authenticator, Authy 等 |
| Phone | SMS で送信される OTP コード | SMS 対応端末 |
AAL(Authenticator Assurance Level)
| レベル | 説明 |
|---|---|
aal1 | 1 要素認証(パスワードのみ) |
aal2 | 2 要素認証(パスワード + MFA) |
MFA フロー
1. Enroll(登録): ユーザーが MFA 要素を登録 2. Challenge(チャレンジ): 認証サーバーがチャレンジを作成 3. Verify(検証): ユーザーが OTP コードで応答 4. Unenroll(解除): MFA 要素を削除
コード例
// === TOTP の登録 ===
// 1. TOTP 要素を登録(QR コード生成)
const { data, error } = await supabase.auth.mfa.enroll({
factorType: 'totp',
friendlyName: 'My Authenticator App',
})
if (data) {
// QR コードを表示(ユーザーが認証アプリでスキャン)
const qrCode = data.totp.qr_code // SVG 形式の QR コード
const secret = data.totp.secret // 手動入力用のシークレット
const factorId = data.id
// QR コードを画面に表示
document.getElementById('qr')!.innerHTML = qrCode
}
// 2. 登録を確認(認証アプリのコードで検証)
const { data: challengeData, error: challengeError } =
await supabase.auth.mfa.challenge({
factorId: factorId,
})
const { data: verifyData, error: verifyError } =
await supabase.auth.mfa.verify({
factorId: factorId,
challengeId: challengeData.id,
code: '123456', // 認証アプリのコード
})
// === challengeAndVerify(便利メソッド) ===
// challenge + verify を 1 ステップで実行
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
factorId: factorId,
code: '123456',
})
// === Phone MFA の登録 ===
// 1. Phone 要素を登録
const { data, error } = await supabase.auth.mfa.enroll({
factorType: 'phone',
phone: '+819012345678',
friendlyName: 'My Phone',
})
// 2. SMS で送信されたコードで検証
const { data: verifyData, error: verifyError } =
await supabase.auth.mfa.challengeAndVerify({
factorId: data.id,
code: '123456',
})
// === MFA 要素の一覧取得 ===
const { data: factors, error } = await supabase.auth.mfa.listFactors()
// verified な要素のみ
const verifiedFactors = factors?.totp.filter((f) => f.status === 'verified')
const phoneFactors = factors?.phone.filter((f) => f.status === 'verified')
// === ログイン時の MFA フロー ===
// 1. 通常のパスワードログイン(aal1)
const { data: signInData, error: signInError } =
await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'password123',
})
// 2. AAL レベルを確認
const { data: { currentLevel, nextLevel } } =
await supabase.auth.mfa.getAuthenticatorAssuranceLevel()
if (currentLevel === 'aal1' && nextLevel === 'aal2') {
// MFA が必要 → MFA 検証画面を表示
showMFAVerificationForm()
}
// 3. MFA コードで aal2 に昇格
const { data: factors } = await supabase.auth.mfa.listFactors()
const totpFactor = factors?.totp[0]
if (totpFactor) {
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
factorId: totpFactor.id,
code: '123456', // ユーザー入力
})
}
// === MFA 要素の解除 ===
const { error } = await supabase.auth.mfa.unenroll({
factorId: 'factor-uuid',
})
// === RLS での AAL チェック ===
// SQL: aal2 のユーザーのみアクセスを許可
// CREATE POLICY "Require MFA" ON public.sensitive_data
// FOR ALL
// TO authenticated
// USING (
// (auth.jwt() ->> 'aal') = 'aal2'
// );
// SQL: auth.jwt() の session_id を使って AAL を確認
// CREATE POLICY "Require MFA" ON public.sensitive_data
// FOR ALL
// TO authenticated
// USING (
// EXISTS (
// SELECT 1 FROM auth.sessions
// WHERE id = (auth.jwt() ->> 'session_id')::uuid
// AND aal = 'aal2'
// )
// );注意点
- MFA の enroll 後、verify を完了しないと要素は
unverified状態のままになる challengeAndVerify()はchallenge()+verify()のショートカットで、通常はこちらを使用する- AAL レベルは JWT のクレームに含まれるため、RLS ポリシーで参照可能
- MFA が有効なユーザーがログインすると、最初は
aal1でセッションが作成される。MFA 検証後にaal2に昇格する - TOTP のシークレットは enroll 時にのみ表示される。紛失した場合は unenroll して再登録が必要
- Phone MFA は SMS プロバイダの設定が必要
listFactors()は現在のユーザーの MFA 要素のみ返す
関連
- Auth 概要
- パスワード認証
- JWT 構造
- Auth Hooks
Supabase OAuth Server
Supabase プロジェクトを OAuth 2.0 プロバイダとして機能させる。
概要
Supabase は OAuth 2.0 サーバーとしても機能し、外部アプリケーションが Supabase プロジェクトのユーザーを認証できる。これにより、自分の Supabase プロジェクトを「Login with MyApp」のような OAuth プロバイダとして公開できる。MCP(Model Context Protocol)の認証にも利用可能。
主な機能
- OAuth 2.0 Authorization Code フロー: セキュアな認証フローの提供
- クライアント管理: OAuth クライアントの作成・管理
- スコープ制御: アクセス可能な範囲の制御
- PKCE サポート: パブリッククライアント向けのセキュアなフロー
OAuth 2.0 エンドポイント
| エンドポイント | URL |
|---|---|
| Authorization | https://<project-ref>.supabase.co/auth/v1/authorize |
| Token | https://<project-ref>.supabase.co/auth/v1/token |
| UserInfo | https://<project-ref>.supabase.co/auth/v1/user |
コード例
// === OAuth クライアントの管理(Admin API) ===
// OAuth クライアントを作成
// POST /auth/v1/admin/oauth/clients
// Headers: { Authorization: 'Bearer <service_role_key>' }
// Body: {
// "name": "My External App",
// "redirect_uris": ["https://external-app.com/callback"],
// "scopes": ["openid", "email", "profile"]
// }
// クライアント一覧を取得
// GET /auth/v1/admin/oauth/clients
// クライアントを削除
// DELETE /auth/v1/admin/oauth/clients/<client_id>
// === 外部アプリケーションからの OAuth 認証フロー ===
// 1. Authorization URL を構築
const authUrl = new URL(
'https://your-project.supabase.co/auth/v1/authorize'
)
authUrl.searchParams.set('client_id', 'your-oauth-client-id')
authUrl.searchParams.set('redirect_uri', 'https://external-app.com/callback')
authUrl.searchParams.set('response_type', 'code')
authUrl.searchParams.set('scope', 'openid email profile')
authUrl.searchParams.set('state', generateRandomState())
// PKCE の場合
const codeVerifier = generateCodeVerifier()
const codeChallenge = await generateCodeChallenge(codeVerifier)
authUrl.searchParams.set('code_challenge', codeChallenge)
authUrl.searchParams.set('code_challenge_method', 'S256')
// ユーザーを認証ページにリダイレクト
window.location.href = authUrl.toString()
// 2. コールバックでコードをトークンに交換
async function handleCallback(code: string) {
const response = await fetch(
'https://your-project.supabase.co/auth/v1/token',
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: 'https://external-app.com/callback',
client_id: 'your-oauth-client-id',
client_secret: 'your-oauth-client-secret',
// PKCE の場合
code_verifier: codeVerifier,
}),
}
)
const tokens = await response.json()
// tokens.access_token, tokens.refresh_token
return tokens
}
// 3. ユーザー情報を取得
async function getUserInfo(accessToken: string) {
const response = await fetch(
'https://your-project.supabase.co/auth/v1/user',
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
)
const user = await response.json()
return user
}
// === MCP 認証での利用 ===
// MCP サーバーの認証に Supabase OAuth を使用
// MCP クライアントが Supabase の OAuth エンドポイントに対して
// Authorization Code フローを実行し、
// 取得したアクセストークンで MCP サーバーにアクセス
// PKCE ヘルパー関数
function generateCodeVerifier(): string {
const array = new Uint8Array(32)
crypto.getRandomValues(array)
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
}
async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder()
const data = encoder.encode(verifier)
const digest = await crypto.subtle.digest('SHA-256', data)
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
}
function generateRandomState(): string {
const array = new Uint8Array(16)
crypto.getRandomValues(array)
return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('')
}注意点
- OAuth クライアントの管理には
service_roleキーが必要 - クライアントシークレットは安全に保管し、サーバーサイドでのみ使用すること
- PKCE フローはパブリッククライアント(SPA、モバイルアプリ)で推奨される
redirect_uriは OAuth クライアント作成時に登録した URL と完全に一致する必要があるstateパラメータを使用して CSRF 攻撃を防止すること- トークンの有効期限は Supabase Auth の JWT 設定に従う
- この機能はまだ比較的新しく、API が変更される可能性がある
関連
- Auth 概要
- JWT 構造
- サードパーティ Auth
- セッション管理
Auth アーキテクチャ概要
Supabase Auth は GoTrue ベースの認証サーバーで、JWT によるセッション管理を提供する。
概要
Supabase Auth は、オープンソースの GoTrue サーバーをベースとした認証・認可システムである。すべてのユーザー情報は PostgreSQL の auth.users テーブルに格納され、RLS(Row Level Security)と連携してデータアクセスを制御する。
主要コンポーネント
- GoTrue サーバー: 認証 API を提供するサーバー。JWT の発行・検証を担当
- auth.users テーブル: ユーザー情報を格納する PostgreSQL テーブル
- JWT: アクセストークンとリフレッシュトークンによるセッション管理
- RLS:
auth.jwt()関数を通じて、行レベルのアクセス制御を実現
認証フロー
クライアントサイドフロー:
- ブラウザやモバイルアプリから直接 Supabase Auth API を呼び出す
supabase-jsクライアントライブラリを使用- JWT はクライアント側で保持される
サーバーサイドフロー(SSR):
- Next.js / SvelteKit 等のサーバーフレームワークから認証を行う
@supabase/ssrパッケージを使用- Cookie ベースのセッション管理
getUser()でサーバー側でユーザーを検証(getSession()は信頼しない)
ロール
| ロール | 説明 | 用途 |
|---|---|---|
anon | 未認証ユーザー | 公開データへのアクセス |
authenticated | 認証済みユーザー | ユーザー固有のデータアクセス |
service_role | サービスロール | RLS をバイパスする管理操作(サーバーサイドのみ) |
認証方式
- パスワード認証(メール / 電話番号)
- パスワードレス認証(OTP / Magic Link)
- ソーシャルログイン(OAuth 2.0 / OIDC)
- SSO / SAML
- 匿名認証
- 多要素認証(MFA)
コード例
import { createClient } from '@supabase/supabase-js'
// クライアント作成(anon key を使用)
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
)
// 認証状態の変更を監視
supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth event:', event)
console.log('Session:', session)
})
// 現在のセッションを取得(クライアントサイドのみ)
const { data: { session } } = await supabase.auth.getSession()
// 現在のユーザーを取得(サーバーサイドで推奨)
const { data: { user } } = await supabase.auth.getUser()
// サービスロールクライアント(サーバーサイドのみ、RLS バイパス)
const supabaseAdmin = createClient(
'https://your-project.supabase.co',
'your-service-role-key',
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
}
)注意点
service_roleキーはサーバーサイドでのみ使用し、クライアントに露出させないことgetSession()はローカルストレージから取得するため、サーバーサイドではgetUser()を使用してトークンを検証することanonキーはクライアントに公開されるが、RLS によりアクセスが制御される- JWT の有効期限はデフォルトで 3600 秒(1 時間)
関連
- ユーザー管理
- セッション管理
- JWT 構造
- サーバーサイド認証
パスワード認証
メールアドレスまたは電話番号とパスワードによるサインアップ・サインイン。
概要
パスワード認証は、Supabase Auth の最も基本的な認証方式である。ユーザーはメールアドレス(または電話番号)とパスワードを使用してアカウントを作成し、ログインする。
パスワード要件
ダッシュボードの Auth Settings で以下を設定可能:
- 最小パスワード長: デフォルト 6 文字
- 文字種の要件: 大文字、小文字、数字、特殊文字の必須化
- Leaked Password Protection: Have I Been Pwned データベースと照合し、漏洩済みパスワードを拒否
メール確認
- 確認メール有効: サインアップ後に確認メールが送信される。確認前はログイン不可(設定による)
- 確認メール無効: サインアップ即座にログイン可能(開発環境向け)
- Confirm email (Secure): 確認前のログインを完全にブロック
コード例
// === サインアップ ===
// メールとパスワードでサインアップ
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'securePassword123!',
options: {
data: {
display_name: 'John Doe',
age: 30,
},
emailRedirectTo: 'https://example.com/welcome',
},
})
if (error) {
console.error('Sign up error:', error.message)
} else if (data.user?.identities?.length === 0) {
// メールアドレスが既に登録済みの場合
console.log('User already exists')
} else {
console.log('User created:', data.user)
}
// 電話番号とパスワードでサインアップ
const { data, error } = await supabase.auth.signUp({
phone: '+81901234567',
password: 'securePassword123!',
})
// === サインイン ===
// メールとパスワードでサインイン
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'securePassword123!',
})
if (error) {
console.error('Sign in error:', error.message)
// error.message: 'Invalid login credentials'
} else {
console.log('Logged in:', data.user)
console.log('Session:', data.session)
}
// 電話番号とパスワードでサインイン
const { data, error } = await supabase.auth.signInWithPassword({
phone: '+81901234567',
password: 'securePassword123!',
})
// === パスワードリセット ===
// 1. パスワードリセットメールを送信
const { error } = await supabase.auth.resetPasswordForEmail(
'user@example.com',
{
redirectTo: 'https://example.com/reset-password',
}
)
// 2. リダイレクト先でパスワードを更新
// onAuthStateChange で PASSWORD_RECOVERY イベントを受け取る
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'PASSWORD_RECOVERY') {
// パスワードリセットフォームを表示
showResetForm()
}
})
// 3. 新しいパスワードを設定
const { data, error } = await supabase.auth.updateUser({
password: 'newSecurePassword456!',
})
// === パスワード変更(ログイン済みユーザー) ===
const { data, error } = await supabase.auth.updateUser({
password: 'newPassword789!',
})
// === Admin API でユーザー作成(メール確認不要) ===
const { data, error } = await supabaseAdmin.auth.admin.createUser({
email: 'admin-created@example.com',
password: 'initialPassword123!',
email_confirm: true, // メール確認をスキップ
})注意点
signUpで既存のメールアドレスを使用した場合、セキュリティ上の理由からエラーは返さず、identitiesが空配列のユーザーオブジェクトを返す- パスワードリセットメールのリンクは一度しか使えない
resetPasswordForEmailのredirectToは、ダッシュボードの Redirect URLs に登録されている必要がある- Leaked Password Protection を有効にすると、漏洩済みパスワードでのサインアップ・パスワード変更がブロックされる
- パスワードは bcrypt でハッシュ化されて保存される
- CAPTCHA を有効にしている場合、
signUp/signInWithPasswordにcaptchaTokenを渡す必要がある
関連
- メール OTP / Magic Link
- 電話番号認証
- CAPTCHA
- エラーコード
電話番号認証
SMS OTP による電話番号ベースの認証フロー。
概要
電話番号認証は、SMS で送信される OTP(ワンタイムパスワード)を使って認証を行う方式である。ユーザーは電話番号を入力し、受信した OTP コードを入力してログインする。
SMS プロバイダ
Supabase Auth は以下の SMS プロバイダをサポートしている:
| プロバイダ | 設定項目 |
|---|---|
| Twilio | Account SID, Auth Token, Message Service SID |
| Twilio Verify | Account SID, Auth Token, Verify Service SID |
| MessageBird | Access Key, Originator |
| Vonage | API Key, API Secret, From Number |
| TextLocal | API Key, Sender |
認証フロー
1. ユーザーが電話番号を入力 2. signInWithOtp() で SMS 送信をリクエスト 3. SMS で OTP コードが送信される 4. ユーザーが OTP コードを入力 5. verifyOtp() でコードを検証 6. セッションが作成される
コード例
// === 電話番号で OTP サインイン ===
// 1. SMS で OTP コードを送信
const { data, error } = await supabase.auth.signInWithOtp({
phone: '+819012345678',
})
if (error) {
console.error('SMS send error:', error.message)
}
// 2. ユーザーが入力した OTP コードで検証
const { data: verifyData, error: verifyError } = await supabase.auth.verifyOtp({
phone: '+819012345678',
token: '123456',
type: 'sms',
})
if (verifyData.session) {
console.log('Logged in:', verifyData.user)
}
// === 電話番号とパスワードでサインアップ ===
const { data, error } = await supabase.auth.signUp({
phone: '+819012345678',
password: 'securePassword123!',
})
// SMS で送信された OTP コードで確認
const { data: verifyData, error: verifyError } = await supabase.auth.verifyOtp({
phone: '+819012345678',
token: '123456',
type: 'sms',
})
// === 電話番号とパスワードでサインイン ===
const { data, error } = await supabase.auth.signInWithPassword({
phone: '+819012345678',
password: 'securePassword123!',
})
// === 電話番号の変更 ===
// ログイン済みユーザーの電話番号を変更
const { data, error } = await supabase.auth.updateUser({
phone: '+819087654321',
})
// 新しい番号に送信された OTP で確認
const { data: verifyData, error: verifyError } = await supabase.auth.verifyOtp({
phone: '+819087654321',
token: '123456',
type: 'phone_change',
})
// === verifyOtp の type パラメータ ===
// 'sms' - SMS OTP でのサインイン
// 'phone_change' - 電話番号変更の確認
// === Admin API でユーザー作成 ===
const { data, error } = await supabaseAdmin.auth.admin.createUser({
phone: '+819012345678',
phone_confirm: true, // 電話番号確認をスキップ
})注意点
- 電話番号は国際形式(E.164)で指定する必要がある(例:
+819012345678) - SMS 送信にはレート制限がある(デフォルト: 1 通/60 秒)
- SMS プロバイダの設定はダッシュボードの Auth > Providers > Phone で行う
- SMS の送信コストはプロバイダの料金体系に依存する(Supabase とは別課金)
- OTP コードのデフォルト有効期限は 5 分間
- テスト環境では、Auth Hook の Send SMS Hook を使って実際の SMS 送信を回避できる
- Twilio Verify を使用する場合、OTP の生成と検証は Twilio 側で行われる
関連
- パスワード認証
- メール OTP / Magic Link
- Auth Hooks
- レート制限
Auth レート制限
認証エンドポイント別のレート制限と対策。
概要
Supabase Auth は、サービスの安定性と不正アクセス防止のために、各エンドポイントにレート制限を設けている。レート制限を超えると HTTP 429(Too Many Requests)エラーが返される。
エンドポイント別レート制限(Free プラン)
| エンドポイント | 制限 | 期間 |
|---|---|---|
サインアップ(signUp) | 制限あり | IP あたり |
サインイン(signInWithPassword) | 制限あり | IP あたり |
OTP 送信(signInWithOtp) | 制限あり | IP あたり |
| パスワードリセット | 制限あり | IP あたり |
| トークンリフレッシュ | 制限あり | IP あたり |
| ユーザー情報取得 | 制限あり | IP あたり |
メール送信制限
| 種類 | Free プラン | Pro プラン |
|---|---|---|
| メール送信(合計) | 2 通/時間 | 制限緩和可能 |
| 同一アドレスへの送信 | 60 秒に 1 通 | 60 秒に 1 通 |
| 確認メール | レート制限あり | 制限緩和可能 |
SMS 送信制限
| 種類 | 制限 |
|---|---|
| SMS 送信 | 60 秒に 1 通(同一番号) |
| SMS 送信(合計) | プランにより異なる |
プラン別の制限
| プラン | 特徴 |
|---|---|
| Free | 厳格なレート制限。開発・テスト向け |
| Pro | レート制限の緩和が可能。サポートに連絡して制限値を調整 |
| Enterprise | カスタムレート制限の設定が可能 |
コード例
// === レート制限エラーのハンドリング ===
async function signInWithRetry(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
if (error.status === 429) {
// レート制限に到達
console.error('Rate limit exceeded. Please try again later.')
// Retry-After ヘッダーがある場合はその時間を待つ
// ユーザーに待機を促す UI を表示
showRateLimitMessage()
return
}
console.error('Sign in error:', error.message)
return
}
return data
}
// === OTP 送信のレート制限対策 ===
async function sendOTP(email: string) {
const { data, error } = await supabase.auth.signInWithOtp({
email,
})
if (error) {
if (error.status === 429) {
// メール送信のレート制限
// "Email rate limit exceeded" など
showMessage('メールの送信制限に達しました。60 秒後に再試行してください。')
return
}
if (error.message.includes('rate limit')) {
showMessage('送信制限に達しました。しばらくお待ちください。')
return
}
}
showMessage('確認コードを送信しました。')
}
// === クライアントサイドでの送信制限 ===
// 連続送信を防止するデバウンス
let lastSendTime = 0
const SEND_INTERVAL = 60000 // 60秒
async function sendOTPWithThrottle(email: string) {
const now = Date.now()
const elapsed = now - lastSendTime
if (elapsed < SEND_INTERVAL) {
const remaining = Math.ceil((SEND_INTERVAL - elapsed) / 1000)
showMessage(`${remaining} 秒後に再送信できます。`)
return
}
const { data, error } = await supabase.auth.signInWithOtp({ email })
if (!error) {
lastSendTime = Date.now()
}
}
// === React での再送信タイマー ===
import { useState, useEffect } from 'react'
function OTPForm() {
const [cooldown, setCooldown] = useState(0)
const [email, setEmail] = useState('')
useEffect(() => {
if (cooldown > 0) {
const timer = setTimeout(() => setCooldown(cooldown - 1), 1000)
return () => clearTimeout(timer)
}
}, [cooldown])
async function handleSendOTP() {
if (cooldown > 0) return
const { error } = await supabase.auth.signInWithOtp({ email })
if (error?.status === 429) {
setCooldown(60)
return
}
if (!error) {
setCooldown(60) // 60秒のクールダウン
}
}
return (
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button onClick={handleSendOTP} disabled={cooldown > 0}>
{cooldown > 0 ? `再送信 (${cooldown}秒)` : 'OTP を送信'}
</button>
</div>
)
}
// === CAPTCHA によるレート制限対策 ===
// CAPTCHA を有効にすることで、bot による大量リクエストを防止
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
options: {
captchaToken: hcaptchaToken,
},
})
// === サーバーサイドでのレート制限(Edge Function) ===
// Edge Function で独自のレート制限を実装
// Deno.serve(async (req) => {
// const ip = req.headers.get('x-forwarded-for') || 'unknown'
// const key = `rate_limit:${ip}`
//
// // KV ストアやデータベースでレート制限を管理
// const count = await getRequestCount(key)
// if (count > 10) {
// return new Response('Too Many Requests', { status: 429 })
// }
//
// await incrementRequestCount(key)
// // ... 処理を続行
// })注意点
- Free プランのメール送信制限は非常に厳格(2 通/時間)。開発時はメール確認を無効にするか、Inbucket を使用すること
- レート制限は IP アドレスベースのため、NAT 環境下では複数ユーザーが同じ制限を共有する可能性がある
- Pro プランでのレート制限緩和は、サポートチームに連絡して依頼する必要がある
- クライアントサイドで送信間隔を制御しても、サーバーサイドのレート制限は独立して適用される
- SMS 送信は外部プロバイダ(Twilio 等)の料金が発生するため、レート制限は特に重要
- CAPTCHA を有効にすることで、bot による不正なリクエストを効果的に防止できる
supabase.auth.resend()メソッドでも OTP / 確認メールを再送信できるが、同じレート制限が適用される- 本番環境で Free プランを使用する場合は、メール送信制限に特に注意が必要
関連
- Auth 概要
- CAPTCHA
- エラーコード
- メール OTP / Magic Link
Auth
| Name | Description | Path |
|---|---|---|
| 匿名認証 | 匿名ユーザーの作成と、認証済みアカウントへの変換。 | anonymous-auth.md |
| Auth Hooks | サーバーサイドで認証フローをカスタマイズする 6 種類のフック。 | auth-hooks.md |
| CAPTCHA 連携 | hCaptcha と Cloudflare Turnstile による bot 対策。 | captcha.md |
| メール OTP / Magic Link | メールベースのパスワードレス認証(OTP コードとマジックリンク)。 | email-passwordless.md |
| Auth エラーコード一覧 | Supabase Auth が返すエラーコードの一覧と対処法。 | error-codes.md |
| ID 管理・アカウントリンク | auth.identities テーブルを使った複数プロバイダのアカウントリンク機能。 | identities.md |
| JWT 構造 | Supabase Auth が発行する JWT のクレーム構造と RLS での活用。 | jwts.md |
| 多要素認証(MFA) | TOTP と Phone(SMS)を使った多要素認証の実装。 | mfa.md |
| Supabase OAuth Server | Supabase プロジェクトを OAuth 2.0 プロバイダとして機能させる。 | oauth-server.md |
| Auth アーキテクチャ概要 | Supabase Auth は GoTrue ベースの認証サーバーで、JWT によるセッション管理を提供する。 | overview.md |
| パスワード認証 | メールアドレスまたは電話番号とパスワードによるサインアップ・サインイン。 | passwords.md |
| 電話番号認証 | SMS OTP による電話番号ベースの認証フロー。 | phone-login.md |
| Auth レート制限 | 認証エンドポイント別のレート制限と対策。 | rate-limits.md |
| リダイレクト URL 設定 | 認証フロー後のリダイレクト先 URL の設定と管理。 | redirect-urls.md |
| サーバーサイド認証(SSR) | @supabase/ssr パッケージを使ったサーバーサイドでの認証管理。 | server-side.md |
| セッション管理 | JWT ベースのセッション管理、PKCE フロー、リフレッシュトークン、認証状態の監視。 | sessions.md |
| ソーシャルログイン(OAuth) | OAuth 2.0 / OIDC プロバイダを使ったソーシャルログイン。 | social-login.md |
| Enterprise SSO / SAML | SAML 2.0 プロトコルを使ったエンタープライズ向けシングルサインオン。 | sso-saml.md |
| サードパーティ Auth 連携 | 外部認証プロバイダとの統合と移行パス。 | third-party-auth.md |
| ユーザー管理 | auth.users テーブルの構造とユーザーの作成・更新・削除を行う方法。 | users.md |
リダイレクト URL 設定
認証フロー後のリダイレクト先 URL の設定と管理。
概要
Supabase Auth では、OAuth、マジックリンク、パスワードリセットなどの認証フロー後にユーザーをリダイレクトする URL を管理する必要がある。セキュリティのため、リダイレクト先 URL はダッシュボードの許可リストに登録しなければならない。
設定場所
ダッシュボードの Auth > URL Configuration で以下を設定:
- Site URL: デフォルトのリダイレクト先(必須)
- Redirect URLs: 許可するリダイレクト URL のリスト
ワイルドカードサポート
リダイレクト URL にはワイルドカード(*)が使用可能:
| パターン | マッチ例 |
|---|---|
https://*.example.com/** | https://app.example.com/dashboard |
https://example.com/* | https://example.com/callback |
http://localhost:*/** | http://localhost:3000/auth/callback |
コード例
// === redirectTo の指定 ===
// OAuth サインインでリダイレクト先を指定
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://example.com/auth/callback',
},
})
// マジックリンクでリダイレクト先を指定
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'https://example.com/dashboard',
},
})
// パスワードリセットでリダイレクト先を指定
const { error } = await supabase.auth.resetPasswordForEmail(
'user@example.com',
{
redirectTo: 'https://example.com/reset-password',
}
)
// サインアップでメール確認後のリダイレクト先を指定
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'password123',
options: {
emailRedirectTo: 'https://example.com/welcome',
},
})
// === 開発環境用の設定 ===
// ダッシュボードの Redirect URLs に追加:
// http://localhost:3000/**
// http://localhost:5173/**
// http://127.0.0.1:3000/**
// === Vercel プレビューデプロイ用 ===
// ダッシュボードの Redirect URLs に追加:
// https://*-your-team.vercel.app/**
// === ディープリンク(モバイルアプリ) ===
// iOS: カスタム URL スキーム
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'myapp://auth/callback',
},
})
// Android: カスタム URL スキーム
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'myapp://auth/callback',
},
})
// ダッシュボードの Redirect URLs に追加:
// myapp://auth/callback
// === Expo / React Native ===
import { makeRedirectUri } from 'expo-auth-session'
import * as QueryParams from 'expo-auth-session/build/QueryParams'
import * as Linking from 'expo-linking'
// Expo のリダイレクト URI を生成
const redirectTo = makeRedirectUri()
// 開発: exp://192.168.1.10:8081
// プロダクション: myapp://
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo,
skipBrowserRedirect: true,
},
})
// ブラウザを開く
if (data?.url) {
const result = await WebBrowser.openAuthSessionAsync(
data.url,
redirectTo
)
if (result.type === 'success') {
const { url } = result
// URL からセッションを抽出
const { params, errorCode } = QueryParams.getQueryParams(url)
if (params?.code) {
const { data, error } = await supabase.auth.exchangeCodeForSession(
params.code
)
}
}
}
// === コールバックルートの実装 ===
// Next.js: app/auth/callback/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
}
}
return NextResponse.redirect(`${origin}/auth/auth-code-error`)
}注意点
redirectToに指定する URL は、ダッシュボードの Redirect URLs に登録されている必要がある。未登録の URL は無視され、Site URL にリダイレクトされる- ワイルドカードは便利だが、過度に広いパターン(例:
https://**)はセキュリティリスクがある localhostは開発環境でのみ使用し、本番環境では削除すること- iOS / Android のディープリンクを使用する場合、カスタム URL スキームをダッシュボードに登録する必要がある
- Expo の開発環境では
exp://スキームが使用されるが、ビルド時にはアプリのカスタムスキームに変わる - メール内のリダイレクト URL は
emailRedirectTo、OAuth のリダイレクト URL はredirectToパラメータで指定する(名前が異なることに注意) - PKCE フローでは、リダイレクト先で
exchangeCodeForSession()を呼ぶ必要がある
関連
- Auth 概要
- ソーシャルログイン
- サーバーサイド認証
- セッション管理