بنقرة واحدة
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 auth-only to React apps (no database needed).
// Sets up Neon Auth in React applications (Vite, CRA). Configures authentication adapters, creates auth client, and sets up UI components. Use when adding auth-only to React apps (no database needed).
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).
Sets up the full Neon SDK with authentication AND database queries in React apps (Vite, CRA). Creates typed client, generates database types, and configures auth UI. Use for auth + database integration.
| 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 auth-only to React apps (no database needed). |
| allowed-tools | ["Bash","Write","Read","Edit","Glob","Grep"] |
Help developers set up @neondatabase/auth (authentication only, no database) in React applications with Vite, Create React App, or similar bundlers.
Use this skill when:
neon-auth-nextjs skill for Next.js)() - they are factory functions@neondatabase/auth/react/adapterscreateAuthClient(url, config)/ui/css OR /ui/tailwind, never bothnpm install @neondatabase/auth
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
}
);
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>
);
}
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>
);
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.
The provider includes next-themes for dark mode. Control via defaultTheme prop:
<NeonAuthUIProvider
authClient={authClient}
defaultTheme="system" // 'light' | 'dark' | 'system'
// ...
>
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 */
}
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>
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
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 />
</>
);
}
Dropdown menu with user avatar, name, and sign-out:
import { UserButton } from '@neondatabase/auth/react/ui';
function Header() {
return (
<header>
<nav>...</nav>
<UserButton />
</header>
);
}
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>
</>
);
}
import {
OrganizationSwitcher, // Switch between orgs
OrganizationSettingsCards, // Org settings
OrganizationMembersCard, // Member management
AcceptInvitationCard, // Accept org invite
} from '@neondatabase/auth/react/ui';
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();
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'
});
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
Enable providers in NeonAuthUIProvider:
<NeonAuthUIProvider
social={{
providers: ['google'],
}}
>
// 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',
},
});
google, github, twitter, discord, apple, microsoft, facebook, linkedin, spotify, twitch, gitlab, bitbucket
OAuth automatically uses popup flow when running in iframes (due to X-Frame-Options restrictions). No configuration needed.
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;
};
}
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?.();
const token = await authClient.getJWTToken();
const response = await fetch('/api/data', {
headers: {
Authorization: `Bearer ${token}`,
},
});
// 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 });
// 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://...',
},
});
// 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',
});
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();
Automatic via BroadcastChannel. Sign out in one tab signs out all tabs.
// 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>
</>
);
}
// 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} />;
}
// 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>
</>
);
}
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
}
| 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 {
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');
}
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 automatically uses popup flow in iframes. Make sure:
Check that:
emailVerification setting)