Labsco
neondatabase logo

neon-auth-react

✓ Official13

by neondatabase · part of neondatabase/neon-js

Sets up Neon Auth in React applications (Vite, CRA). Configures authentication adapters, creates auth client, and sets up UI components. Use when adding…

🔥🔥🔥✓ VerifiedFreeQuick setup
🧰 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 React applications (Vite, CRA). Configures authentication adapters, creates auth client, and sets up UI components. Use when adding… npx skills add https://github.com/neondatabase/neon-js --skill neon-auth-react Download ZIPGitHub13

Neon Auth for React

Help developers set up @neondatabase/auth (authentication only, no database) in React applications with Vite, Create React App, or similar bundlers.

When to Use

Use this skill when:

  • Setting up auth-only in React (no database needed)

  • User already has a database solution

  • User mentions "@neondatabase/auth" without "neon-js"

  • User is NOT using Next.js (use neon-auth-nextjs skill for Next.js)

Critical Rules

  • Adapter Factory Pattern: Always call adapters with () - they are factory functions

  • React Adapter Import: Use subpath @neondatabase/auth/react/adapters

  • createAuthClient takes URL as first arg: createAuthClient(url, config)

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

CSS & Styling

Import Options

Without Tailwind (pre-built CSS bundle ~47KB):

/* In your main CSS file or import in provider */
@import '@neondatabase/auth/ui/css';

With Tailwind CSS v4:

@import 'tailwindcss';
@import '@neondatabase/auth/ui/tailwind';

IMPORTANT: Never import both - causes duplicate styles.

Dark Mode

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

 

Custom Theming

Override CSS variables in your stylesheet:

:root {
 --primary: oklch(0.7 0.15 250);
 --primary-foreground: oklch(0.98 0 0);
 --background: oklch(1 0 0);
 --foreground: oklch(0.1 0 0);
 --card: oklch(1 0 0);
 --card-foreground: oklch(0.1 0 0);
 --border: oklch(0.9 0 0);
 --input: oklch(0.9 0 0);
 --ring: oklch(0.7 0 0);
 --radius: 0.5rem;
 /* See theme.css for full list */
}

.dark {
 --background: oklch(0.15 0 0);
 --foreground: oklch(0.98 0 0);
 /* Dark mode overrides */
}

NeonAuthUIProvider Props

Full configuration options:

 {children} } // Router's Link component
 redirectTo="/dashboard" // Where to redirect after auth

 // 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'],
 }}

 // Avatar Configuration
 avatar={{
 size: 256,
 extension: 'webp',
 }}

 // Organization Features
 organization={{}} // Enable org features

 // Dark Mode
 defaultTheme="system" // 'light' | 'dark' | 'system'

 // Custom Labels
 localization={{
 SIGN_IN: 'Welcome Back',
 SIGN_IN_DESCRIPTION: 'Sign in to your account',
 SIGN_UP: 'Create Account',
 SIGN_UP_DESCRIPTION: 'Join us today',
 FORGOT_PASSWORD: 'Forgot Password?',
 OR_CONTINUE_WITH: 'or continue with',
 // See better-auth-ui docs for full list
 }}
>
 {children}
 

UI Components

AuthView - Main Auth Interface

Handles sign-in, sign-up, forgot password, and callback routes:

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

// Route: /auth/:pathname
function AuthPage() {
 const { pathname } = useParams(); // 'sign-in', 'sign-up', 'forgot-password', etc.

 return ;
}

Supported pathnames: 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 (
 <>
 {/* Show while checking auth state */}
 
 
 

 {/* Show only when authenticated */}
 
 
 

 {/* Show only when NOT authenticated */}
 
 
 

 {/* Redirect to sign-in if not authenticated */}
 
 
 );
}

UserButton

Dropdown menu with user avatar, name, and sign-out:

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

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

Account Management Components

import {
 AccountSettingsCards, // Profile info (avatar, name, email)
 SecuritySettingsCards, // Security options (linked accounts)
 SessionsCard, // Active sessions management
 ChangePasswordCard, // Password change form
 ChangeEmailCard, // Email change form
 DeleteAccountCard, // Account deletion
 ProvidersCard, // Linked OAuth providers
} from '@neondatabase/auth/react/ui';

function AccountPage() {
 const { view } = useParams(); // 'settings', 'security', 'sessions'

 return (
 <>
 
 
 {view === 'settings' && }
 {view === 'security' && (
 <>
 
 
 
 )}
 {view === 'sessions' && }
 
 
 );
}

Organization Components

import {
 OrganizationSwitcher, // Switch between orgs
 OrganizationSettingsCards, // Org settings
 OrganizationMembersCard, // Member management
 AcceptInvitationCard, // Accept org invite
} from '@neondatabase/auth/react/ui';

Adapter Options

BetterAuthReactAdapter (Recommended for React)

Native Better Auth API with React hooks:

import { BetterAuthReactAdapter } from '@neondatabase/auth/react/adapters';

const authClient = createAuthClient(url, {
 adapter: BetterAuthReactAdapter(),
});

// Methods
await authClient.signIn.email({ email, password });
await authClient.signUp.email({ email, password, name });
await authClient.signIn.social({ provider: 'google', callbackURL: '/dashboard' });
await authClient.signOut();
const session = await authClient.getSession();

// React Hook
const { data, isPending, error } = authClient.useSession();

SupabaseAuthAdapter (Supabase-compatible API)

For migrating from Supabase or familiar API:

import { SupabaseAuthAdapter } from '@neondatabase/auth/vanilla/adapters';

const authClient = createAuthClient(url, {
 adapter: SupabaseAuthAdapter(),
});

// Supabase-style methods
await authClient.signUp({ email, password, options: { data: { name } } });
await authClient.signInWithPassword({ email, password });
await authClient.signInWithOAuth({ provider: 'google', options: { redirectTo } });
await authClient.signOut();
const { data: session } = await authClient.getSession();

// Event listener
authClient.onAuthStateChange((event, session) => {
 console.log(event); // 'SIGNED_IN', 'SIGNED_OUT', 'TOKEN_REFRESHED', 'USER_UPDATED'
});

BetterAuthVanillaAdapter (Non-React)

For vanilla JS/TS without React hooks:

import { BetterAuthVanillaAdapter } from '@neondatabase/auth/vanilla/adapters';

const authClient = createAuthClient(url, {
 adapter: BetterAuthVanillaAdapter(),
});

// Same API as BetterAuthReactAdapter, but no useSession() hook

Social/OAuth Providers

Configuration

Enable providers in NeonAuthUIProvider:

 

Programmatic OAuth Sign-In

// BetterAuth API
await authClient.signIn.social({
 provider: 'google',
 callbackURL: '/dashboard',
 scopes: ['email', 'profile'], // Optional
});

// Supabase API
await authClient.signInWithOAuth({
 provider: 'google',
 options: {
 redirectTo: '/dashboard',
 scopes: 'email profile',
 },
});

Supported Providers

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

OAuth in Iframes

OAuth automatically uses popup flow when running in iframes (due to X-Frame-Options restrictions). No configuration needed.

Session Hook

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

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

 return (
 
 Hello, {session.user.name}

 Email: {session.user.email}

 ID: {session.user.id}

 
 

 );
}

Session object shape:

{
 user: {
 id: string;
 email: string;
 name: string;
 image?: string;
 emailVerified: boolean;
 createdAt: Date;
 updatedAt: Date;
 };
 session: {
 id: string;
 token: string; // JWT token
 expiresAt: Date;
 ipAddress?: string;
 userAgent?: string;
 };
}

Advanced Features

Anonymous Access

Enable RLS-based data access for unauthenticated users:

// Client setup
const authClient = createAuthClient(url, {
 adapter: BetterAuthReactAdapter(),
 allowAnonymous: true,
});

// Get token (returns anonymous JWT if not signed in)
const token = await authClient.getJWTToken?.();

Get JWT Token (for API calls)

const token = await authClient.getJWTToken();

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

Password Reset Flow

// 1. Request reset email (Supabase API)
await authClient.resetPasswordForEmail(email, {
 redirectTo: '/auth/reset-password',
});

// 2. User clicks link, lands on reset page
// 3. Verify OTP and set new password
await authClient.verifyOtp({
 email,
 token: otpFromUrl,
 type: 'recovery',
});

// Then call password update
await authClient.updateUser({ password: newPassword });

Update User Profile

// BetterAuth API
await authClient.updateUser({
 name: 'New Name',
 image: 'https://...',
 // Custom fields defined in account.fields
});

// Supabase API
await authClient.updateUser({
 data: {
 name: 'New Name',
 avatar_url: 'https://...',
 },
});

Identity/Account Linking

// List linked accounts
const { data } = await authClient.getUserIdentities();
// Returns: { identities: [{ provider: 'google', ... }] }

// Link new provider
await authClient.linkIdentity({
 provider: 'google',
 options: { redirectTo: '/account/security' },
});

// Unlink provider
await authClient.unlinkIdentity({
 identity_id: 'identity-uuid',
});

Auth State Events (Supabase Adapter)

const { data: { subscription } } = authClient.onAuthStateChange((event, session) => {
 switch (event) {
 case 'SIGNED_IN':
 console.log('User signed in:', session?.user);
 break;
 case 'SIGNED_OUT':
 console.log('User signed out');
 break;
 case 'TOKEN_REFRESHED':
 console.log('Token refreshed');
 break;
 case 'USER_UPDATED':
 console.log('User profile updated');
 break;
 }
});

// Cleanup
subscription.unsubscribe();

Cross-Tab Synchronization

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

Protected Routes

Pattern with React Router

// routes.tsx
import { Routes, Route } from 'react-router-dom';

export function AppRoutes() {
 return (
 
 {/* Public */}
 } />

 {/* Auth routes */}
 } />

 {/* Protected */}
 } />
 } />
 
 );
}

// ProtectedRoute.tsx
function ProtectedRoute({ children }: { children: React.ReactNode }) {
 return (
 <>
 
 
 
 
 
 {children}
 
 
 );
}

Auth Page Setup

// pages/AuthPage.tsx
import { useParams } from 'react-router-dom';
import { AuthView } from '@neondatabase/auth/react/ui';

export function AuthPage() {
 const { pathname } = useParams();
 return ;
}

Account Page Setup

// pages/AccountPage.tsx
import { useParams } from 'react-router-dom';
import {
 SignedIn,
 RedirectToSignIn,
 AccountSettingsCards,
 SecuritySettingsCards,
 SessionsCard,
 ChangePasswordCard,
} from '@neondatabase/auth/react/ui';

export function AccountPage() {
 const { view = 'settings' } = useParams();

 return (
 <>
 
 
 {view === 'settings' && }
 {view === 'security' && (
 <>
 
 
 
 )}
 {view === 'sessions' && }
 
 
 );
}

Error Handling

Error Response Shape

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

if (result.error) {
 console.error(result.error.message); // Human-readable message
 console.error(result.error.status); // HTTP status code
}

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 Rate limited Too many requests

Try-Catch Pattern

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

 if (error) {
 // Handle auth-specific errors
 toast.error(error.message);
 return;
 }

 // Success - redirect
 navigate('/dashboard');
} catch (err) {
 // Handle network/unexpected errors
 toast.error('Something went wrong');
}

Performance Notes

  • Session caching: 60-second TTL, automatic JWT expiration handling

  • Request deduplication: Concurrent calls share single network request

  • Cold start: ~200ms (single request)

  • Cross-tab sync: <50ms via BroadcastChannel