NEW Browse AI tools across categories — updated daily. See what's new →

Neon Auth React

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

Version1.0.0
LicenseMIT
Token count~4,684
UpdatedJun 5, 2026

Install

Quick install

via npx skills · works with 57+ agents
npx skills add https://github.com/neondatabase/neon-js/tree/HEAD/skills/neon-auth-react
Or pick agent:
npx skills add neondatabase/neon-js --skill neon-auth-react --agent claude-code
npx skills add neondatabase/neon-js --skill neon-auth-react --agent cursor
npx skills add neondatabase/neon-js --skill neon-auth-react --agent codex
npx skills add neondatabase/neon-js --skill neon-auth-react --agent opencode
npx skills add neondatabase/neon-js --skill neon-auth-react --agent github-copilot
npx skills add neondatabase/neon-js --skill neon-auth-react --agent windsurf
More install options

Shorthand — useful for multi-skill repos:

npx skills add neondatabase/neon-js --skill neon-auth-react

Manual — clone the repo and drop the folder into your agent's skills directory:

git clone https://github.com/neondatabase/neon-js.git
cp -r neon-js/skills/neon-auth-react ~/.claude/skills/
How to use: Once installed, ask your agent to "use the neon-auth-react skill" or describe what you want (e.g. "Sets up Neon Auth in React applications (Vite, CRA). Configures authentication a"). Requires Node.js 18+.

neon-auth-react

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

neon-auth-reactby 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-reactDownload ZIPGitHub

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

Setup

1. Install

`npm install @neondatabase/auth
`

2. Create Client (src/auth-client.ts)

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

export const authClient = createAuthClient(
import.meta.env.VITE_AUTH_URL,
{
adapter: BetterAuthReactAdapter(),
// allowAnonymous: true, // Enable for RLS access without login
}
);
`

3. Create Provider (src/providers.tsx)

`import { NeonAuthUIProvider } from '@neondatabase/auth/react/ui';
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { authClient } from './auth-client';

// Import CSS (choose one)
import '@neondatabase/auth/ui/css';

export function Providers({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();

return (
<NeonAuthUIProvider
authClient={authClient}
navigate={navigate}
redirectTo="/dashboard"
Link={({ children, href }) => <Link to={href}>{children}</Link>}
>
{children}
</NeonAuthUIProvider>
);
}
`

4. Wrap App (src/main.tsx)

`import { BrowserRouter } from 'react-router-dom';
import { Providers } from './providers';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<Providers>
<App />
</Providers>
</BrowserRouter>
);
`

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:

`<NeonAuthUIProvider
authClient={authClient}
defaultTheme="system" // 'light' | 'dark' | 'system'
// ...
>
`

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:

`<NeonAuthUIProvider
// Required
authClient={authClient}

// Navigation (required for React Router)
navigate={navigate} // Router's navigate function
Link={({href, children}) => <Link to={href}>{children}</Link>} // 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}
</NeonAuthUIProvider>
`

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

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

{/* Show only when authenticated */}
<SignedIn>
<Dashboard />
</SignedIn>

{/* Show only when NOT authenticated */}
<SignedOut>
<LandingPage />
</SignedOut>

{/* Redirect to sign-in if not authenticated */}
<RedirectToSignIn />
</>
);
}
`

UserButton

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

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

function Header() {
return (
<header>
<nav>...</nav>
<UserButton />
</header>
);
}
`

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 (
<>
<RedirectToSignIn />
<SignedIn>
{view === 'settings' && <AccountSettingsCards />}
{view === 'security' && (
<>
<ChangePasswordCard />
<SecuritySettingsCards />
</>
)}
{view === 'sessions' && <SessionsCard />}
</SignedIn>
</>
);
}
`

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:

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

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 <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!session) return <div>Not signed in</div>;

return (
<div>
<p>Hello, {session.user.name}</p>
<p>Email: {session.user.email}</p>
<p>ID: {session.user.id}</p>
<img src={session.user.image} alt="Avatar" />
</div>
);
}
`

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 (
<Routes>
{/* Public */}
<Route path="/" element={<HomePage />} />

{/* Auth routes */}
<Route path="/auth/:pathname" element={<AuthPage />} />

{/* Protected */}
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/account/:view?" element={<ProtectedRoute><AccountPage /></ProtectedRoute>} />
</Routes>
);
}

// ProtectedRoute.tsx
function ProtectedRoute({ children }: { children: React.ReactNode }) {
return (
<>
<AuthLoading>
<LoadingSpinner />
</AuthLoading>
<RedirectToSignIn />
<SignedIn>
{children}
</SignedIn>
</>
);
}
`

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

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 (
<>
<RedirectToSignIn />
<SignedIn>
{view === 'settings' && <AccountSettingsCards />}
{view === 'security' && (
<>
<ChangePasswordCard />
<SecuritySettingsCards />
</>
)}
{view === 'sessions' && <SessionsCard />}
</SignedIn>
</>
);
}
`

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

ErrorCauseInvalid credentialsWrong email/passwordUser already existsEmail already registeredEmail not verifiedVerification requiredSession not foundExpired or invalid sessionRate limitedToo 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

FAQ / Troubleshooting

Anonymous access not working?

When using allowAnonymous: true, you must grant permissions to the anonymous role in your database:

`-- Grant SELECT on specific tables
GRANT SELECT ON public.posts TO anonymous;
GRANT SELECT ON public.products TO anonymous;

-- Or grant on all tables in schema (be careful!)
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anonymous;

-- For INSERT/UPDATE/DELETE (if needed)
GRANT INSERT, UPDATE ON public.comments TO anonymous;
`

Your RLS policies must also allow the anonymous role:

`-- Example: Allow anonymous users to read published posts
CREATE POLICY "Anyone can read published posts"
ON public.posts FOR SELECT
USING (published = true);

-- Example: Allow anonymous users to read products
CREATE POLICY "Anyone can read products"
ON public.products FOR SELECT
USING (true);
`

OAuth redirect not working in iframe?

OAuth automatically uses popup flow in iframes. Make sure:

  • Your auth server allows the popup callback URL
  • Popups aren't blocked by the browser

Session not persisting after refresh?

Check that:

  • Cookies are enabled
  • Your auth server URL matches your domain (or has proper CORS)
  • You're not in incognito mode with cookies blocked

"Invalid credentials" but password is correct?

  • Email might not be verified (check emailVerification setting)
  • Account might be locked after too many failed attempts
  • Check if using correct adapter API (Supabase vs BetterAuth style)

More skills from neondatabase

claimable-postgresby neondatabaseInstant Postgres databases for local development, demos, prototyping, and test environments. No account required. Databases expire after 72 hours unless claimed to a Neon account.neon-postgres-branchesby neondatabaseThe outcome of this skill should be a created Neon branch (or a clear, actionable next step if creation cannot proceed). Choose the correct branch type, then execute branch creation via MCP or CLI.neon-postgres-egress-optimizerby neondatabaseGuide the user through diagnosing and fixing application-side query patterns that cause excessive data transfer (egress) from their Postgres database. Most high egress bills come from the application fetching more data than it uses.plugin-managerby neondatabaseManage plugin structure and configuration for this repository across both Cursor and Claude Code. Use when creating, updating, or reviewing plugin folders…skill-creatorby neondatabaseGuide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's…add-neon-docsby neondatabaseUse this skill when the user asks to add documentation, add docs, add references, or install documentation about Neon. Adds Neon best practices reference links…neon-authby neondatabaseSets up Neon Auth for your application. Configures authentication, creates auth routes, and generates UI components. Use when adding authentication to Next.js,…neon-drizzleby neondatabaseCreates a fully functional Drizzle ORM setup with a provisioned Neon database. Installs dependencies, provisions database credentials, configures connections,…

---

Source: https://github.com/neondatabase/neon-js/tree/HEAD/skills/neon-auth-react
Author: neondatabase
Discovered via: mcpservers.org

SKILL.md source

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

# neon-auth-react

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

# neon-auth-reactby 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 ZIPGitHub

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

## Setup

### 1. Install

```
`npm install @neondatabase/auth
`
```

### 2. Create Client (`src/auth-client.ts`)

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

export const authClient = createAuthClient(
import.meta.env.VITE_AUTH_URL,
{
adapter: BetterAuthReactAdapter(),
// allowAnonymous: true, // Enable for RLS access without login
}
);
`
```

### 3. Create Provider (`src/providers.tsx`)

```
`import { NeonAuthUIProvider } from '@neondatabase/auth/react/ui';
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { authClient } from './auth-client';

// Import CSS (choose one)
import '@neondatabase/auth/ui/css';

export function Providers({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();

return (
<NeonAuthUIProvider
authClient={authClient}
navigate={navigate}
redirectTo="/dashboard"
Link={({ children, href }) => <Link to={href}>{children}</Link>}
>
{children}
</NeonAuthUIProvider>
);
}
`
```

### 4. Wrap App (`src/main.tsx`)

```
`import { BrowserRouter } from 'react-router-dom';
import { Providers } from './providers';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<Providers>
<App />
</Providers>
</BrowserRouter>
);
`
```

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

```
`<NeonAuthUIProvider
authClient={authClient}
defaultTheme="system" // 'light' | 'dark' | 'system'
// ...
>
`
```

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

```
`<NeonAuthUIProvider
// Required
authClient={authClient}

// Navigation (required for React Router)
navigate={navigate} // Router's navigate function
Link={({href, children}) => <Link to={href}>{children}</Link>} // 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}
</NeonAuthUIProvider>
`
```

## 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 <AuthView pathname={pathname} />;
}
`
```

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

{/* Show only when authenticated */}
<SignedIn>
<Dashboard />
</SignedIn>

{/* Show only when NOT authenticated */}
<SignedOut>
<LandingPage />
</SignedOut>

{/* Redirect to sign-in if not authenticated */}
<RedirectToSignIn />
</>
);
}
`
```

### UserButton

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

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

function Header() {
return (
<header>
<nav>...</nav>
<UserButton />
</header>
);
}
`
```

### 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 (
<>
<RedirectToSignIn />
<SignedIn>
{view === 'settings' && <AccountSettingsCards />}
{view === 'security' && (
<>
<ChangePasswordCard />
<SecuritySettingsCards />
</>
)}
{view === 'sessions' && <SessionsCard />}
</SignedIn>
</>
);
}
`
```

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

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

### 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 <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!session) return <div>Not signed in</div>;

return (
<div>
<p>Hello, {session.user.name}</p>
<p>Email: {session.user.email}</p>
<p>ID: {session.user.id}</p>
<img src={session.user.image} alt="Avatar" />
</div>
);
}
`
```

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 (
<Routes>
{/* Public */}
<Route path="/" element={<HomePage />} />

{/* Auth routes */}
<Route path="/auth/:pathname" element={<AuthPage />} />

{/* Protected */}
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/account/:view?" element={<ProtectedRoute><AccountPage /></ProtectedRoute>} />
</Routes>
);
}

// ProtectedRoute.tsx
function ProtectedRoute({ children }: { children: React.ReactNode }) {
return (
<>
<AuthLoading>
<LoadingSpinner />
</AuthLoading>
<RedirectToSignIn />
<SignedIn>
{children}
</SignedIn>
</>
);
}
`
```

### 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 <AuthView pathname={pathname} />;
}
`
```

### 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 (
<>
<RedirectToSignIn />
<SignedIn>
{view === 'settings' && <AccountSettingsCards />}
{view === 'security' && (
<>
<ChangePasswordCard />
<SecuritySettingsCards />
</>
)}
{view === 'sessions' && <SessionsCard />}
</SignedIn>
</>
);
}
`
```

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

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

## FAQ / Troubleshooting

### Anonymous access not working?

When using `allowAnonymous: true`, you must grant permissions to the `anonymous` role in your database:

```
`-- Grant SELECT on specific tables
GRANT SELECT ON public.posts TO anonymous;
GRANT SELECT ON public.products TO anonymous;

-- Or grant on all tables in schema (be careful!)
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anonymous;

-- For INSERT/UPDATE/DELETE (if needed)
GRANT INSERT, UPDATE ON public.comments TO anonymous;
`
```

Your RLS policies must also allow the anonymous role:

```
`-- Example: Allow anonymous users to read published posts
CREATE POLICY "Anyone can read published posts"
ON public.posts FOR SELECT
USING (published = true);

-- Example: Allow anonymous users to read products
CREATE POLICY "Anyone can read products"
ON public.products FOR SELECT
USING (true);
`
```

### OAuth redirect not working in iframe?

OAuth automatically uses popup flow in iframes. Make sure:

* Your auth server allows the popup callback URL

* Popups aren't blocked by the browser

### Session not persisting after refresh?

Check that:

* Cookies are enabled

* Your auth server URL matches your domain (or has proper CORS)

* You're not in incognito mode with cookies blocked

### "Invalid credentials" but password is correct?

* Email might not be verified (check `emailVerification` setting)

* Account might be locked after too many failed attempts

* Check if using correct adapter API (Supabase vs BetterAuth style)

## More skills from neondatabase
claimable-postgresby neondatabaseInstant Postgres databases for local development, demos, prototyping, and test environments. No account required. Databases expire after 72 hours unless claimed to a Neon account.neon-postgres-branchesby neondatabaseThe outcome of this skill should be a created Neon branch (or a clear, actionable next step if creation cannot proceed). Choose the correct branch type, then execute branch creation via MCP or CLI.neon-postgres-egress-optimizerby neondatabaseGuide the user through diagnosing and fixing application-side query patterns that cause excessive data transfer (egress) from their Postgres database. Most high egress bills come from the application fetching more data than it uses.plugin-managerby neondatabaseManage plugin structure and configuration for this repository across both Cursor and Claude Code. Use when creating, updating, or reviewing plugin folders…skill-creatorby neondatabaseGuide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's…add-neon-docsby neondatabaseUse this skill when the user asks to add documentation, add docs, add references, or install documentation about Neon. Adds Neon best practices reference links…neon-authby neondatabaseSets up Neon Auth for your application. Configures authentication, creates auth routes, and generates UI components. Use when adding authentication to Next.js,…neon-drizzleby neondatabaseCreates a fully functional Drizzle ORM setup with a provisioned Neon database. Installs dependencies, provisions database credentials, configures connections,…

---

**Source**: https://github.com/neondatabase/neon-js/tree/HEAD/skills/neon-auth-react
**Author**: neondatabase
**Discovered via**: mcpservers.org

Related skills 6

caveman

★ Featured

Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, wenyan-lite, wenyan-full, wenyan-ultra. Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.

juliusbrussee 167k
Development

secure-linux-web-hosting

★ Featured

Use when setting up, hardening, or reviewing a cloud server for self-hosting, including DNS, SSH, firewalls, Nginx, static-site hosting, reverse-proxying an app, HTTPS with Let's Encrypt or ACME clients, safe HTTP-to-HTTPS redirects, or optional post-launch network tuning such as BBR.

xixu-me 155k
Development

readme-i18n

★ Featured

Use when the user wants to translate a repository README, make a repo multilingual, localize docs, add a language switcher, internationalize the README, or update localized README variants in a GitHub-style repository.

xixu-me 155k
Development

lark-shared

★ Featured

Use when first setting up lark-cli, running auth login, switching user/bot identity (--as), handling permission denied or scope errors, needing to update lark-cli, or seeing _notice in JSON output.

larksuite 155k
Development

improve-codebase-architecture

★ Featured

Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable.

mattpocock 151k
Development

paper-context-resolver

★ Featured

Optional RigorPilot helper for README-first deep learning repo reproduction. Use only when the README and repository files leave a narrow reproduction-critical gap and the task is to resolve a specific paper detail such as dataset split, preprocessing, evaluation protocol, checkpoint mapping, or runtime assumption from primary paper sources while recording conflicts. Do not use for general paper summary, repo scanning, environment setup, command execution, title-only paper lookup, or replacin...

lllllllama 127k
Development