better-auth について
better-auth の実装ガイドで、Cloudflare D1 の設定、認証プラグイン、設定ミスの防止を扱います。アプリに認証を組み込む開発者向けです。
主な機能
- 01Drizzle または Kysely による D1 設定
- 02OAuth 2.1 Provider の設定ガイド
- 03セッションと JWT の設定
- 04プラグインとフレームワークの連携情報
- 05レート制限の設定例
活用シーン
- 01D1 データベースアダプターの選定
- 02OAuth Provider 機能の追加
- 03ステートレスセッションの設定
- 04フレームワークの Cookie 処理の調査
MCP 対応
MCP サーバー向け OAuth 2.1 Provider の構築方法を説明し、専用 MCP プラグインは非推奨としています。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill better-authインストール後、Claude Code で/better-authを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonassets/auth-flow-diagram.mdcommands/setup.mdreferences/cloudflare-worker-drizzle.tscloudflare-worker-kysely.tsdatabase-schema.tshono-cloudflare.tsreact-client-hooks.tsxreferences/nextjs/README.mdpostgres-example.tsrules/better-auth.mdscripts/setup-d1-drizzle.sh詳細
better-auth - D1 アダプターとエラー防止ガイド
パッケージ: [email protected](2026年1月21日) 破壊的変更: ESM 専用(v1.4.0)、管理者のなりすまし防止がデフォルトで有効(v1.4.6)、複数チームのテーブル変更(v1.3)、D1 には Drizzle/Kysely が必要(直接利用できるアダプターはありません)
⚠️ 重要:D1 アダプターの要件
better-auth には d1Adapter() がありません。次のいずれかを必ず使用してください。
- Drizzle ORM(推奨):
drizzleAdapter(db, { provider: "sqlite" }) - Kysely:
new Kysely({ dialect: new D1Dialect({ database: env.DB }) })
詳しくは、下記の問題 #1 を参照してください。
v1.4.10 の新機能(2025年12月31日)
主な機能:
- OAuth 2.1 Provider プラグイン - 独自の OAuth プロバイダーを構築できます(MCP プラグインに代わるものです)
- Patreon OAuth プロバイダー - Patreon によるソーシャルサインイン
- Kick OAuth プロバイダー - リフレッシュトークンに対応
- Vercel OAuth プロバイダー - Vercel でサインイン
- グローバル
backgroundTasks設定 - パフォーマンス向上のための遅延アクション - フォームデータ対応 - fetch メタデータへのフォールバックを備えたメール認証
- Stripe の機能強化 - 柔軟なサブスクリプションライフサイクル、
disableRedirectオプション
Admin プラグインの更新:
- ⚠️ 破壊的変更:管理者のなりすましはデフォルトで無効です(v1.4.6)
- 権限ベースのユーザー更新に対応するロール
- ロール型の推論を改善
セキュリティ修正:
- 設定可能なサイズ制約による SAML XML パーサーの強化
- プロバイダーごとのクロックスキューに対応した SAML アサーションのタイムスタンプ検証
- SSO ドメイン検証済みプロバイダーの信頼
- 非推奨アルゴリズムの拒否
- 行 nonce の適用
📚 ドキュメント: https://www.better-auth.com/changelogs
v1.4.0 の新機能(2025年11月22日)
主な機能:
- ステートレスなセッション管理 - データベースに保存しないセッション
- ESM 専用パッケージ ⚠️ 破壊的変更:CommonJS はサポートされなくなりました
- JWT キーローテーション - セキュリティ強化のための自動キーローテーション
- SCIM プロビジョニング - エンタープライズ向けのユーザープロビジョニングプロトコル
- @standard-schema/spec - 検証で ZodType に代わるもの
- CaptchaFox との統合 - CAPTCHA の組み込みサポート
- サーバーサイド IP の自動検出
- Cookie ベースのアカウントデータ保存
- 複数のパスキーオリジンに対応
- RP 起点のログアウトエンドポイント(OIDC)
📚 ドキュメント: https://www.better-auth.com/changelogs
v1.3 の新機能(2025年7月)
主な機能:
- SAML 2.0 による SSO - エンタープライズ向けシングルサインオン(独立した
@better-auth/ssoパッケージに移行) - 複数チームのサポート ⚠️ 破壊的変更:メンバーテーブルから
teamIdが削除され、新しいteamMembersテーブルが必要です - 追加フィールド - 組織/メンバー/招待モデル用のカスタムフィールド
- パフォーマンスの改善とバグ修正
📚 ドキュメント: https://www.better-auth.com/blog/1-3
代替案:Kysely アダプターパターン
Drizzle より Kysely を使用したい場合:
ファイル: src/auth.ts
import { betterAuth } from "better-auth";
import { Kysely, CamelCasePlugin } from "kysely";
import { D1Dialect } from "kysely-d1";
type Env = {
DB: D1Database;
BETTER_AUTH_SECRET: string;
// ... other env vars
};
export function createAuth(env: Env) {
return betterAuth({
secret: env.BETTER_AUTH_SECRET,
// Kysely with D1Dialect
database: {
db: new Kysely({
dialect: new D1Dialect({
database: env.DB,
}),
plugins: [
// CRITICAL: Required if using Drizzle schema with snake_case
new CamelCasePlugin(),
],
}),
type: "sqlite",
},
emailAndPassword: {
enabled: true,
},
// ... other config
});
}
CamelCasePlugin を使う理由
Drizzle スキーマで snake_case のカラム名(例:email_verified)を使用していても、better-auth が camelCase(例:emailVerified)を想定している場合、CamelCasePlugin が両者を自動的に変換します。
⚠️ Cloudflare Workers に関する注意:D1 データベースのバインディングは、リクエストハンドラー(fetch() 関数)の内部でのみ利用できます。リクエストコンテキストの外で better-auth を初期化することはできません。ファクトリー関数パターンを使用してください。
// ❌ WRONG - DB binding not available outside request
const db = drizzle(env.DB, { schema }) // env.DB doesn't exist here
export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite" }) })
// ✅ CORRECT - Create auth instance per-request
export default {
fetch(request, env, ctx) {
const db = drizzle(env.DB, { schema })
const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite" }) })
return auth.handler(request)
}
}
コミュニティによる検証:複数の本番環境での実装により、このパターンが確認されています(Medium、AnswerOverflow、公式 Hono の例)。
フレームワークとの統合
TanStack Start
⚠️ 重大:TanStack Start で Cookie を正しく設定するには、reactStartCookies プラグインが必要です。
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { reactStartCookies } from "better-auth/react-start";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "sqlite" }),
plugins: [
twoFactor(),
organization(),
reactStartCookies(), // ⚠️ MUST be LAST plugin
],
});
必要な理由:TanStack Start は特殊な Cookie 処理システムを使用します。このプラグインがないと、signInEmail() や signUpEmail() などの認証関数で Cookie が正しく設定されず、認証に失敗します。
重要:reactStartCookies プラグインは、配列の最後に配置する必要があります。
セッションの null 許容パターン:TanStack Start で useSession() を使用する場合、セッションオブジェクトは常に存在しますが、ログインしていないときは session.user と session.session が null になります。
const { data: session } = authClient.useSession()
// When NOT logged in:
console.log(session) // { user: null, session: null }
console.log(!!session) // true (unexpected!)
// Correct check:
if (session?.user) {
// User is logged in
}
session だけでなく、必ず session?.user または session?.session を確認してください。これは想定された動作です(セッションオブジェクトのコンテナは常に存在します)。
API ルートの設定(/src/routes/api/auth/$.ts):
import { auth } from '@/lib/auth'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/api/auth/$')({
server: {
handlers: {
GET: ({ request }) => auth.handler(request),
POST: ({ request }) => auth.handler(request),
},
},
})
📚 公式ドキュメント:https://www.better-auth.com/docs/integrations/tanstack
利用可能なプラグイン(v1.4+)
Better Auth は、高度な認証機能を提供するプラグインを用意しています。
| プラグイン | インポート | 説明 | ドキュメント |
|---|---|---|---|
| OAuth 2.1 Provider | better-auth/plugins | PKCE、JWT トークン、同意フローを備えた OAuth 2.1 プロバイダーを構築します(MCP および OIDC プラグインを置き換えます) | 📚 |
| SSO | better-auth/plugins | OIDC、OAuth2、SAML 2.0 に対応したエンタープライズ向けシングルサインオン | 📚 |
| Stripe | better-auth/plugins | 柔軟なライフサイクル管理に対応した決済およびサブスクリプション管理 | 📚 |
| MCP | better-auth/plugins | ⚠️ 非推奨 — 代わりに OAuth 2.1 Provider を使用してください | 📚 |
| Expo | better-auth/expo | webBrowserOptions と最終ログイン方法の追跡に対応した React Native/Expo | 📚 |
OAuth 2.1 Provider プラグイン(v1.4.9 で追加)
MCP サーバー、サードパーティアプリ、API アクセス向けに独自の OAuth プロバイダーを構築できます。
import { betterAuth } from "better-auth";
import { oauthProvider } from "better-auth/plugins";
import { jwt } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
jwt(), // Required for token signing
oauthProvider({
// Token expiration (seconds)
accessTokenExpiresIn: 3600, // 1 hour
refreshTokenExpiresIn: 2592000, // 30 days
authorizationCodeExpiresIn: 600, // 10 minutes
}),
],
});
主な機能:
- OAuth 2.1 準拠 — PKCE が必須で、S256 のみを使用し、インプリシットフローには対応しません
- 3 種類のグラントタイプ:
authorization_code、refresh_token、client_credentials - JWT または不透明トークン — トークン形式を設定できます
- 動的クライアント登録 — RFC 7591 に準拠しています
- 同意管理 — 信頼されたクライアントの同意をスキップできます
- OIDC UserInfo エンドポイント — スコープに基づくクレームを返す
/oauth2/userinfo
必須の Well-Known エンドポイント:
// app/api/.well-known/oauth-authorization-server/route.ts
export async function GET() {
return Response.json({
issuer: process.env.BETTER_AUTH_URL,
authorization_endpoint: `${process.env.BETTER_AUTH_URL}/api/auth/oauth2/authorize`,
token_endpoint: `${process.env.BETTER_AUTH_URL}/api/auth/oauth2/token`,
// ... other metadata
});
}
OAuth クライアントの作成:
const client = await auth.api.createOAuthClient({
body: {
name: "My MCP Server",
redirectURLs: ["https://claude.ai/callback"],
type: "public", // or "confidential"
},
});
// Returns: { clientId, clientSecret (if confidential) }
📚 詳細なドキュメント:https://www.better-auth.com/docs/plugins/oauth-provider
⚠️ 注意:このプラグインは現在開発中であり、まだ本番環境での使用には適さない可能性があります。
その他のプラグインの一覧
| プラグイン | 説明 | ドキュメント |
|---|---|---|
| Bearer | API トークン認証(API で Cookie を使う場合の代替手段) | 📚 |
| One Tap | 手軽な Google One Tap サインイン | 📚 |
| SCIM | エンタープライズ向けユーザープロビジョニング(SCIM 2.0) | 📚 |
| Anonymous | 個人を特定できる情報なしでのゲストユーザーアクセス | 📚 |
| Username | ユーザー名ベースのサインイン(メールアドレスの代替手段) | 📚 |
| Generic OAuth | PKCE に対応したカスタム OAuth プロバイダー | 📚 |
| Multi-Session | 同じブラウザーでの複数アカウントの利用 | 📚 |
| API Key | レート制限に対応したトークンベースの認証 | 📚 |
Bearer Token プラグイン
API 専用の認証(モバイルアプリ、CLI ツール、サードパーティ連携)向け:
import { bearer } from "better-auth/plugins";
import { bearerClient } from "better-auth/client/plugins";
// Server
export const auth = betterAuth({
plugins: [bearer()],
});
// Client - Store token after sign-in
const { token } = await authClient.signIn.email({ email, password });
localStorage.setItem("auth_token", token);
// Client - Configure fetch to include token
const authClient = createAuthClient({
plugins: [bearerClient()],
fetchOptions: {
auth: { type: "Bearer", token: () => localStorage.getItem("auth_token") },
},
});
Google One Tap Plugin
すでに Google にサインインしているユーザーが、ワンタップでスムーズにサインインできます。
import { oneTap } from "better-auth/plugins";
import { oneTapClient } from "better-auth/client/plugins";
// Server
export const auth = betterAuth({
plugins: [oneTap()],
});
// Client
authClient.oneTap({
onSuccess: (session) => {
window.location.href = "/dashboard";
},
});
要件:Google Cloud Console で承認済みの JavaScript オリジンを設定してください。
Anonymous Plugin
メールアドレスやパスワードを求めずにゲストアクセスを提供します。
import { anonymous } from "better-auth/plugins";
// Server
export const auth = betterAuth({
plugins: [
anonymous({
emailDomainName: "anon.example.com", // temp@{id}.anon.example.com
onLinkAccount: async ({ anonymousUser, newUser }) => {
// Migrate anonymous user data to linked account
await migrateUserData(anonymousUser.id, newUser.id);
},
}),
],
});
// Client
await authClient.signIn.anonymous();
// Later: user can link to real account via signIn.social/email
Generic OAuth Plugin
組み込みリストにないカスタム OAuth プロバイダーを追加します。
import { genericOAuth } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
genericOAuth({
config: [
{
providerId: "linear",
clientId: env.LINEAR_CLIENT_ID,
clientSecret: env.LINEAR_CLIENT_SECRET,
discoveryUrl: "https://linear.app/.well-known/openid-configuration",
scopes: ["openid", "email", "profile"],
pkce: true, // Recommended
},
],
}),
],
});
コールバック URL のパターン:{baseURL}/api/auth/oauth2/callback/{providerId}
レート制限
カスタマイズ可能なルールを備えた組み込みのレート制限です。
export const auth = betterAuth({
rateLimit: {
window: 60, // seconds (default: 60)
max: 100, // requests per window (default: 100)
// Custom rules for sensitive endpoints
customRules: {
"/sign-in/email": { window: 10, max: 3 },
"/two-factor/*": { window: 10, max: 3 },
"/forget-password": { window: 60, max: 5 },
},
// Use Redis/KV for distributed systems
storage: "secondary-storage", // or "database"
},
// Secondary storage for rate limiting
secondaryStorage: {
get: async (key) => env.KV.get(key),
set: async (key, value, ttl) => env.KV.put(key, value, { expirationTtl: ttl }),
delete: async (key) => env.KV.delete(key),
},
});
注:auth.api.* を介したサーバーサイドの呼び出しには、レート制限が適用されません。
ステートレスセッション(v1.4.0+)
データベースに保存せず、署名付き Cookie のみにセッションを保存します。
export const auth = betterAuth({
session: {
// Stateless: No database storage, session lives in cookie only
storage: undefined, // or omit entirely
// Cookie configuration
cookieCache: {
enabled: true,
maxAge: 60 * 60 * 24 * 7, // 7 days
encoding: "jwt", // Use JWT for stateless (not "compact")
},
// Session expiration
expiresIn: 60 * 60 * 24 * 7, // 7 days
},
});
使用する場面:
| ストレージの種類 | 使用例 | トレードオフ |
|---|---|---|
| ステートレス(Cookie のみ) | 読み取りの多いアプリ、エッジ/サーバーレス、失効が不要な場合 | セッションを失効できない、ペイロードサイズに制限がある |
| D1 データベース | 完全なセッション管理、監査証跡、失効 | 結果整合性の問題 |
| KV ストレージ | 強い整合性、高い読み取りパフォーマンス | 追加のバインディング設定 |
主なポイント:
- ステートレスセッションは失効できません(ユーザーは有効期限まで待つ必要があります)
- Cookie のサイズ上限は約 4KB です(セッションデータが制限されます)
- 相互運用性には
encoding: "jwt"、暗号化には"jwe"を使用してください - すべてのインスタンスで、サーバーの
BETTER_AUTH_SECRETを同一にする必要があります
JWT キーローテーション(v1.4.0+)
セキュリティを強化するため、JWT 署名キーを自動的にローテーションします。
import { jwt } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
jwt({
// Key rotation (optional, enterprise security)
keyRotation: {
enabled: true,
rotationInterval: 60 * 60 * 24 * 30, // Rotate every 30 days
keepPreviousKeys: 3, // Keep 3 old keys for validation
},
// Custom signing algorithm (default: HS256)
algorithm: "RS256", // Requires asymmetric keys
// JWKS endpoint (auto-generated at /api/auth/jwks)
exposeJWKS: true,
}),
],
});
主なポイント:
- キーローテーションにより、侵害されたキーが無期限に有効になるのを防ぎます
- 既存のトークンを検証できるよう、古いキーは一時的に保持されます
- 外部サービス向けの JWKS エンドポイントは
/api/auth/jwksです - 公開鍵による検証(マイクロサービス)には RS256 を使用してください
- 単一サービスのアプリには HS256(デフォルト)を使用してください
プロバイダーのスコープ一覧
ユーザーデータの取得に必要な、一般的な OAuth プロバイダーとスコープです。
| プロバイダー | スコープ | 取得内容 |
|---|---|---|
openid | ユーザー ID のみ | |
email | メールアドレス、email_verified | |
profile | 名前、アバター(picture)、ロケール | |
| GitHub | user:email | メールアドレス(非公開の場合があります) |
read:user | 名前、アバター、プロフィール URL、自己紹介 | |
| Microsoft | openid | ユーザー ID のみ |
email | メールアドレス | |
profile | 名前、ロケール | |
User.Read | Graph API からの完全なプロフィール | |
| Discord | identify | ユーザー名、アバター、識別子 |
email | メールアドレス | |
| Apple | name | 名/姓(初回認証時のみ) |
email | メールアドレスまたはリレーアドレス | |
| Patreon | identity | ユーザー ID、名前 |
identity[email] | メールアドレス | |
| Vercel | (自動) | メールアドレス、名前、アバター |
設定例:
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
scope: ["openid", "email", "profile"], // All user data
},
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
scope: ["user:email", "read:user"], // Email + full profile
},
microsoft: {
clientId: env.MS_CLIENT_ID,
clientSecret: env.MS_CLIENT_SECRET,
scope: ["openid", "email", "profile", "User.Read"],
},
}
セッション Cookie のキャッシュ
セッション Cookie には 3 種類のエンコード方式があります。
| 方式 | 形式 | 使用例 |
|---|---|---|
| Compact(デフォルト) | Base64url + HMAC-SHA256 | 最小サイズで最速 |
| JWT | 標準 JWT | 相互運用性 |
| JWE | A256CBC-HS512 による暗号化 | 最も安全 |
export const auth = betterAuth({
session: {
cookieCache: {
enabled: true,
maxAge: 300, // 5 minutes
encoding: "compact", // or "jwt" or "jwe"
},
freshAge: 60 * 60 * 24, // 1 day - operations requiring fresh session
},
});
新しいセッション:一部の機密性の高い操作には、作成から時間が経っていないセッションが必要です。この期間は freshAge で設定します。
新しいソーシャルプロバイダー(v1.4.9+)
socialProviders: {
// Patreon - Creator economy
patreon: {
clientId: env.PATREON_CLIENT_ID,
clientSecret: env.PATREON_CLIENT_SECRET,
scope: ["identity", "identity[email]"],
},
// Kick - Streaming platform (with refresh tokens)
kick: {
clientId: env.KICK_CLIENT_ID,
clientSecret: env.KICK_CLIENT_SECRET,
},
// Vercel - Developer platform
vercel: {
clientId: env.VERCEL_CLIENT_ID,
clientSecret: env.VERCEL_CLIENT_SECRET,
},
}
Cloudflare Workers の要件
⚠️ 重要:Cloudflare Workers では AsyncLocalStorage のサポートが必要です。
# wrangler.toml
compatibility_flags = ["nodejs_compat"]
# or for older Workers:
# compatibility_flags = ["nodejs_als"]
このフラグがないと、better-auth はコンテキスト関連のエラーで失敗します。
データベースフック
データベース操作中にカスタムロジックを実行します。
export const auth = betterAuth({
databaseHooks: {
user: {
create: {
before: async (user, ctx) => {
// Validate or modify before creation
if (user.email?.endsWith("@blocked.com")) {
throw new APIError("BAD_REQUEST", { message: "Email domain not allowed" });
}
return { data: { ...user, role: "member" } };
},
after: async (user, ctx) => {
// Send welcome email, create related records, etc.
await sendWelcomeEmail(user.email);
await createDefaultWorkspace(user.id);
},
},
},
session: {
create: {
after: async (session, ctx) => {
// Audit logging
await auditLog.create({ action: "session_created", userId: session.userId });
},
},
},
},
});
利用可能なフック:user、session、account、verification テーブルの create、update。
Expo/React Native との連携
モバイル連携の完全なパターンです。
// Client setup with secure storage
import { expoClient } from "@better-auth/expo";
import * as SecureStore from "expo-secure-store";
const authClient = createAuthClient({
baseURL: "https://api.example.com",
plugins: [expoClient({ storage: SecureStore })],
});
// OAuth with deep linking
await authClient.signIn.social({
provider: "google",
callbackURL: "myapp://auth/callback", // Deep link
});
// Or use ID token verification (no redirect)
await authClient.signIn.social({
provider: "google",
idToken: {
token: googleIdToken,
nonce: generatedNonce,
},
});
// Authenticated requests
const cookie = await authClient.getCookie();
await fetch("https://api.example.com/data", {
headers: { Cookie: cookie },
credentials: "omit",
});
app.json のディープリンク設定:
{
"expo": {
"scheme": "myapp"
}
}
サーバーの trustedOrigins(開発環境):
trustedOrigins: ["exp://**", "myapp://"]
API リファレンス
概要:無料で利用できる機能
auth.handler() を呼び出すと、better-auth は本番環境ですぐに使える 80 以上の REST エンドポイントを /api/auth/* に自動的に公開します。すべてのエンドポイントは、プログラムから利用できる サーバーサイドメソッドとして auth.api.* からも使用できます。
この二層構造の API システムにより、次のことが可能になります。
- クライアント(React、Vue、モバイルアプリ)は HTTP エンドポイントを直接呼び出します
- サーバーサイドコード(ミドルウェア、バックグラウンドジョブ)は
auth.api.*メソッドを使用します - 定型コードは不要 — 認証エンドポイントを手動で作成する必要はありません
時間の節約:ゼロから構築する場合は約 220 時間かかります。better-auth を使えば約 4〜8 時間です。97%削減できます。
自動生成される HTTP エンドポイント
auth.handler() を使用すると、すべてのエンドポイントが /api/auth/* に自動的に公開されます。
認証のコアエンドポイント
| エンドポイント | メソッド | 説明 |
|---|---|---|
/sign-up/email | POST | メールアドレスとパスワードで登録 |
/sign-in/email | POST | メールアドレスとパスワードで認証 |
/sign-out | POST | ユーザーをログアウト |
/change-password | POST | パスワードを更新(現在のパスワードが必要) |
/forget-password | POST | パスワードリセットフローを開始 |
/reset-password | POST | トークンを使ってパスワードリセットを完了 |
/send-verification-email | POST | メールアドレス確認リンクを送信 |
/verify-email | GET | トークンを使ってメールアドレスを確認(?token=<token>) |
/get-session | GET | 現在のセッションを取得 |
/list-sessions | GET | ユーザーのアクティブなセッションをすべて取得 |
/revoke-session | POST | 指定したセッションを終了 |
/revoke-other-sessions | POST | 現在のセッション以外のすべてのセッションを終了 |
/revoke-sessions | POST | ユーザーのすべてのセッションを終了 |
/update-user | POST | ユーザープロフィールを変更(名前、画像) |
/change-email | POST | メールアドレスを更新 |
/set-password | POST | OAuth のみのアカウントにパスワードを追加 |
/delete-user | POST | ユーザーアカウントを削除 |
/list-accounts | GET | リンク済みの認証プロバイダーを取得 |
/link-social | POST | OAuth プロバイダーをアカウントに接続 |
/unlink-account | POST | プロバイダーの接続を解除 |
ソーシャル OAuth エンドポイント
| エンドポイント | メソッド | 説明 |
|---|---|---|
/sign-in/social | POST | OAuth フローを開始(プロバイダーは本文で指定) |
/callback/:provider | GET | OAuth コールバックハンドラー(例:/callback/google) |
/get-access-token | GET | プロバイダーのアクセストークンを取得 |
OAuth フローの例:
// Client initiates
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
// better-auth handles redirect to Google
// Google redirects back to /api/auth/callback/google
// better-auth creates session automatically
プラグインのエンドポイント
二要素認証(2FA プラグイン)
import { twoFactor } from "better-auth/plugins";
| エンドポイント | メソッド | 説明 |
|---|---|---|
/two-factor/enable | POST | ユーザーの 2FA を有効化 |
/two-factor/disable | POST | 2FA を無効化 |
/two-factor/get-totp-uri | GET | 認証アプリ用の QR コード URI を取得 |
/two-factor/verify-totp | POST | 認証アプリの TOTP コードを検証 |
/two-factor/send-otp | POST | メールで OTP を送信 |
/two-factor/verify-otp | POST | メールの OTP を検証 |
/two-factor/generate-backup-codes | POST | リカバリーコードを作成 |
/two-factor/verify-backup-code | POST | バックアップコードを使ってログイン |
/two-factor/view-backup-codes | GET | 現在のバックアップコードを表示 |
📚 ドキュメント:https://www.better-auth.com/docs/plugins/2fa
組織プラグイン(マルチテナント SaaS)
import { organization } from "better-auth/plugins";
組織(10 エンドポイント):
| エンドポイント | メソッド | 説明 |
|---|---|---|
/organization/create | POST | 組織を作成 |
/organization/list | GET | ユーザーの組織一覧を取得 |
/organization/get-full | GET | 組織の詳細情報を取得 |
/organization/update | PUT | 組織を変更 |
/organization/delete | DELETE | 組織を削除 |
/organization/check-slug | GET | スラッグが利用可能か確認 |
/organization/set-active | POST | アクティブな組織コンテキストを設定 |
メンバー(8 エンドポイント):
| エンドポイント | メソッド | 説明 |
|---|---|---|
/organization/list-members | GET | 組織のメンバーを取得 |
/organization/add-member | POST | メンバーを直接追加 |
/organization/remove-member | DELETE | メンバーを削除 |
/organization/update-member-role | PUT | メンバーのロールを変更 |
/organization/get-active-member | GET | 現在のメンバー情報を取得 |
/organization/leave | POST | 組織から脱退 |
招待(7 エンドポイント):
| エンドポイント | メソッド | 説明 |
|---|---|---|
/organization/invite-member | POST | 招待メールを送信 |
/organization/accept-invitation | POST | 招待を承諾 |
/organization/reject-invitation | POST | 招待を拒否 |
/organization/cancel-invitation | POST | 保留中の招待を取り消し |
/organization/get-invitation | GET | 招待の詳細を取得 |
/organization/list-invitations | GET | 組織の招待一覧を取得 |
/organization/list-user-invitations | GET | ユーザーの保留中の招待一覧を取得 |
チーム(8 エンドポイント):
| エンドポイント | メソッド | 説明 |
|---|---|---|
/organization/create-team | POST | 組織内にチームを作成 |
/organization/list-teams | GET | 組織のチーム一覧を取得 |
/organization/update-team | PUT | チームを変更 |
/organization/remove-team | DELETE | チームを削除 |
/organization/set-active-team | POST | アクティブなチームコンテキストを設定 |
/organization/list-team-members | GET | チームのメンバー一覧を取得 |
/organization/add-team-member | POST | チームにメンバーを追加 |
/organization/remove-team-member | DELETE | チームメンバーを削除 |
権限とロール(6 エンドポイント):
| エンドポイント | メソッド | 説明 |
|---|---|---|
/organization/has-permission | POST | ユーザーが権限を持っているか確認 |
/organization/create-role | POST | カスタムロールを作成 |
/organization/delete-role | DELETE | カスタムロールを削除 |
/organization/list-roles | GET | すべてのロールを一覧表示 |
/organization/get-role | GET | ロールの詳細を取得 |
/organization/update-role | PUT | ロールの権限を変更 |
📚 ドキュメント: https://www.better-auth.com/docs/plugins/organization
管理者プラグイン
import { admin } from "better-auth/plugins";
// v1.4.10 configuration options
admin({
defaultRole: "user",
adminRoles: ["admin"],
adminUserIds: ["user_abc123"], // Always grant admin to specific users
impersonationSessionDuration: 3600, // 1 hour (seconds)
allowImpersonatingAdmins: false, // ⚠️ Default changed in v1.4.6
defaultBanReason: "Violation of Terms of Service",
bannedUserMessage: "Your account has been suspended",
})
| エンドポイント | メソッド | 説明 |
|---|---|---|
/admin/create-user | POST | 管理者としてユーザーを作成 |
/admin/list-users | GET | すべてのユーザーを一覧表示(フィルター/ページネーション付き) |
/admin/set-role | POST | ユーザーにロールを割り当て |
/admin/set-user-password | POST | ユーザーのパスワードを変更 |
/admin/update-user | PUT | ユーザーの詳細を変更 |
/admin/remove-user | DELETE | ユーザーアカウントを削除 |
/admin/ban-user | POST | ユーザーアカウントを禁止(有効期限の指定は任意) |
/admin/unban-user | POST | ユーザーの禁止を解除 |
/admin/list-user-sessions | GET | ユーザーのアクティブなセッションを取得 |
/admin/revoke-user-session | DELETE | 特定のユーザーセッションを終了 |
/admin/revoke-user-sessions | DELETE | すべてのユーザーセッションを終了 |
/admin/impersonate-user | POST | ユーザーのなりすましを開始 |
/admin/stop-impersonating | POST | なりすましセッションを終了 |
⚠️ 破壊的変更(v1.4.6): allowImpersonatingAdmins のデフォルト値が false になりました。管理者が別の管理者になりすます必要がある場合は、明示的に true に設定してください。
権限付きカスタムロール(v1.4.10):
import { createAccessControl } from "better-auth/plugins/access";
// Define resources and permissions
const ac = createAccessControl({
user: ["create", "read", "update", "delete", "ban", "impersonate"],
project: ["create", "read", "update", "delete", "share"],
} as const);
// Create custom roles
const supportRole = ac.newRole({
user: ["read", "ban"], // Can view and ban users
project: ["read"], // Can view projects
});
const managerRole = ac.newRole({
user: ["read", "update"],
project: ["create", "read", "update", "delete"],
});
// Use in plugin
admin({
ac,
roles: {
support: supportRole,
manager: managerRole,
},
})
📚 ドキュメント: https://www.better-auth.com/docs/plugins/admin
その他のプラグインエンドポイント
Passkey プラグイン(5 エンドポイント) - ドキュメント:
/passkey/add、/sign-in/passkey、/passkey/list、/passkey/delete、/passkey/update
Magic Link プラグイン(2 エンドポイント) - ドキュメント:
/sign-in/magic-link、/magic-link/verify
Username プラグイン(2 エンドポイント) - ドキュメント:
/sign-in/username、/username/is-available
電話番号プラグイン(5 エンドポイント) - ドキュメント:
/sign-in/phone-number、/phone-number/send-otp、/phone-number/verify、/phone-number/request-password-reset、/phone-number/reset-password
Email OTP プラグイン(6 エンドポイント) - ドキュメント:
/email-otp/send-verification-otp、/email-otp/check-verification-otp、/sign-in/email-otp、/email-otp/verify-email、/forget-password/email-otp、/email-otp/reset-password
匿名プラグイン(1 エンドポイント) - ドキュメント:
/sign-in/anonymous
JWT プラグイン(2 エンドポイント) - ドキュメント:
/token(JWT を取得)、/jwks(検証用の公開鍵)
OpenAPI プラグイン(2 エンドポイント) - ドキュメント:
/reference(Scalar UI を使用したインタラクティブな API ドキュメント)/generate-openapi-schema(OpenAPI 仕様を JSON 形式で取得)
サーバーサイド API メソッド(auth.api.*)
すべての HTTP エンドポイントには、対応するサーバーサイドメソッドがあります。次の場合に使用します。
- サーバーサイドミドルウェア(ルートの保護)
- バックグラウンドジョブ(ユーザーの整理、通知)
- 管理操作(ユーザーの一括管理)
- カスタム認証フロー(プログラムによるセッションの作成)
コア API メソッド
// Authentication
await auth.api.signUpEmail({
body: { email, password, name },
headers: request.headers,
});
await auth.api.signInEmail({
body: { email, password, rememberMe: true },
headers: request.headers,
});
await auth.api.signOut({ headers: request.headers });
// Session Management
const session = await auth.api.getSession({ headers: request.headers });
await auth.api.listSessions({ headers: request.headers });
await auth.api.revokeSession({
body: { token: "session_token_here" },
headers: request.headers,
});
// User Management
await auth.api.updateUser({
body: { name: "New Name", image: "https://..." },
headers: request.headers,
});
await auth.api.changeEmail({
body: { newEmail: "[email protected]" },
headers: request.headers,
});
await auth.api.deleteUser({
body: { password: "current_password" },
headers: request.headers,
});
// Account Linking
await auth.api.linkSocialAccount({
body: { provider: "google" },
headers: request.headers,
});
await auth.api.unlinkAccount({
body: { providerId: "google", accountId: "google_123" },
headers: request.headers,
});
プラグイン API メソッド
2FA プラグイン:
// Enable 2FA
const { totpUri, backupCodes } = await auth.api.enableTwoFactor({
body: { issuer: "MyApp" },
headers: request.headers,
});
// Verify TOTP code
await auth.api.verifyTOTP({
body: { code: "123456", trustDevice: true },
headers: request.headers,
});
// Generate backup codes
const { backupCodes } = await auth.api.generateBackupCodes({
headers: request.headers,
});
Organization プラグイン:
// Create organization
const org = await auth.api.createOrganization({
body: { name: "Acme Corp", slug: "acme" },
headers: request.headers,
});
// Add member
await auth.api.addMember({
body: {
userId: "user_123",
role: "admin",
organizationId: org.id,
},
headers: request.headers,
});
// Check permissions
const hasPermission = await auth.api.hasPermission({
body: {
organizationId: org.id,
permission: "users:delete",
},
headers: request.headers,
});
Admin プラグイン:
// List users with pagination
const users = await auth.api.listUsers({
query: {
search: "john",
limit: 10,
offset: 0,
sortBy: "createdAt",
sortOrder: "desc",
},
headers: request.headers,
});
// Ban user
await auth.api.banUser({
body: {
userId: "user_123",
reason: "Violation of ToS",
expiresAt: new Date("2025-12-31"),
},
headers: request.headers,
});
// Impersonate user (for admin support)
const impersonationSession = await auth.api.impersonateUser({
body: {
userId: "user_123",
expiresIn: 3600, // 1 hour
},
headers: request.headers,
});
用途に応じた使い分け
| 用途 | HTTP エンドポイントを使用 | auth.api.* メソッドを使用 |
|---|---|---|
| クライアントサイド認証 | ✅ はい | ❌ いいえ |
| サーバーミドルウェア | ❌ いいえ | ✅ はい |
| バックグラウンドジョブ | ❌ いいえ | ✅ はい |
| 管理ダッシュボード | ✅ はい(クライアントから) | ✅ はい(サーバーから) |
| カスタム認証フロー | ❌ いいえ | ✅ はい |
| モバイルアプリ | ✅ はい | ❌ いいえ |
| API ルート | ✅ はい(ハンドラーにプロキシ) | ✅ はい(直接呼び出し) |
例:保護されたルートのミドルウェア
import { Hono } from "hono";
import { createAuth } from "./auth";
import { createDatabase } from "./db";
const app = new Hono<{ Bindings: Env }>();
// Middleware using server-side API
app.use("/api/protected/*", async (c, next) => {
const db = createDatabase(c.env.DB);
const auth = createAuth(db, c.env);
// Use server-side method
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
return c.json({ error: "Unauthorized" }, 401);
}
// Attach to context
c.set("user", session.user);
c.set("session", session.session);
await next();
});
// Protected route
app.get("/api/protected/profile", async (c) => {
const user = c.get("user");
return c.json({ user });
});
利用可能なエンドポイントの確認
OpenAPI プラグインを使って、設定内のすべてのエンドポイントを確認できます。
import { betterAuth } from "better-auth";
import { openAPI } from "better-auth/plugins";
export const auth = betterAuth({
database: /* ... */,
plugins: [
openAPI(), // Adds /api/auth/reference endpoint
],
});
インタラクティブなドキュメント:http://localhost:8787/api/auth/reference にアクセスしてください。
次の内容を含む Scalar UI が表示されます。
- ✅ 機能ごとに分類された利用可能なすべてのエンドポイント
- ✅ 型付きのリクエスト/レスポンススキーマ
- ✅ 試用機能(ブラウザーでエンドポイントをテスト)
- ✅ 認証要件
- ✅ 複数の言語でのコード例
プログラムからのアクセス:
const schema = await auth.api.generateOpenAPISchema();
console.log(JSON.stringify(schema, null, 2));
// Returns full OpenAPI 3.0 spec
定量化した時間の削減
ゼロから構築する場合(手動で実装):
- コア認証エンドポイント(サインアップ、サインイン、OAuth、セッション):40 時間
- メール確認とパスワードリセット:10 時間
- 2FA システム(TOTP、バックアップコード、メール OTP):20 時間
- Organization(チーム、招待、RBAC):60 時間
- 管理パネル(ユーザー管理、なりすまし):30 時間
- テストとデバッグ:50 時間
- セキュリティ強化:20 時間
手動作業の合計:約 220 時間(フルタイムで 5.5 週間)
better-auth を使用する場合:
- 初期設定:2〜4 時間
- カスタマイズとスタイリング:2〜4 時間
better-auth を使った場合の合計:4〜8 時間
削減率:開発時間を約 97%削減
主なポイント
better-auth は、以下をカバーする 本番環境対応のエンドポイントを 80 個以上提供します。
- ✅ コア認証(20 個のエンドポイント)
- ✅ 2FA とパスワードレス認証(15 個のエンドポイント)
- ✅ 組織とチーム(35 個のエンドポイント)
- ✅ 管理者とユーザーの管理(15 個のエンドポイント)
- ✅ ソーシャル OAuth(コールバックを自動設定)
- ✅ OpenAPI ドキュメント(インタラクティブ UI)
エンドポイントのコードを書く必要はありません。 機能を設定して、auth.handler() を呼び出すだけです。
既知の問題と解決策
問題 1:「d1Adapter is not exported」エラー
問題:コードに import { d1Adapter } from 'better-auth/adapters/d1' とありますが、これは存在しません。
症状:エクスポートが見つからないという TypeScript エラーまたは実行時エラーが発生します。
解決策:代わりに Drizzle または Kysely を使用します。
// ❌ WRONG - This doesn't exist
import { d1Adapter } from 'better-auth/adapters/d1'
database: d1Adapter(env.DB)
// ✅ CORRECT - Use Drizzle
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { drizzle } from 'drizzle-orm/d1'
const db = drizzle(env.DB, { schema })
database: drizzleAdapter(db, { provider: "sqlite" })
// ✅ CORRECT - Use Kysely
import { Kysely } from 'kysely'
import { D1Dialect } from 'kysely-d1'
database: {
db: new Kysely({ dialect: new D1Dialect({ database: env.DB }) }),
type: "sqlite"
}
出典:better-auth と D1 を使用している 4 つの本番環境リポジトリで確認済み
問題 2:スキーマ生成に失敗する
問題:npx better-auth migrate を実行しても、D1 と互換性のあるスキーマが作成されません。
症状:マイグレーション SQL の構文が正しくないか、D1 で動作しません。
解決策:Drizzle Kit を使用してマイグレーションを生成します。
# Generate migration from Drizzle schema
npx drizzle-kit generate
# Apply to D1
wrangler d1 migrations apply my-app-db --remote
理由:Drizzle Kit は D1 で動作する、SQLite 互換の SQL を生成します。
問題 3:「CamelCase」と「snake_case」のカラム不一致
問題:データベースには email_verified がありますが、better-auth は emailVerified を想定しています。
症状:セッションの読み取りに失敗し、ユーザーデータのフィールドが欠落します。
⚠️ 重要(v1.4.10 以降):Kysely の CamelCasePlugin を使用すると、better-auth アダプターでの結合解析が壊れます。このプラグインは _joined_user_user_id のような結合キーを _joinedUserUserId に変換するため、セッションクエリでユーザーデータが null になります。
Drizzle の解決策:最初から camelCase でスキーマを定義します(例を参照)。
CamelCasePlugin を使用する Kysely の解決策:better-auth 用に、CamelCasePlugin を使用しない別の Kysely インスタンスを使います。
// DB for better-auth (no CamelCasePlugin)
const authDb = new Kysely({
dialect: new D1Dialect({ database: env.DB }),
})
// DB for app queries (with CamelCasePlugin)
const appDb = new Kysely({
dialect: new D1Dialect({ database: env.DB }),
plugins: [new CamelCasePlugin()],
})
export const auth = betterAuth({
database: { db: authDb, type: "sqlite" },
})
問題 4:D1 の結果整合性
問題:書き込み直後のセッション読み取りで、古いデータが返されます。
症状:ユーザーがログインしたのに、次のリクエストで getSession() が null を返します。
解決策:セッションストレージに Cloudflare KV を使用します(強い整合性)。
import { betterAuth } from "better-auth";
export function createAuth(db: Database, env: Env) {
return betterAuth({
database: drizzleAdapter(db, { provider: "sqlite" }),
session: {
storage: {
get: async (sessionId) => {
const session = await env.SESSIONS_KV.get(sessionId);
return session ? JSON.parse(session) : null;
},
set: async (sessionId, session, ttl) => {
await env.SESSIONS_KV.put(sessionId, JSON.stringify(session), {
expirationTtl: ttl,
});
},
delete: async (sessionId) => {
await env.SESSIONS_KV.delete(sessionId);
},
},
},
});
}
wrangler.toml に追加します:
[[kv_namespaces]]
binding = "SESSIONS_KV"
id = "your-kv-namespace-id"
問題 5:SPA アプリケーションでの CORS エラー
問題:認証 API とフロントエンドのオリジンが異なる場合に、CORS エラーが発生します。
症状:ブラウザーのコンソールに Access-Control-Allow-Origin エラーが表示されます。
解決策:Worker で CORS ヘッダーを設定し、trustedOrigins が一致することを確認します。
import { cors } from "hono/cors";
// CRITICAL: Both must match frontend origin exactly
app.use(
"/api/auth/*",
cors({
origin: "http://localhost:5173", // Frontend URL (no trailing slash)
credentials: true, // Allow cookies
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
})
);
// And in better-auth config
export const auth = betterAuth({
trustedOrigins: ["http://localhost:5173"], // Same as CORS origin
// ...
});
よくある間違い:
- オリジン URL の入力ミス(末尾のスラッシュ、http と https の違い、ポート番号の誤り)
- CORS 設定と
trustedOriginsのオリジンが一致していない - CORS ミドルウェアが認証ルートの後に登録されている(前に登録する必要があります)
問題 6:OAuth リダイレクト URI の不一致
問題:「redirect_uri_mismatch」エラーが発生し、ソーシャルサインインに失敗します。
症状:ユーザーの同意後に Google/GitHub OAuth がエラーを返します。
解決策:OAuth プロバイダーの設定で、完全に一致していることを確認します。
Provider setting: https://yourdomain.com/api/auth/callback/google
better-auth URL: https://yourdomain.com/api/auth/callback/google
❌ Wrong: http vs https, trailing slash, subdomain mismatch
✅ Right: Exact character-for-character match
better-auth のコールバック URL を確認します:
// It's always: {baseURL}/api/auth/callback/{provider}
const callbackURL = `${env.BETTER_AUTH_URL}/api/auth/callback/google`;
console.log("Configure this URL in Google Console:", callbackURL);
問題 7:依存関係の不足
問題:パッケージが見つからないという TypeScript エラーまたは実行時エラーが発生します。
症状:Cannot find module 'drizzle-orm' などのエラーが表示されます。
解決策:必要なパッケージをすべてインストールします。
Drizzle を使用する場合:
npm install better-auth drizzle-orm drizzle-kit @cloudflare/workers-types
Kysely を使用する場合:
npm install better-auth kysely kysely-d1 @cloudflare/workers-types
問題 8:メール確認メールが送信されない
問題:メール確認リンクが届きません。
症状:ユーザーが登録しても、メールが届きません。
解決策:sendVerificationEmail ハンドラーを実装します。
export const auth = betterAuth({
database: /* ... */,
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},
emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
// Use your email service (SendGrid, Resend, etc.)
await sendEmail({
to: user.email,
subject: "Verify your email",
html: `
<p>Click the link below to verify your email:</p>
<a href="${url}">Verify Email</a>
`,
});
},
sendOnSignUp: true,
autoSignInAfterVerification: true,
expiresIn: 3600, // 1 hour
},
});
Cloudflare の場合:Cloudflare Email Routing または外部サービス(Resend、SendGrid)を使用します。
問題 9:セッションの有効期限が短すぎる
問題:セッションの有効期限が予期せず切れる、または切れません。
症状:ユーザーが予期せずログアウトされたり、ログアウト後もセッションが継続したりします。
解決策:セッションの有効期限を設定します。
export const auth = betterAuth({
database: /* ... */,
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days (in seconds)
updateAge: 60 * 60 * 24, // Update session every 24 hours
},
});
Issue 10:ソーシャルプロバイダーでユーザーデータが不足する
問題:ソーシャルサインインは成功しますが、ユーザーデータ(名前、アバター)が不足しています。
症状:Google/GitHub でサインインした後、session.user.name が null になります。
解決策:追加のスコープをリクエストします。
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
scope: ["openid", "email", "profile"], // Include 'profile' for name/image
},
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
scope: ["user:email", "read:user"], // 'read:user' for full profile
},
}
Issue 11:Drizzle スキーマで TypeScript エラーが発生する
問題:TypeScript がスキーマの型についてエラーを表示します。
症状:Type 'DrizzleD1Database' is not assignable to...
解決策:データベースから適切な型をエクスポートします。
// src/db/index.ts
import { drizzle, type DrizzleD1Database } from "drizzle-orm/d1";
import * as schema from "./schema";
export type Database = DrizzleD1Database<typeof schema>;
export function createDatabase(d1: D1Database): Database {
return drizzle(d1, { schema });
}
Issue 12:Wrangler の開発モードが動作しない
問題:データベースエラーにより wrangler dev が失敗します。
症状:ローカル開発中に「Database not found」またはマイグレーションエラーが発生します。
解決策:まずローカルでマイグレーションを適用します。
# Apply migrations to local D1
wrangler d1 migrations apply my-app-db --local
# Then run dev server
wrangler dev
Issue 13:ユーザーデータの更新が UI に反映されない(TanStack Query 使用時)
問題:ユーザーデータ(アバター、名前など)を更新した後、queryClient.invalidateQueries() を呼び出しても、変更が useSession() に反映されません。
症状:更新が成功した後も、アバター画像やユーザープロフィールのデータが古いまま表示されます。TanStack Query のキャッシュには更新後のデータが表示されますが、better-auth のセッションには古い値が表示されます。
根本原因:better-auth はセッション状態の管理に TanStack Query ではなく、nanostores を使用しています。queryClient.invalidateQueries() を呼び出しても、無効化されるのは React Query のキャッシュのみで、better-auth の nanostore は無効化されません。
解決策:ユーザーデータの更新後、nanostore に手動で通知します。
// Update user data
const { data, error } = await authClient.updateUser({
image: newAvatarUrl,
name: newName
})
if (!error) {
// Manually invalidate better-auth session state
authClient.$store.notify('$sessionSignal')
// Optional: Also invalidate React Query if using it for other data
queryClient.invalidateQueries({ queryKey: ['user-profile'] })
}
使用するケース:
- better-auth と TanStack Query を併用する場合
- ユーザープロフィールのフィールド(名前、画像、メールアドレス)を更新する場合
- クライアント側でセッションのユーザーデータを変更する操作全般
代替方法:useSession() の refetch() を呼び出す方法もありますが、$store.notify() のほうが直接的です。
const { data: session, refetch } = authClient.useSession()
// After update
await refetch()
注:$store はドキュメントに記載されていない内部 API です。この方法は本番環境で検証済みですが、今後の better-auth のバージョンで変更される可能性があります。
出典:コミュニティで見つかった方法で、本番環境での使用を確認済み
Issue 14:apiKey テーブルのスキーマが D1 と一致しない
問題:D1 を使用すると、better-auth CLI(npx @better-auth/cli generate)が「Failed to initialize database adapter」で失敗します。
症状:CLI が D1 に接続してスキーマを調査できません。CLI 経由でマイグレーションを実行することもできません。
根本原因:CLI は直接の SQLite 接続を想定していますが、D1 には Cloudflare のバインディング API が必要です。
解決策:CLI を使用せず、ドキュメントに記載された apiKey スキーマを使ってマイグレーションを手動で作成します。
CREATE TABLE api_key (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
name TEXT,
start TEXT,
prefix TEXT,
key TEXT NOT NULL,
enabled INTEGER DEFAULT 1,
rate_limit_enabled INTEGER,
rate_limit_time_window INTEGER,
rate_limit_max INTEGER,
request_count INTEGER DEFAULT 0,
last_request INTEGER,
remaining INTEGER,
refill_interval INTEGER,
refill_amount INTEGER,
last_refill_at INTEGER,
expires_at INTEGER,
permissions TEXT,
metadata TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
重要なポイント:
- テーブルには正確に 21 個のカラムがあります(better-auth v1.4 以降)
- カラム名には
snake_caseを使用します(例:rateLimitTimeWindowではなくrate_limit_time_window) - D1 は
ALTER TABLE DROP COLUMNをサポートしていません。スキーマにずれが生じた場合は、新規マイグレーションのパターン(テーブルを削除して再作成)を使用します - Drizzle アダプターの設定では、テーブル名のマッピングに
apikey(小文字)を使用します
D1 の新規マイグレーションパターン:
-- Drop in reverse dependency order
DROP TABLE IF EXISTS api_key;
DROP TABLE IF EXISTS session;
-- ... other tables
-- Recreate with clean schema
CREATE TABLE api_key (...);
出典:D1 と better-auth apiKey プラグインを使った本番環境でのデバッグ
Issue 15:Admin プラグインには DB 内のロールが必要(2 段階の認証)
問題:ミドルウェアを通過しているにもかかわらず、listUsers などの Admin プラグインのメソッドが「You are not allowed to list users」で失敗します。
症状:カスタムの requireAdmin ミドルウェア(ADMIN_EMAILS 環境変数をチェック)が通過しても、auth.api.listUsers() は 403 を返します。
根本原因:better-auth の Admin プラグインには、2 つの認可レイヤーがあります。
- ユーザー側のミドルウェア - カスタムチェック(例:ADMIN_EMAILS)
- better-auth 内部 - データベースの
user.role === 'admin'をチェック
Admin プラグインのメソッドを動作させるには、両方のチェックを通過する必要があります。
解決策:データベースでユーザーのロールを「admin」に設定します。
-- Fix for existing users
UPDATE user SET role = 'admin' WHERE email = '[email protected]';
または、初期設定後に Admin UI/API を使ってロールを設定します。
理由:Admin プラグインの listUsers、banUser、impersonateUser などはすべて、ユーザー独自のミドルウェアのロジックではなく、データベース内の user.role をチェックします。
出典:本番環境でのデバッグ。誤解を招くエラーメッセージをきっかけに、wrangler tail で根本原因を特定
Issue 16:Organization/Team の updated_at は NULL を許可する必要があります
問題:API が「slug はすでに存在します」と返すにもかかわらず、Organization の作成が SQL 制約エラーで失敗します。
症状:
- エラーメッセージに「この slug を持つ Organization はすでに存在します」と表示されます
- データベースのテーブルは実際には空です
wrangler tailに次のように表示されます:Failed query: insert into "organization" ... values (?, ?, ?, null, null, ?, null)
根本原因:better-auth は作成時に updated_at に null を挿入します(更新時のみ値を設定します)。スキーマに NOT NULL 制約がある場合、挿入に失敗します。
解決策:スキーマとマイグレーションの両方で updated_at を NULL 許可にします:
// Drizzle schema - CORRECT
export const organization = sqliteTable('organization', {
// ...
updatedAt: integer('updated_at', { mode: 'timestamp' }), // No .notNull()
});
export const team = sqliteTable('team', {
// ...
updatedAt: integer('updated_at', { mode: 'timestamp' }), // No .notNull()
});
-- Migration - CORRECT
CREATE TABLE organization (
-- ...
updated_at INTEGER -- No NOT NULL
);
該当箇所:organization と team のテーブル(他のプラグインのテーブルも該当する可能性があります)
出典:本番環境でのデバッグ。wrangler tail により、「slug はすでに存在します」という誤解を招くメッセージの裏にある実際の SQL エラーが判明
Issue 17:API レスポンスの二重ネスト(listMembers など)
問題:カスタム API エンドポイントが { members: { members: [...], total: N } } のように、データを二重にネストして返します。
症状:データが存在するにもかかわらず、UI の件数が「undefined」と表示され、リストが空になります。
根本原因:listMembers のような better-auth のメソッドは { members: [...], total: N } を返します。c.json({ members: result }) でラップすると、二重にネストされます。
解決策:better-auth のレスポンスから配列を取り出します:
// ❌ WRONG - Double nesting
const result = await auth.api.listMembers({ ... });
return c.json({ members: result });
// Returns: { members: { members: [...], total: N } }
// ✅ CORRECT - Extract array
const result = await auth.api.listMembers({ ... });
const members = result?.members || [];
return c.json({ members });
// Returns: { members: [...] }
影響を受けるメソッド(配列ではなくオブジェクトを返します):
listMembers→{ members: [...], total: N }listUsers→{ users: [...], total: N, limit: N }listOrganizations→{ organizations: [...] }(構造を確認してください)listInvitations→{ invitations: [...] }
パターン:API レスポンスでラップする前に、better-auth メソッドの戻り値の型を必ず確認してください。
出典:本番環境でのデバッグ。UI の件数が「undefined」と表示され、API を調査したところネストの問題が判明
Issue 18:Expo クライアントで fromJSONSchema のクラッシュ(v1.4.16)
問題:v1.4.16 で @better-auth/expo/client から expoClient をインポートすると、TypeError: Cannot read property 'fromJSONSchema' of undefined が発生してクラッシュします。
症状:React Native/Expo アプリで expoClient をインポートすると、すぐに実行時クラッシュが発生します。
根本原因:PR #6933(Expo の Cookie ベースの OAuth state 修正)の後に導入されたリグレッションです。f4a9f15 以降の3つのコミットのうち、いずれかがビルドを壊しました。
解決策:
- 一時的な対応:リグレッション発生前のコミット
f4a9f15の継続的ビルドを使用します - 恒久的な対応:修正を待ちます(2026-01-20 時点で issue #7491 は未解決)
// Crashes on v1.4.16
import { expoClient } from '@better-auth/expo/client'
// Workaround: Use continuous build at f4a9f15
// Or wait for fix in next release
Issue 19:additionalFields の string[] が JSON 文字列を返す
問題:v1.4.12 以降、Drizzle で直接クエリした場合、type: 'string[]' の additionalFields はネイティブ配列ではなく、文字列化された配列('["a","b"]')を返します。
症状:user.notificationTokens が配列ではなく文字列になります。配列を想定したコードが動作しなくなります。
根本原因:Drizzle アダプターでは、string[] フィールドは配列を想定する mode: 'json' を使って保存されます。しかし、better-auth v1.4.4 以降は Drizzle に文字列を渡すため、二重に文字列化されます。Drizzle で直接クエリすると値は文字列ですが、better-auth の internalAdapter を使用すると、トランスフォーマーによって正しく配列が返されます。
解決策:
- Drizzle に直接クエリする代わりに、better-auth の
internalAdapterを使用します(トランスフォーマーがあります) string[]フィールド用に Drizzle スキーマを.jsonb()に変更します- 修正されるまで JSON 文字列を手動で解析します
// Config
additionalFields: {
notificationTokens: {
type: 'string[]',
required: true,
input: true,
},
}
// Create user
notificationTokens: ['token1', 'token2']
// Result in DB (when querying via Drizzle directly)
// '["token1","token2"]' (string, not array)
Issue 20: additionalFields の "returned" プロパティが入力をブロックする
問題: additionalFields に returned: false を設定すると、input: true であっても、API 経由でフィールドを保存できなくなります。
症状: API エンドポイント経由で作成または更新しても、フィールドがデータベースに保存されません。
根本原因: returned: false プロパティは、意図されている読み取りだけでなく、読み取りと書き込みの両方の操作をブロックします。input: true プロパティは、書き込みアクセスを独立して制御する必要があります。
解決策:
- API からの書き込みアクセスが必要な場合は、
returned: falseを使用しないでください - 代わりにサーバー側のメソッド(
auth.api.*)経由で書き込んでください
// Organization plugin config
additionalFields: {
secretField: {
type: 'string',
required: true,
input: true, // Should allow API writes
returned: false, // Should only block reads, but blocks writes too
},
}
// API request to create organization
// secretField is never saved to database
Issue 21: freshAge はアクティビティではなく作成時刻に基づく
問題: session.freshAge が確認するのは、最近のアクティビティではなく作成からの経過時間です。アクティブなセッションでも、頻繁に使用されている場合であっても、freshAge が経過すると「新鮮ではない」と判定されます。
症状: 「新鮮なセッションが必要」なエンドポイントが、有効でアクティブなセッションを拒否します。
発生する理由: freshSessionMiddleware は Date.now() - (session.updatedAt || session.createdAt) を確認しますが、updatedAt が更新されるのは、updateAge に基づいてセッションが更新されたときだけです。updateAge > freshAge の場合、updatedAt が更新される前にセッションが「新鮮ではない」状態になります。
解決策:
updateAge <= freshAgeに設定し、期限切れ前に新鮮さが更新されるようにします- 長期間有効なセッションでは、「新鮮なセッションが必要」という制限を避けてください
- 仕様として受け入れてください:freshAge は厳密に作成からの経過時間です(メンテナー確認済み)
// Config
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
freshAge: 60 * 60 * 24, // 24 hours
updateAge: 60 * 60 * 24 * 3, // 3 days (> freshAge!) ⚠️ PROBLEM
// CORRECT - updateAge <= freshAge
updateAge: 60 * 60 * 12, // 12 hours (< freshAge)
}
// Timeline with bad config:
// T+0h: User signs in (createdAt = now)
// T+12h: User makes requests (session active, still fresh)
// T+25h: User makes request (session active, BUT NOT FRESH - freshAge elapsed)
// Result: "Fresh session required" endpoints reject active session
Issue 22: OAuth トークンエンドポイントがラップされた JSON を返す
問題: OAuth 2.1 および OIDC のトークンエンドポイントは、仕様に準拠したトップレベルの JSON ではなく、{ "response": { ...tokens... } } を返します。OAuth クライアントはルートで { "access_token": "...", "token_type": "bearer" } が返されることを想定しています。
症状: OAuth クライアントが Bearer undefined または invalid_token で失敗します。
根本原因: エンドポイントのパイプラインは内部利用向けに { response, headers, status } を返しますが、HTTP リクエストではこれがそのままシリアライズされます。このため、OAuth/OIDC の仕様要件を満たせません。
解決策:
- 一時的な対処: クライアント側で
.responseフィールドを手動でアンラップします - 恒久的な対処: 修正を待ちます(issue #7355 は対応中で、コントリビューションを受け付けています)
// Expected (spec-compliant)
{ "access_token": "...", "token_type": "bearer", "expires_in": 3600 }
// Actual (wrapped)
{ "response": { "access_token": "...", "token_type": "bearer", "expires_in": 3600 } }
// Result: OAuth clients fail to parse, send `Bearer undefined`
移行ガイド
Clerk からの移行
主な違い:
- Clerk:サードパーティサービス → better-auth:セルフホスト可能
- Clerk:プロプライエタリ → better-auth:オープンソース
- Clerk:月額費用あり → better-auth:無料
移行手順:
- Clerk からユーザーデータをエクスポートします(CSV または API)
- better-auth のデータベースにインポートします:
// migration script const clerkUsers = await fetchClerkUsers(); for (const clerkUser of clerkUsers) { await db.insert(user).values({ id: clerkUser.id, email: clerkUser.email, emailVerified: clerkUser.email_verified, name: clerkUser.first_name + " " + clerkUser.last_name, image: clerkUser.profile_image_url, }); } - Clerk SDK を better-auth クライアントに置き換えます:
// Before (Clerk) import { useUser } from "@clerk/nextjs"; const { user } = useUser(); // After (better-auth) import { authClient } from "@/lib/auth-client"; const { data: session } = authClient.useSession(); const user = session?.user; - セッション検証用にミドルウェアを更新します
- ソーシャルプロバイダーを設定します(同じ OAuth アプリを使用し、設定は異なります)
Auth.js (NextAuth) からの移行
主な違い:
- Auth.js:機能が限定的 → better-auth:包括的(2FA、組織など)
- Auth.js:コールバック中心 → better-auth:プラグインベース
- Auth.js:セッション処理に一貫性がない → better-auth:一貫性がある
移行手順:
- データベーススキーマ:Auth.js と better-auth は類似したスキーマを使用しますが、カラム名が異なります
- 設定を置き換えます:
// Before (Auth.js) import NextAuth from "next-auth"; import GoogleProvider from "next-auth/providers/google"; export default NextAuth({ providers: [GoogleProvider({ /* ... */ })], }); // After (better-auth) import { betterAuth } from "better-auth"; export const auth = betterAuth({ socialProviders: { google: { /* ... */ }, }, }); - クライアントフックを更新します:
// Before import { useSession } from "next-auth/react"; // After import { authClient } from "@/lib/auth-client"; const { data: session } = authClient.useSession();
追加リソース
公式ドキュメント
- ホームページ: https://better-auth.com
- はじめに: https://www.better-auth.com/docs/introduction
- インストール: https://www.better-auth.com/docs/installation
- 基本的な使い方: https://www.better-auth.com/docs/basic-usage
コアコンセプト
- セッション管理: https://www.better-auth.com/docs/concepts/session-management
- ユーザーとアカウント: https://www.better-auth.com/docs/concepts/users-accounts
- クライアント SDK: https://www.better-auth.com/docs/concepts/client
- プラグインシステム: https://www.better-auth.com/docs/concepts/plugins
認証方法
- メールとパスワード: https://www.better-auth.com/docs/authentication/email-password
- OAuth プロバイダー: https://www.better-auth.com/docs/concepts/oauth
プラグインのドキュメント
コアプラグイン:
- 2FA (二要素認証): https://www.better-auth.com/docs/plugins/2fa
- Organization: https://www.better-auth.com/docs/plugins/organization
- Admin: https://www.better-auth.com/docs/plugins/admin
- マルチセッション: https://www.better-auth.com/docs/plugins/multi-session
- API キー: https://www.better-auth.com/docs/plugins/api-key
- 汎用 OAuth: https://www.better-auth.com/docs/plugins/generic-oauth
パスワードレスプラグイン:
- パスキー: https://www.better-auth.com/docs/plugins/passkey
- マジックリンク: https://www.better-auth.com/docs/plugins/magic-link
- メール OTP: https://www.better-auth.com/docs/plugins/email-otp
- 電話番号: https://www.better-auth.com/docs/plugins/phone-number
- 匿名: https://www.better-auth.com/docs/plugins/anonymous
高度なプラグイン:
- ユーザー名: https://www.better-auth.com/docs/plugins/username
- JWT: https://www.better-auth.com/docs/plugins/jwt
- OpenAPI: https://www.better-auth.com/docs/plugins/open-api
- OIDC プロバイダー: https://www.better-auth.com/docs/plugins/oidc-provider
- SSO: https://www.better-auth.com/docs/plugins/sso
- Stripe: https://www.better-auth.com/docs/plugins/stripe
- MCP: https://www.better-auth.com/docs/plugins/mcp
フレームワーク統合
- TanStack Start: https://www.better-auth.com/docs/integrations/tanstack
- Expo (React Native): https://www.better-auth.com/docs/integrations/expo
コミュニティとサポート
- GitHub: https://github.com/better-auth/better-auth (22.4k ⭐)
- Examples: https://github.com/better-auth/better-auth/tree/main/examples
- Discord: https://discord.gg/better-auth
- Changelog: https://github.com/better-auth/better-auth/releases
関連ドキュメント
- Drizzle ORM: https://orm.drizzle.team/docs/get-started-sqlite
- Kysely: https://kysely.dev/
本番環境の例
動作確認済みの D1 リポジトリ (すべて Drizzle または Kysely を使用):
- zpg6/better-auth-cloudflare - Drizzle + D1 (CLI を含む)
- zwily/example-react-router-cloudflare-d1-drizzle-better-auth - Drizzle + D1
- foxlau/react-router-v7-better-auth - Drizzle + D1
- matthewlynch/better-auth-react-router-cloudflare-d1 - Kysely + D1
直接 d1Adapter を使用しているものは ありません - すべて Drizzle/Kysely が必要です。
バージョン互換性
テスト済み:
[email protected][email protected][email protected][email protected][email protected]@cloudflare/workers-types@latest[email protected]- Node.js 18+、Bun 1.0+
破壊的変更:
- v1.4.6:
allowImpersonatingAdminsのデフォルトがfalseに - v1.4.0: ESM のみ (CommonJS なし)
- v1.3.0: マルチチームのテーブル構造の変更
変更履歴を確認: https://github.com/better-auth/better-auth/releases
コミュニティリソース
Cloudflare 固有のガイド:
- zpg6/better-auth-cloudflare - Drizzle + D1 のリファレンス
- Hono + better-auth on Cloudflare - Hono の公式サンプル
- React Router + Cloudflare D1 - React Router v7 ガイド
- SvelteKit + Cloudflare D1 - SvelteKit ガイド
トークン効率:
- スキルなし:~35,000 トークン(D1 アダプターのエラー、15 以上のプラグイン、レート制限、セッションキャッシュ、データベースフック、モバイル統合)
- スキルあり:~8,000 トークン(エラー + パターン + すべてのプラグイン + API リファレンスに重点)
- 削減率:~77%(~27,000 トークン)
防止したエラー:解決策を明記した 22 件の問題 主な価値:D1 アダプターの要件、nodejs_compat フラグ、OAuth 2.1 プロバイダー、Bearer/OneTap/SCIM/Anonymous プラグイン、レート制限、セッションキャッシュ、データベースフック、Expo 統合、80 以上のエンドポイントリファレンス、additionalFields のバグ、freshAge の動作、OAuth トークンのラッピング
最終確認日:2026-01-21 | スキルのバージョン:5.1.0 | 変更点:トレーニングデータの期限後の調査から新たに 5 件の問題を追加(Expo の fromJSONSchema クラッシュ、additionalFields の string[] バグ、additionalFields の返却プロパティのバグ、freshAge はアクティビティ基準ではない、OAuth トークンのラッピング)。Kysely CamelCasePlugin の JOIN 解析失敗について Issue #3 を拡充。Hono の CORS パターンについて Issue #5 を拡充。Cloudflare Workers の DB バインディングの制約に関する注記を追加。TanStack Start のセッションの null 許容パターンを追加。v1.4.16 に更新。