一键导入
neon-js-react
// 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.
// 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.
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 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).
| name | neon-js-react |
| description | 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. |
| allowed-tools | ["Bash","Write","Read","Edit","Glob","Grep"] |
Help developers set up @neondatabase/neon-js with authentication AND database queries in React applications (Vite, CRA, etc.).
Use this skill when:
neon-auth-nextjs as a starting point and add Data API configuration, or see examples/nextjs-neon-auth/)Adapter Factory Pattern: Always call adapters with ()
adapter: SupabaseAuthAdapter() // Correct
adapter: SupabaseAuthAdapter // Wrong - missing ()
React Adapter Import: NOT exported from main - use subpath
import { BetterAuthReactAdapter } from '@neondatabase/neon-js/auth/react/adapters';
Type Safety: Always use Database generic for type-safe queries
const client = createClient<Database>({...});
CSS Import: Choose ONE - either /ui/css OR /ui/tailwind, never both
npm install @neondatabase/neon-js
npx neon-js gen-types --db-url "postgresql://user:pass@host:5432/db" --output src/database.types.ts
CLI Options:
npx neon-js gen-types --db-url <url> [options]
# Required
--db-url <url> Database connection string
# Optional
--output, -o <path> Output file (default: database.types.ts)
--schema, -s <name> Schema to include (repeatable, default: public)
--postgrest-v9-compat Disable one-to-one relationship detection
--query-timeout <duration> Query timeout (e.g., 30s, 1m, default: 15s)
src/client.ts)import { createClient } from '@neondatabase/neon-js';
import type { Database } from './database.types';
export const neonClient = createClient<Database>({
auth: {
url: import.meta.env.VITE_NEON_AUTH_URL,
// allowAnonymous: true, // Enable for RLS access without login
},
dataApi: {
url: import.meta.env.VITE_NEON_DATA_API_URL,
},
});
src/providers.tsx)import { NeonAuthUIProvider } from '@neondatabase/neon-js/auth/react';
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { neonClient } from './client';
// Import CSS (choose one)
import '@neondatabase/neon-js/ui/css';
export function Providers({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
return (
<NeonAuthUIProvider
authClient={neonClient.auth}
navigate={navigate}
redirectTo="/dashboard"
Link={({href, children}) => <Link to={href}>{children}</Link>}
>
{children}
</NeonAuthUIProvider>
);
}
src/main.tsx)import { BrowserRouter } from 'react-router-dom';
import { Providers } from './providers';
import App from './App';
createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<Providers>
<App />
</Providers>
</BrowserRouter>
);
.env.local)VITE_NEON_AUTH_URL=https://your-auth.neon.tech
VITE_NEON_DATA_API_URL=https://your-data-api.neon.tech/rest/v1
Without Tailwind (pre-built CSS bundle ~47KB):
// In provider or main.tsx
import '@neondatabase/neon-js/ui/css';
With Tailwind CSS v4:
@import 'tailwindcss';
@import '@neondatabase/neon-js/ui/tailwind';
IMPORTANT: Never import both - causes duplicate styles.
<NeonAuthUIProvider
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);
--border: oklch(0.9 0 0);
--radius: 0.5rem;
}
.dark {
--background: oklch(0.15 0 0);
--foreground: oklch(0.98 0 0);
}
Full configuration:
<NeonAuthUIProvider
// Required
authClient={neonClient.auth} // Note: .auth property of neonClient
// Navigation
navigate={navigate}
Link={({href, children}) => <Link to={href}>{children}</Link>}
redirectTo="/dashboard"
// Social/OAuth
social={{
providers: ['google'],
}}
// Feature Flags
emailOTP={true}
emailVerification={true}
magicLink={false}
multiSession={false}
credentials={{ forgotPassword: true }}
// Sign Up Fields
signUp={{ fields: ['name'] }}
// Account Fields
account={{ fields: ['image', 'name', 'company'] }}
// Organizations
organization={{}}
// Dark Mode
defaultTheme="system"
// Custom Labels
localization={{
SIGN_IN: 'Welcome Back',
SIGN_UP: 'Create Account',
}}
>
{children}
</NeonAuthUIProvider>
// Basic select
const { data, error } = await neonClient
.from('todos')
.select('*');
// Select with filter
const { data, error } = await neonClient
.from('todos')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false });
// Select with relations
const { data, error } = await neonClient
.from('posts')
.select(`
*,
author:users(name, avatar),
comments(id, content)
`);
// Single row
const { data, error } = await neonClient
.from('todos')
.select('*')
.eq('id', todoId)
.single();
// Single insert
const { data, error } = await neonClient
.from('todos')
.insert({ title: 'New todo', user_id: userId })
.select()
.single();
// Bulk insert
const { data, error } = await neonClient
.from('todos')
.insert([
{ title: 'Todo 1', user_id: userId },
{ title: 'Todo 2', user_id: userId },
])
.select();
const { data, error } = await neonClient
.from('todos')
.update({ completed: true })
.eq('id', todoId)
.select()
.single();
const { error } = await neonClient
.from('todos')
.delete()
.eq('id', todoId);
const { data, error } = await neonClient
.from('profiles')
.upsert({ user_id: userId, bio: 'Updated bio' })
.select()
.single();
// Equality
.eq('column', value)
.neq('column', value)
// Comparison
.gt('column', value) // greater than
.gte('column', value) // greater than or equal
.lt('column', value) // less than
.lte('column', value) // less than or equal
// Pattern matching
.like('column', '%pattern%')
.ilike('column', '%pattern%') // case insensitive
// Arrays
.in('column', [1, 2, 3])
.contains('tags', ['javascript'])
.containedBy('tags', ['javascript', 'typescript'])
// Null
.is('column', null)
.not('column', 'is', null)
// Range
.range(0, 9) // pagination
const { data, error } = await neonClient
.from('posts')
.select('*')
.order('created_at', { ascending: false })
.range(0, 9) // First 10 items
.limit(10);
// Sign up
await neonClient.auth.signUp.email({ email, password, name });
// Sign in
await neonClient.auth.signIn.email({ email, password });
// OAuth
await neonClient.auth.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
});
// Get session
const session = await neonClient.auth.getSession();
// Sign out
await neonClient.auth.signOut();
import { createClient, SupabaseAuthAdapter } from '@neondatabase/neon-js';
const neonClient = createClient<Database>({
auth: {
url: import.meta.env.VITE_NEON_AUTH_URL,
adapter: SupabaseAuthAdapter(),
},
dataApi: {
url: import.meta.env.VITE_NEON_DATA_API_URL,
},
});
// Supabase-style methods
await neonClient.auth.signUp({ email, password, options: { data: { name } } });
await neonClient.auth.signInWithPassword({ email, password });
await neonClient.auth.signInWithOAuth({ provider: 'google', options: { redirectTo } });
const { data: session } = await neonClient.auth.getSession();
await neonClient.auth.signOut();
// Event listener
neonClient.auth.onAuthStateChange((event, session) => {
console.log(event); // 'SIGNED_IN', 'SIGNED_OUT', 'TOKEN_REFRESHED'
});
import { createClient } from '@neondatabase/neon-js';
import { BetterAuthReactAdapter } from '@neondatabase/neon-js/auth/react/adapters';
const neonClient = createClient<Database>({
auth: {
url: import.meta.env.VITE_NEON_AUTH_URL,
adapter: BetterAuthReactAdapter(),
},
dataApi: {
url: import.meta.env.VITE_NEON_DATA_API_URL,
},
});
// Includes useSession() hook
const { data, isPending, error } = neonClient.auth.useSession();
function MyComponent() {
const { data: session, isPending, error, refetch } = neonClient.auth.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>
</div>
);
}
Session shape:
{
user: {
id: string;
email: string;
name: string;
image?: string;
emailVerified: boolean;
};
session: {
id: string;
token: string;
expiresAt: Date;
};
}
import { AuthView } from '@neondatabase/neon-js/auth/react';
// Route: /auth/:pathname
function AuthPage() {
const { pathname } = useParams();
return <AuthView pathname={pathname} />;
}
Pathnames: sign-in, sign-up, forgot-password, reset-password, callback, sign-out
import {
SignedIn,
SignedOut,
AuthLoading,
RedirectToSignIn,
} from '@neondatabase/neon-js/auth/react';
function MyPage() {
return (
<>
<AuthLoading>
<LoadingSpinner />
</AuthLoading>
<SignedIn>
<Dashboard />
</SignedIn>
<SignedOut>
<LandingPage />
</SignedOut>
<RedirectToSignIn />
</>
);
}
import { UserButton } from '@neondatabase/neon-js/auth/react';
function Header() {
return (
<header>
<UserButton />
</header>
);
}
import {
AccountSettingsCards,
SecuritySettingsCards,
SessionsCard,
ChangePasswordCard,
ChangeEmailCard,
DeleteAccountCard,
ProvidersCard,
} from '@neondatabase/neon-js/auth/react';
import {
OrganizationSwitcher,
OrganizationSettingsCards,
OrganizationMembersCard,
} from '@neondatabase/neon-js/auth/react';
<NeonAuthUIProvider
social={{
providers: ['google'],
}}
>
await neonClient.auth.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
});
google, github, twitter, discord, apple, microsoft, facebook, linkedin, spotify, twitch, gitlab, bitbucket
// routes.tsx
import { Routes, Route } from 'react-router-dom';
export function AppRoutes() {
return (
<Routes>
{/* Public */}
<Route path="/" element={<HomePage />} />
{/* Auth */}
<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>
</>
);
}
Enable RLS-based data access for unauthenticated users:
const neonClient = createClient<Database>({
auth: {
url: import.meta.env.VITE_NEON_AUTH_URL,
allowAnonymous: true,
},
dataApi: {
url: import.meta.env.VITE_NEON_DATA_API_URL,
},
});
// Queries work without sign-in (using anonymous JWT)
const { data } = await neonClient.from('public_posts').select('*');
const token = await neonClient.auth.getJWTToken();
// Use for external API calls
const response = await fetch('/api/external', {
headers: { Authorization: `Bearer ${token}` },
});
// List linked accounts
const { data } = await neonClient.auth.getUserIdentities();
// Link new provider
await neonClient.auth.linkIdentity({
provider: 'github',
options: { redirectTo: '/account/security' },
});
// Unlink provider
await neonClient.auth.unlinkIdentity({ identity_id: 'id' });
const { data: { subscription } } = neonClient.auth.onAuthStateChange((event, session) => {
switch (event) {
case 'SIGNED_IN': /* ... */ break;
case 'SIGNED_OUT': /* ... */ break;
case 'TOKEN_REFRESHED': /* ... */ break;
case 'USER_UPDATED': /* ... */ break;
}
});
// Cleanup
subscription.unsubscribe();
Automatic via BroadcastChannel. Sign out in one tab signs out all tabs.
const { data, error } = await neonClient.from('todos').select('*');
if (error) {
console.error('Query failed:', error.message);
return;
}
// Use data safely
console.log(data);
const { error } = await neonClient.auth.signIn.email({ email, password });
if (error) {
toast.error(error.message);
return;
}
| Error | Cause |
|---|---|
Invalid credentials | Wrong email/password |
User already exists | Email registered |
permission denied for table | Missing RLS policy or GRANT |
JWT expired | Token needs refresh |
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;
-- RLS policy for anonymous access
CREATE POLICY "Anyone can read published posts"
ON public.posts FOR SELECT
USING (published = true);
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;GRANT SELECT, INSERT ON public.posts TO authenticated;Regenerate types after schema changes:
npx neon-js gen-types --db-url "postgresql://..." --output src/database.types.ts
OAuth automatically uses popup flow in iframes. Ensure popups aren't blocked.
.env.local?