---
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"]
---
# 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
1. **Server vs Client imports**: Use correct import paths
2. **`'use client'` directive**: Required for client components using hooks
3. **CSS Import**: Choose ONE - either `/ui/css` OR `/ui/tailwind`, never both
4. **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.)
---
## Setup
### 1. Install
```bash
npm install @neondatabase/auth
```
### 2. Environment (`.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:
```bash
openssl rand -base64 32
```
### 3. Server Setup (`lib/auth/server.ts`)
Create a auth instance that provides handler, middleware, and server methods:
```typescript
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 Route (`app/api/auth/[...path]/route.ts`)
```typescript
import { auth } from '@/lib/auth/server';
export const { GET, POST } = auth.handler();
```
### 5. Middleware (`middleware.ts`)
```typescript
import { auth } from '@/lib/auth/server';
export default auth.middleware({
loginUrl: '/auth/sign-in',
});
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
};
```
### 6. Client (`lib/auth/client.ts`)
```typescript
'use client';
import { createAuthClient } from '@neondatabase/auth/next';
export const authClient = createAuthClient();
```
### 7. Provider (`app/providers.tsx`)
```typescript
'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 (
router.refresh()}
redirectTo="/dashboard"
Link={({href, children}) => {children}}
>
{children}
);
}
```
### 8. Layout (`app/layout.tsx`)
```typescript
import { Providers } from './providers';
import '@neondatabase/auth/ui/css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
### 9. Auth Pages (`app/auth/[path]/page.tsx`)
```typescript
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 ;
}
```
---
## CSS & Styling
### Import Options
**Without Tailwind** (pre-built CSS bundle ~47KB):
```typescript
// app/layout.tsx
import '@neondatabase/auth/ui/css';
```
**With Tailwind CSS v4** (`app/globals.css`):
```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:
```typescript
```
### Custom Theming
Override CSS variables in `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 Props
Full configuration options:
```typescript
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
```typescript
// 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
```typescript
// 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
```typescript
// 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:
```typescript
// 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
```typescript
'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
```typescript
'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
```typescript
import { AuthView } from '@neondatabase/auth/react/ui';
// Handles: sign-in, sign-up, forgot-password, reset-password, callback, sign-out
```
### Conditional Rendering
```typescript
import {
SignedIn,
SignedOut,
AuthLoading,
RedirectToSignIn,
} from '@neondatabase/auth/react/ui';
function MyPage() {
return (
<>
{/* Auto-redirect if not signed in */}
>
);
}
```
### UserButton
```typescript
import { UserButton } from '@neondatabase/auth/react/ui';
function Header() {
return (
);
}
```
### Account Management
```typescript
import {
AccountSettingsCards,
SecuritySettingsCards,
SessionsCard,
ChangePasswordCard,
ChangeEmailCard,
DeleteAccountCard,
ProvidersCard,
} from '@neondatabase/auth/react/ui';
```
### Organization Components
```typescript
import {
OrganizationSwitcher,
OrganizationSettingsCards,
OrganizationMembersCard,
AcceptInvitationCard,
} from '@neondatabase/auth/react/ui';
```
---
## Social/OAuth Providers
### Configuration
```typescript
```
### Programmatic OAuth
```typescript
// Client-side
await authClient.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
});
```
### Supported Providers
`google`, `github`, `twitter`, `discord`, `apple`, `microsoft`, `facebook`, `linkedin`, `spotify`, `twitch`, `gitlab`, `bitbucket`
---
## Middleware Configuration
### Basic Protected Routes
```typescript
import { auth } from '@/lib/auth/server';
export default auth.middleware({
loginUrl: '/auth/sign-in',
});
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*', '/settings/:path*'],
};
```
### Custom Logic
```typescript
import { auth } from '@/lib/auth/server';
import { NextResponse } from 'next/server';
export default auth.middleware({
loginUrl: '/auth/sign-in',
});
```
---
## Account Pages Setup
### Account Layout (`app/account/[path]/page.tsx`)
```typescript
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 (
<>
{path === 'settings' && }
{path === 'security' && (
<>
>
)}
{path === 'sessions' && }
>
);
}
```
---
## Advanced Features
### Anonymous Access
Enable RLS-based data access for unauthenticated users:
```typescript
// lib/auth/client.ts
'use client';
import { createAuthClient } from '@neondatabase/auth/next';
export const authClient = createAuthClient({
allowAnonymous: true,
});
```
### Get JWT Token
```typescript
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:
```typescript
router.refresh()} // Refreshes Server Components!
// ...
>
```
Without this, Server Components won't update after sign-in/sign-out.
---
## Error Handling
### Server Actions
```typescript
'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
```typescript
'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 |
---
## FAQ / Troubleshooting
### Server Components not updating after sign-in?
Make sure you have `onSessionChange={() => router.refresh()}` in your provider:
```typescript
router.refresh()}
// ...
>
```
### Anonymous access not working?
Grant permissions to the `anonymous` role in your database:
```sql
GRANT SELECT ON public.posts TO anonymous;
GRANT SELECT ON public.products TO anonymous;
```
And configure RLS policies:
```sql
CREATE POLICY "Anyone can read published posts"
ON public.posts FOR SELECT
USING (published = true);
```
### Middleware not protecting routes?
Check your `matcher` configuration:
```typescript
export const config = {
matcher: [
'/dashboard/:path*',
'/account/:path*',
// Add your protected routes here
],
};
```
### OAuth callback errors?
Ensure your API route is set up correctly at `app/api/auth/[...path]/route.ts`:
```typescript
import { auth } from '@/lib/auth/server';
export const { GET, POST } = auth.handler();
```
### Session not persisting?
1. Check cookies are enabled
2. Verify `NEON_AUTH_BASE_URL` is correct in `.env.local`
3. Verify `NEON_AUTH_COOKIE_SECRET` is set and at least 32 characters
4. Make sure you're not in incognito with cookies blocked
### Session data cache not working?
1. Verify `NEON_AUTH_COOKIE_SECRET` is at least 32 characters long
2. Check `cookies.secret` is passed to `createNeonAuth()`
3. Optionally configure `cookies.sessionDataTtl` (default: 300 seconds)
---
## 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