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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "neon-auth-nextjs" with this command: npx skills add neondatabase/neon-js/neondatabase-neon-js-neon-auth-nextjs

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

PurposeImport 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

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:

openssl rand -base64 32

3. Server Setup (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
  },
});

4. API Route (app/api/auth/[...path]/route.ts)

import { auth } from '@/lib/auth/server';

export const { GET, POST } = auth.handler();

5. Middleware (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. Client (lib/auth/client.ts)

'use client';
import { createAuthClient } from '@neondatabase/auth/next';

export const authClient = createAuthClient();

7. Provider (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. Layout (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. Auth Pages (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 & 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:

<NeonAuthUIProvider
  defaultTheme="system" // 'light' | 'dark' | 'system'
  // ...
>

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:

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

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

  return (
    <div>
      <p>Hello, {session.user.name}</p>
      <p>Email: {session.user.email}</p>
    </div>
  );
}

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

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
<AuthView pathname={path} />

Conditional Rendering

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

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

<NeonAuthUIProvider
  social={{
    providers: ['google'],
  }}
>

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


Middleware Configuration

Basic Protected Routes

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

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)

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

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:

<NeonAuthUIProvider
  onSessionChange={() => 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

ErrorCause
Invalid credentialsWrong email/password
User already existsEmail already registered
Email not verifiedVerification required
Session not foundExpired or invalid session

FAQ / Troubleshooting

Server Components not updating after sign-in?

Make sure you have onSessionChange={() => router.refresh()} in your provider:

<NeonAuthUIProvider
  onSessionChange={() => router.refresh()}
  // ...
>

Anonymous access not working?

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

Middleware not protecting routes?

Check your matcher configuration:

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:

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

neon-js

No summary provided by upstream source.

Repository SourceNeeds Review
General

neon-js-react

No summary provided by upstream source.

Repository SourceNeeds Review
General

neon-auth-react

No summary provided by upstream source.

Repository SourceNeeds Review
General

neon-drizzle

No summary provided by upstream source.

Repository SourceNeeds Review