一键导入
neon-auth-nextjs
// Sets up Neon Auth in Next.js App Router applications. Configures API routes, middleware, server components, and UI. Use when adding auth-only to Next.js apps (no database needed).
// Sets up Neon Auth in Next.js App Router applications. Configures API routes, middleware, server components, and UI. Use when adding auth-only to Next.js apps (no database needed).
Sets up Neon Auth in React applications (Vite, CRA). Configures authentication adapters, creates auth client, and sets up UI components. Use when adding auth-only to React apps (no database needed).
Sets up the full Neon SDK with authentication AND database queries in React apps (Vite, CRA). Creates typed client, generates database types, and configures auth UI. Use for auth + database integration.
| name | neon-auth-nextjs |
| description | Sets up Neon Auth in Next.js App Router applications. Configures API routes, middleware, server components, and UI. Use when adding auth-only to Next.js apps (no database needed). |
| allowed-tools | ["Bash","Write","Read","Edit","Glob","Grep"] |
Help developers set up @neondatabase/auth in Next.js App Router applications (auth only, no database).
Use this skill when:
'use client' directive: Required for client components using hooks/ui/css OR /ui/tailwind, never bothrouter.refresh() to update Server Components| Purpose | Import From |
|---|---|
Unified Server (createNeonAuth) | @neondatabase/auth/next/server |
| Client Auth | @neondatabase/auth/next |
| UI Components | @neondatabase/auth/react/ui |
| View Paths (static params) | @neondatabase/auth/react/ui/server |
Note: Use createNeonAuth() from @neondatabase/auth/next/server to get a unified auth instance that provides:
.handler() - API route handler.middleware() - Route protection middleware.signIn, .signUp, .getSession, etc.)npm install @neondatabase/auth
.env.local)NEON_AUTH_BASE_URL=https://your-auth.neon.tech
NEON_AUTH_COOKIE_SECRET=your-secret-at-least-32-characters-long
Important: Generate a secure secret (32+ characters) for production:
openssl rand -base64 32
lib/auth/server.ts)Create a auth instance that provides handler, middleware, and server methods:
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
},
});
app/api/auth/[...path]/route.ts)import { auth } from '@/lib/auth/server';
export const { GET, POST } = auth.handler();
middleware.ts)import { auth } from '@/lib/auth/server';
export default auth.middleware({
loginUrl: '/auth/sign-in',
});
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
};
lib/auth/client.ts)'use client';
import { createAuthClient } from '@neondatabase/auth/next';
export const authClient = createAuthClient();
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>
);
}
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>
);
}
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} />;
}
Without Tailwind (pre-built CSS bundle ~47KB):
// app/layout.tsx
import '@neondatabase/auth/ui/css';
With Tailwind CSS v4 (app/globals.css):
@import 'tailwindcss';
@import '@neondatabase/auth/ui/tailwind';
IMPORTANT: Never import both - causes duplicate styles.
The provider includes next-themes. Control via defaultTheme prop:
<NeonAuthUIProvider
defaultTheme="system" // 'light' | 'dark' | 'system'
// ...
>
Override CSS variables in globals.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 */
}
Full configuration options:
<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>
// 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 });
}
Server actions use the same auth instance from lib/auth/server.ts:
// 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('/');
}
The auth instance from createNeonAuth() provides all Better Auth server methods:
// 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();
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 />
</>
);
}
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';
<NeonAuthUIProvider
social={{
providers: ['google'],
}}
>
// 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>
</>
);
}
Enable RLS-based data access for unauthenticated users:
// lib/auth/client.ts
'use client';
import { createAuthClient } from '@neondatabase/auth/next';
export const authClient = createAuthClient({
allowAnonymous: true,
});
const token = await authClient.getJWTToken();
// Use in API calls
const response = await fetch('/api/data', {
headers: { Authorization: `Bearer ${token}` },
});
Automatic via BroadcastChannel. Sign out in one tab signs out all tabs.
The onSessionChange callback is crucial for Next.js:
<NeonAuthUIProvider
onSessionChange={() => router.refresh()} // Refreshes Server Components!
// ...
>
Without this, Server Components won't update after sign-in/sign-out.
'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);
}
| Error | Cause |
|---|---|
Invalid credentials | Wrong email/password |
User already exists | Email already registered |
Email not verified | Verification required |
Session not found | Expired or invalid session |
Make sure you have onSessionChange={() => router.refresh()} in your provider:
<NeonAuthUIProvider
onSessionChange={() => router.refresh()}
// ...
>
Grant permissions to the anonymous role in your database:
GRANT SELECT ON public.posts TO anonymous;
GRANT SELECT ON public.products TO anonymous;
And configure RLS policies:
CREATE POLICY "Anyone can read published posts"
ON public.posts FOR SELECT
USING (published = true);
Check your matcher configuration:
export const config = {
matcher: [
'/dashboard/:path*',
'/account/:path*',
// Add your protected routes here
],
};
Ensure your API route is set up correctly at app/api/auth/[...path]/route.ts:
import { auth } from '@/lib/auth/server';
export const { GET, POST } = auth.handler();
NEON_AUTH_BASE_URL is correct in .env.localNEON_AUTH_COOKIE_SECRET is set and at least 32 charactersNEON_AUTH_COOKIE_SECRET is at least 32 characters longcookies.secret is passed to createNeonAuth()cookies.sessionDataTtl (default: 300 seconds)session_data cookie with configurable TTL (default: 5 minutes)
cookies.sessionDataTtl in seconds/get-session on cache missauth.getSession() for zero-JS session accesscookies.domain for cross-subdomain cookie sharing