neon-auth-nextjs について
neon-auth-nextjsは、データベースを使わずにNext.js App Routerへ@neondatabase/authを導入する開発者向けセットアップスキルです。サーバー・クライアント設定、ルート保護、認証UIに加え、インポート方法とセッション更新時の注意点を扱います。
主な機能
- 01サーバーとクライアントの認証設定
- 02ルートハンドラーとミドルウェア
- 03認証UIコンポーネント
- 04Server Componentsでのセッション処理
- 05CSSとテーマの設定
活用シーン
- 01App RouterアプリへのNeon Auth導入
- 02ダッシュボードとアカウントのルート保護
- 03サインインとアカウントページの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ neondatabase/ neon-js --skill neon-auth-nextjsインストール後、Claude Code で/neon-auth-nextjsを入力して読み込まれたか確認します。
詳細
Next.js 向け Neon Auth
開発者が Next.js App Router アプリケーションに @neondatabase/auth をセットアップできるよう支援します(認証のみ、データベースは不要です)。
使用するタイミング
次の場合にこのスキルを使用します:
- Next.js(App Router)で Neon Auth をセットアップするとき
- ユーザーが Neon Auth とあわせて「next.js」、「next」、「app router」に言及したとき
- 認証のみのセットアップ(データベースが不要な場合)
重要なルール
- サーバーとクライアントのインポート:正しいインポートパスを使用してください
'use client'ディレクティブ:フックを使用するクライアントコンポーネントでは必須です- CSS のインポート:
/ui/cssまたは/ui/tailwindのどちらか一方を選び、両方は使用しないでください - onSessionChange:サーバーコンポーネントを更新するため、必ず
router.refresh()を呼び出してください
重要なインポート
| 用途 | インポート元 |
|---|---|
統合サーバー(createNeonAuth) | @neondatabase/auth/next/server |
| クライアント認証 | @neondatabase/auth/next |
| UI コンポーネント | @neondatabase/auth/react/ui |
| ビューパス(静的パラメーター) | @neondatabase/auth/react/ui/server |
注:@neondatabase/auth/next/server の createNeonAuth() を使って、次の機能を提供する統合 auth インスタンスを取得します:
.handler()- API ルートハンドラー.middleware()- ルート保護ミドルウェア- Better Auth のすべてのサーバーメソッド(
.signIn、.signUp、.getSessionなど)
セットアップ
1. インストール
npm install @neondatabase/auth
2. 環境変数(.env.local)
NEON_AUTH_BASE_URL=https://your-auth.neon.tech
NEON_AUTH_COOKIE_SECRET=your-secret-at-least-32-characters-long
重要:本番環境用に、安全なシークレット(32 文字以上)を生成してください:
openssl rand -base64 32
3. サーバーのセットアップ(lib/auth/server.ts)
ハンドラー、ミドルウェア、サーバーメソッドを提供する auth インスタンスを作成します:
import { createNeonAuth } from '@neondatabase/auth/next/server';
export const auth = createNeonAuth({
baseUrl: process.env.NEON_AUTH_BASE_URL!,
cookies: {
secret: process.env.NEON_AUTH_COOKIE_SECRET!,
sessionDataTtl: 300, // Optional: session data cache TTL in seconds (default: 300 = 5 min)
domain: '.example.com', // Optional: for cross-subdomain cookies
},
});
4. API ルート(app/api/auth/[...path]/route.ts)
import { auth } from '@/lib/auth/server';
export const { GET, POST } = auth.handler();
5. ミドルウェア(middleware.ts)
import { auth } from '@/lib/auth/server';
export default auth.middleware({
loginUrl: '/auth/sign-in',
});
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
};
6. クライアント(lib/auth/client.ts)
'use client';
import { createAuthClient } from '@neondatabase/auth/next';
export const authClient = createAuthClient();
7. プロバイダー(app/providers.tsx)
'use client';
import { NeonAuthUIProvider } from '@neondatabase/auth/react/ui';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { authClient } from '@/lib/auth/client';
export function Providers({ children }: { children: React.ReactNode }) {
const router = useRouter();
return (
<NeonAuthUIProvider
authClient={authClient}
navigate={router.push}
replace={router.replace}
onSessionChange={() => router.refresh()}
redirectTo="/dashboard"
Link={({href, children}) => <Link to={href}>{children}</Link>}
>
{children}
</NeonAuthUIProvider>
);
}
8. レイアウト(app/layout.tsx)
import { Providers } from './providers';
import '@neondatabase/auth/ui/css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
9. 認証ページ(app/auth/[path]/page.tsx)
import { AuthView } from '@neondatabase/auth/react/ui';
import { authViewPaths } from '@neondatabase/auth/react/ui/server';
export function generateStaticParams() {
return Object.values(authViewPaths).map((path) => ({ path }));
}
export default async function AuthPage({ params }: { params: Promise<{ path: string }> }) {
const { path } = await params;
return <AuthView pathname={path} />;
}
CSS とスタイル設定
インポートオプション
Tailwind なし(ビルド済み CSS バンドル:約 47KB):
// app/layout.tsx
import '@neondatabase/auth/ui/css';
Tailwind CSS v4 を使用(app/globals.css):
@import 'tailwindcss';
@import '@neondatabase/auth/ui/tailwind';
重要:両方をインポートしないでください。スタイルが重複します。
ダークモード
プロバイダーには next-themes が含まれています。defaultTheme プロパティで制御します:
<NeonAuthUIProvider
defaultTheme="system" // 'light' | 'dark' | 'system'
// ...
>
カスタムテーマ設定
globals.css で CSS 変数を上書きします:
:root {
--primary: hsl(221.2 83.2% 53.3%);
--primary-foreground: hsl(210 40% 98%);
--background: hsl(0 0% 100%);
--foreground: hsl(222.2 84% 4.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(222.2 84% 4.9%);
--border: hsl(214.3 31.8% 91.4%);
--input: hsl(214.3 31.8% 91.4%);
--ring: hsl(221.2 83.2% 53.3%);
--radius: 0.5rem;
}
.dark {
--background: hsl(222.2 84% 4.9%);
--foreground: hsl(210 40% 98%);
/* ... dark mode overrides */
}
NeonAuthUIProvider のプロパティ
設定オプションの全一覧:
<NeonAuthUIProvider
// Required
authClient={authClient}
// Navigation (Next.js specific)
navigate={router.push} // router.push for navigation
replace={router.replace} // router.replace for redirects
onSessionChange={() => router.refresh()} // Refresh Server Components!
redirectTo="/dashboard" // Where to redirect after auth
Link={({href, children}) => <Link to={href}>{children}</Link>} // Next.js Link component
// Social/OAuth Providers
social={{
providers: ['google'],
}}
// Feature Flags
emailOTP={true} // Enable email OTP sign-in
emailVerification={true} // Require email verification
magicLink={false} // Magic link (disabled by default)
multiSession={false} // Multiple sessions (disabled)
// Credentials Configuration
credentials={{
forgotPassword: true, // Show forgot password link
}}
// Sign Up Fields
signUp={{
fields: ['name'], // Additional fields: 'name', 'username', etc.
}}
// Account Settings Fields
account={{
fields: ['image', 'name', 'company', 'age', 'newsletter'],
}}
// Organization Features
organization={{}} // Enable org features
// Dark Mode
defaultTheme="system" // 'light' | 'dark' | 'system'
// Custom Labels
localization={{
SIGN_IN: 'Welcome Back',
SIGN_UP: 'Create Account',
FORGOT_PASSWORD: 'Forgot Password?',
OR_CONTINUE_WITH: 'or continue with',
}}
>
{children}
</NeonAuthUIProvider>
サーバーコンポーネント(RSC)
サーバーコンポーネントでセッションを取得する
// NO 'use client' - this is a Server Component
import { auth } from '@/lib/auth/server';
// Server components using `auth` methods must be rendered dynamically
export const dynamic = 'force-dynamic'
export async function Profile() {
const { data: session } = await auth.getSession();
if (!session?.user) return <div>Not signed in</div>;
return (
<div>
<p>Hello, {session.user.name}</p>
<p>Email: {session.user.email}</p>
</div>
);
}
認証付きルートハンドラー
// app/api/user/route.ts
import { auth } from '@/lib/auth/server';
import { NextResponse } from 'next/server';
export async function GET() {
const { data: session } = await auth.getSession();
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.json({ user: session.user });
}
サーバーアクション
サーバーアクションでは、lib/auth/server.ts の同じ auth インスタンスを使用します:
サインインアクション
// app/actions/auth.ts
'use server';
import { auth } from '@/lib/auth/server';
import { redirect } from 'next/navigation';
export async function signIn(formData: FormData) {
const { error } = await auth.signIn.email({
email: formData.get('email') as string,
password: formData.get('password') as string,
});
if (error) {
return { error: error.message };
}
redirect('/dashboard');
}
export async function signUp(formData: FormData) {
const { error } = await auth.signUp.email({
email: formData.get('email') as string,
password: formData.get('password') as string,
name: formData.get('name') as string,
});
if (error) {
return { error: error.message };
}
redirect('/dashboard');
}
export async function signOut() {
await auth.signOut();
redirect('/');
}
利用可能なサーバーメソッド
createNeonAuth() の auth インスタンスは、Better Auth のすべてのサーバーメソッドを提供します:
// Authentication
auth.signIn.email({ email, password })
auth.signUp.email({ email, password, name })
auth.signOut()
auth.getSession()
// User Management
auth.updateUser({ name, image })
// Organizations
auth.organization.create({ name, slug })
auth.organization.list()
// Admin (if enabled)
auth.admin.listUsers()
auth.admin.banUser({ userId })
クライアントコンポーネント
セッションフック
'use client';
import { authClient } from '@/lib/auth/client';
export function Dashboard() {
const { data: session, isPending, error } = authClient.useSession();
if (isPending) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!session) return <div>Not signed in</div>;
return <div>Hello, {session.user.name}</div>;
}
クライアント側の認証メソッド
'use client';
import { authClient } from '@/lib/auth/client';
// Sign in
await authClient.signIn.email({ email, password });
// Sign up
await authClient.signUp.email({ email, password, name });
// OAuth
await authClient.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
});
// Sign out
await authClient.signOut();
// Get session
const session = await authClient.getSession();
UI コンポーネント
AuthView - メインの認証インターフェース
import { AuthView } from '@neondatabase/auth/react/ui';
// Handles: sign-in, sign-up, forgot-password, reset-password, callback, sign-out
<AuthView pathname={path} />
条件付きレンダリング
import {
SignedIn,
SignedOut,
AuthLoading,
RedirectToSignIn,
} from '@neondatabase/auth/react/ui';
function MyPage() {
return (
<>
<AuthLoading>
<LoadingSpinner />
</AuthLoading>
<SignedIn>
<Dashboard />
</SignedIn>
<SignedOut>
<LandingPage />
</SignedOut>
{/* Auto-redirect if not signed in */}
<RedirectToSignIn />
</>
);
}
UserButton
import { UserButton } from '@neondatabase/auth/react/ui';
function Header() {
return (
<header>
<nav>...</nav>
<UserButton />
</header>
);
}
アカウント管理
import {
AccountSettingsCards,
SecuritySettingsCards,
SessionsCard,
ChangePasswordCard,
ChangeEmailCard,
DeleteAccountCard,
ProvidersCard,
} from '@neondatabase/auth/react/ui';
組織コンポーネント
import {
OrganizationSwitcher,
OrganizationSettingsCards,
OrganizationMembersCard,
AcceptInvitationCard,
} from '@neondatabase/auth/react/ui';
ソーシャル/OAuth プロバイダー
設定
<NeonAuthUIProvider
social={{
providers: ['google'],
}}
>
プログラムによる OAuth
// Client-side
await authClient.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
});
対応プロバイダー
google、github、twitter、discord、apple、microsoft、facebook、linkedin、spotify、twitch、gitlab、bitbucket
ミドルウェアの設定
基本的な保護ルート
import { auth } from '@/lib/auth/server';
export default auth.middleware({
loginUrl: '/auth/sign-in',
});
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*', '/settings/:path*'],
};
カスタムロジック
import { auth } from '@/lib/auth/server';
import { NextResponse } from 'next/server';
export default auth.middleware({
loginUrl: '/auth/sign-in',
});
アカウントページのセットアップ
アカウントレイアウト(app/account/[path]/page.tsx)
import {
SignedIn,
RedirectToSignIn,
AccountSettingsCards,
SecuritySettingsCards,
SessionsCard,
ChangePasswordCard,
} from '@neondatabase/auth/react/ui';
export default async function AccountPage({ params }: { params: Promise<{ path: string }> }) {
const { path = 'settings' } = await params;
return (
<>
<RedirectToSignIn />
<SignedIn>
{path === 'settings' && <AccountSettingsCards />}
{path === 'security' && (
<>
<ChangePasswordCard />
<SecuritySettingsCards />
</>
)}
{path === 'sessions' && <SessionsCard />}
</SignedIn>
</>
);
}
高度な機能
匿名アクセス
未認証ユーザー向けに、RLS ベースのデータアクセスを有効にします:
// lib/auth/client.ts
'use client';
import { createAuthClient } from '@neondatabase/auth/next';
export const authClient = createAuthClient({
allowAnonymous: true,
});
JWT トークンの取得
const token = await authClient.getJWTToken();
// Use in API calls
const response = await fetch('/api/data', {
headers: { Authorization: `Bearer ${token}` },
});
タブ間の同期
BroadcastChannel により自動で同期されます。1 つのタブでサインアウトすると、すべてのタブでサインアウトします。
サーバーコンポーネントでのセッション更新
Next.js では onSessionChange コールバックが重要です:
<NeonAuthUIProvider
onSessionChange={() => router.refresh()} // Refreshes Server Components!
// ...
>
これがないと、サインイン/サインアウト後にサーバーコンポーネントが更新されません。
エラー処理
サーバーアクション
'use server';
export async function signIn(formData: FormData) {
const { error } = await authServer.signIn.email({
email: formData.get('email') as string,
password: formData.get('password') as string,
});
if (error) {
// Return error to client
return { error: error.message };
}
redirect('/dashboard');
}
クライアントコンポーネント
'use client';
const { error } = await authClient.signIn.email({ email, password });
if (error) {
toast.error(error.message);
}
よくあるエラー
| エラー | 原因 |
|---|---|
Invalid credentials | メールアドレスまたはパスワードが間違っています |
User already exists | そのメールアドレスはすでに登録されています |
Email not verified | メールアドレスの確認が必要です |
Session not found | セッションの有効期限が切れているか、無効です |
FAQ/トラブルシューティング
サインイン後にサーバーコンポーネントが更新されない場合
プロバイダーに onSessionChange={() => router.refresh()} が設定されていることを確認してください。
<NeonAuthUIProvider
onSessionChange={() => router.refresh()}
// ...
>
匿名アクセスが機能しない場合
データベースで anonymous ロールに権限を付与してください。
GRANT SELECT ON public.posts TO anonymous;
GRANT SELECT ON public.products TO anonymous;
また、RLS ポリシーを設定してください。
CREATE POLICY "Anyone can read published posts"
ON public.posts FOR SELECT
USING (published = true);
ミドルウェアでルートが保護されない場合
matcher の設定を確認してください。
export const config = {
matcher: [
'/dashboard/:path*',
'/account/:path*',
// Add your protected routes here
],
};
OAuth コールバックでエラーが発生する場合
API ルートが app/api/auth/[...path]/route.ts に正しく設定されていることを確認してください。
import { auth } from '@/lib/auth/server';
export const { GET, POST } = auth.handler();
セッションが保持されない場合
- Cookie が有効になっていることを確認してください
.env.localでNEON_AUTH_BASE_URLが正しいことを確認してくださいNEON_AUTH_COOKIE_SECRETが設定され、32 文字以上であることを確認してください- Cookie がブロックされるシークレットモードを使用していないことを確認してください
セッションデータのキャッシュが機能しない場合
NEON_AUTH_COOKIE_SECRETが 32 文字以上であることを確認してくださいcookies.secretがcreateNeonAuth()に渡されていることを確認してください- 必要に応じて、
cookies.sessionDataTtlを設定してください(デフォルト:300 秒)
パフォーマンスに関する注意事項
- セッションデータのキャッシュ:TTL を設定可能な、JWT 署名付きの
session_dataCookie(デフォルト:5 分)- 秒単位で
cookies.sessionDataTtlを設定します - ミリ秒未満のセッション検索(<1ms)が可能になります
- キャッシュミス時は上流の
/get-sessionに自動的にフォールバックします
- 秒単位で
- リクエストの重複排除:並行する呼び出しで単一のネットワークリクエストを共有します(コールドスタートが 10 倍高速になります)
- サーバーコンポーネント:JavaScript を使用せずにセッションへアクセスするには、
auth.getSession()を使用してください - タブ間同期:BroadcastChannel 経由で <50ms
- Cookie ドメイン:サブドメイン間で Cookie を共有するには、必要に応じて
cookies.domainを設定します