Labsco
neondatabase logo

neon-auth-nextjs

✓ Official13

by neondatabase · part of neondatabase/neon-js

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…

🔥🔥🔥✓ VerifiedAccount requiredNeeds API keys
🧰 Not standalone. This skill ships with neondatabase/neon-js and only works together with that tool — install the tool first, then add this skill.

This is the playbook your agent receives when the skill activates — you don't need to read it to use the skill, but it's here to audit before installing.

by neondatabase

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… npx skills add https://github.com/neondatabase/neon-js --skill neon-auth-nextjs Download ZIPGitHub13

Neon Auth for Next.js

Help developers set up @neondatabase/auth in Next.js App Router applications (auth only, no database).

When to Use

Use this skill when:

  • Setting up Neon Auth in Next.js (App Router)

  • User mentions "next.js", "next", or "app router" with Neon Auth

  • Auth-only setup (no database needed)

Critical Rules

  • Server vs Client imports: Use correct import paths

  • 'use client' directive: Required for client components using hooks

  • CSS Import: Choose ONE - either /ui/css OR /ui/tailwind, never both

  • onSessionChange: Always call router.refresh() to update Server Components

Critical Imports

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

  • All Better Auth server methods (.signIn, .signUp, .getSession, etc.)

CSS & Styling

Import Options

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.

Dark Mode

The provider includes next-themes. Control via defaultTheme prop:

 

Custom Theming

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 */
}

NeonAuthUIProvider Props

Full configuration options:

 router.refresh()} // Refresh Server Components!
 redirectTo="/dashboard" // Where to redirect after auth
 Link={({href, children}) => {children} } // 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}
 

Server Components (RSC)

Get Session in Server Component

// 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 Not signed in
;

 return (
 
 Hello, {session.user.name}

 Email: {session.user.email}

 

 );
}

Route Handler with Auth

// 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

Server actions use the same auth instance from lib/auth/server.ts:

Sign In Action

// 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('/');
}

Available Server Methods

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 })

Client Components

Session Hook

'use client';
import { authClient } from '@/lib/auth/client';

export function Dashboard() {
 const { data: session, isPending, error } = authClient.useSession();

 if (isPending) return Loading...
;
 if (error) return Error: {error.message}
;
 if (!session) return Not signed in
;

 return Hello, {session.user.name}
;
}

Client-Side Auth Methods

'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 Components

AuthView - Main Auth Interface

import { AuthView } from '@neondatabase/auth/react/ui';

// Handles: sign-in, sign-up, forgot-password, reset-password, callback, sign-out
 

Conditional Rendering

import {
 SignedIn,
 SignedOut,
 AuthLoading,
 RedirectToSignIn,
} from '@neondatabase/auth/react/ui';

function MyPage() {
 return (
 <>
 
 
 

 
 
 

 
 
 

 {/* Auto-redirect if not signed in */}
 
 
 );
}

UserButton

import { UserButton } from '@neondatabase/auth/react/ui';

function Header() {
 return (
 
 ... 
 
 
 );
}

Account Management

import {
 AccountSettingsCards,
 SecuritySettingsCards,
 SessionsCard,
 ChangePasswordCard,
 ChangeEmailCard,
 DeleteAccountCard,
 ProvidersCard,
} from '@neondatabase/auth/react/ui';

Organization Components

import {
 OrganizationSwitcher,
 OrganizationSettingsCards,
 OrganizationMembersCard,
 AcceptInvitationCard,
} from '@neondatabase/auth/react/ui';

Social/OAuth Providers

Configuration

 

Programmatic OAuth

// Client-side
await authClient.signIn.social({
 provider: 'google',
 callbackURL: '/dashboard',
});

Supported Providers

google, github, twitter, discord, apple, microsoft, facebook, linkedin, spotify, twitch, gitlab, bitbucket

Advanced Features

Anonymous Access

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,
});

Get JWT Token

const token = await authClient.getJWTToken();

// Use in API calls
const response = await fetch('/api/data', {
 headers: { Authorization: `Bearer ${token}` },
});

Cross-Tab Sync

Automatic via BroadcastChannel. Sign out in one tab signs out all tabs.

Session Refresh in Server Components

The onSessionChange callback is crucial for Next.js:

 router.refresh()} // Refreshes Server Components!
 // ...
>

Without this, Server Components won't update after sign-in/sign-out.

Error Handling

Server Actions

'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');
}

Client Components

'use client';

const { error } = await authClient.signIn.email({ email, password });

if (error) {
 toast.error(error.message);
}

Common Errors

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

Performance Notes

  • Session data caching: JWT-signed session_data cookie with configurable TTL (default: 5 minutes)

  • Configure via cookies.sessionDataTtl in seconds

  • Enables sub-millisecond session lookups (<1ms)

  • Automatic fallback to upstream /get-session on cache miss

  • Request deduplication: Concurrent calls share single network request (10x faster cold starts)

  • Server Components: Use auth.getSession() for zero-JS session access

  • Cross-tab sync: <50ms via BroadcastChannel

  • Cookie domain: Optional cookies.domain for cross-subdomain cookie sharing