safe-action-better-auth について
safe-action-better-authは、next-safe-actionクライアントにBetter Authのセッション認証を追加するアダプターです。リクエストからセッションを取得して未認証の要求を拒否し、型推論された認証コンテキストをアクションに渡します。
主な機能
- 01リクエストヘッダーからセッション取得
- 02未認証リクエストの拒否
- 03名前空間付き認証コンテキストの追加
- 04ユーザーとセッション型の推論
- 05カスタム認可コールバック対応
活用シーン
- 01Better AuthによるNext.js Server Actionの保護
- 02アクションハンドラーで型付きセッション情報を参照
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ next-safe-action/ skills --skill safe-action-better-authインストール後、Claude Code で/safe-action-better-authを入力して読み込まれたか確認します。
同梱ファイルauthorize-patterns.md
詳細
next-safe-action Better Auth アダプター
インストール
npm install @next-safe-action/adapter-better-auth better-auth
インポート
import { betterAuth } from "@next-safe-action/adapter-better-auth";
クイックスタート
1. Better Auth を設定する
Better Auth のサーバーインスタンスを作成します。アクションで Cookie を設定する必要がある場合(例:signInEmail、signUpEmail)は、nextCookies() プラグインを追加します。
// src/lib/auth.ts
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
// ...your config (database, plugins, etc.)
plugins: [
// ...other plugins
nextCookies(), // must be the last plugin in the array
],
});
2. Next.js で認証割り込みを有効にする
デフォルトの動作では next/navigation の unauthorized() を使用するため、次のフラグが必要です。
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
authInterrupts: true,
},
};
export default nextConfig;
3. 認証済みのアクションクライアントを作成する
// src/lib/safe-action.ts
import { createSafeActionClient } from "next-safe-action";
import { betterAuth } from "@next-safe-action/adapter-better-auth";
import { auth } from "./auth";
// Public action client (no auth required)
export const actionClient = createSafeActionClient();
// Authenticated action client
export const authClient = actionClient.use(betterAuth(auth));
4. アクションで使用する
// src/app/actions.ts
"use server";
import { z } from "zod";
import { authClient } from "@/lib/safe-action";
export const updateProfile = authClient
.inputSchema(z.object({ name: z.string().min(1) }))
.action(async ({ parsedInput, ctx }) => {
// ctx.auth.user and ctx.auth.session are fully typed,
// including fields from Better Auth plugins
const userId = ctx.auth.user.id;
await db.user.update({
where: { id: userId },
data: { name: parsedInput.name },
});
return { success: true };
});
動作の仕組み
betterAuth() は、セーフアクションクライアントの .use() チェーンに対する事前検証ミドルウェアを作成します。
next/headersのリクエストヘッダーを使用してauth.api.getSession({ headers: await headers() })を呼び出し、セッションを取得します- セッションが存在しない場合、
next/navigationのunauthorized()を呼び出して未認証のリクエストをブロックします { auth: { user, session } }をnext()に渡し、アクションコンテキストに統合することで、型付きコンテキストを注入します
コンテキストは、他のミドルウェアのコンテキストプロパティとの衝突を避けるため、auth の下に名前空間化されています。
型推論
ミドルウェアは、プラグインで追加されたフィールドを含め、Better Auth インスタンスから正確な user 型と session 型を推論します。たとえば、organization プラグインを使用している場合、ctx.auth.session には activeOrganizationId が含まれます。手動で型注釈を指定する必要はありません。
エントリーポイント
| エントリーポイント | エクスポート | 環境 |
|---|---|---|
@next-safe-action/adapter-better-auth | betterAuth、型 | サーバー |
エクスポートされる型
| 型 | 説明 |
|---|---|
BetterAuthContext<O> | ミドルウェアによって追加されるコンテキストの形式です:{ auth: { user, session } }。型は Auth<O>["$Infer"]["Session"] を介して Better Auth インスタンスから推論されます。 |
AuthorizeFn<O, NC, Ctx> | authorize コールバックのシグネチャです。{ authData, ctx, next } を受け取ります。 |
BetterAuthOpts<O, NC, Ctx> | betterAuth() のオプションオブジェクトの型です。省略可能な authorize コールバックを含みます。 |
手動の認証ミドルウェアとの比較
Better Auth を使用している場合は、手動で認証ミドルウェアを書くよりも betterAuth(auth) を使用してください。このアダプターは、セッションの取得、Cookie の統合、型付け、未認証リクエストの拒否を自動で処理します。
// Manual — don't do this if you have @next-safe-action/adapter-better-auth installed
const authClient = actionClient.use(async ({ next }) => {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
throw new Error("Unauthorized");
}
return next({ ctx: { userId: session.user.id } });
});
// With adapter — do this instead
const authClient = actionClient.use(betterAuth(auth));
// ctx.auth.user and ctx.auth.session are fully typed automatically
関連ドキュメント
アンチパターン
// BAD: Missing nextCookies() plugin — cookies won't be set in Server Actions
// Session will silently be null when actions try to set cookies
import { betterAuth } from "better-auth";
export const auth = betterAuth({
plugins: [/* no nextCookies() */],
});
// GOOD: Add nextCookies() as the last plugin
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
plugins: [
// ...other plugins
nextCookies(), // must be last
],
});
// BAD: Missing authInterrupts flag — unauthorized() will throw a runtime error
// next.config.ts
const nextConfig: NextConfig = {};
// GOOD: Enable authInterrupts
const nextConfig: NextConfig = {
experimental: {
authInterrupts: true,
},
};
// BAD: Re-fetching session inside authorize — it's already pre-fetched as authData
actionClient.use(
betterAuth(auth, {
authorize: async ({ next }) => {
const session = await auth.api.getSession({ headers: await headers() }); // Redundant!
if (!session || session.user.role !== "admin") {
unauthorized();
}
return next({ ctx: { auth: session } });
},
})
);
// GOOD: Use the pre-fetched authData directly
actionClient.use(
betterAuth(auth, {
authorize: ({ authData, next }) => {
if (!authData || authData.user.role !== "admin") {
unauthorized();
}
return next({ ctx: { auth: authData } });
},
})
);
// BAD: Writing manual Better Auth middleware when the adapter is installed
import { auth } from "./auth";
const authClient = actionClient.use(async ({ next }) => {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) throw new Error("Unauthorized");
return next({ ctx: { user: session.user } });
});
// GOOD: Use the adapter — handles typing, cookies, and unauthorized() automatically
import { betterAuth } from "@next-safe-action/adapter-better-auth";
import { auth } from "./auth";
const authClient = actionClient.use(betterAuth(auth));