neon-js-react
Thiết lập đầy đủ Neon SDK với xác thực VÀ truy vấn cơ sở dữ liệu trong các ứng dụng React (Vite, CRA). Tạo client có kiểu, sinh kiểu cơ sở dữ liệu, và cấu hình…
npx skills add https://github.com/neondatabase/neon-js --skill neon-js-reactNeon JS for React
Help developers set up @neondatabase/neon-js with authentication AND database queries in React applications (Vite, CRA, etc.).
When to Use
Use this skill when:
- Setting up Neon Auth + Database in a React app (Vite, CRA, etc.)
- User needs both authentication AND database queries
- User mentions "neon-js", "neon auth + database", or "full neon SDK"
- User is NOT using Next.js (for Next.js, use
neon-auth-nextjsas a starting point and add Data API configuration, or seeexamples/nextjs-neon-auth/)
Critical Rules
-
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/cssOR/ui/tailwind, never both
Setup
1. Install
npm install @neondatabase/neon-js
2. Generate Database Types
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)
3. Create Client (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,
},
});
4. Create Provider (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>
);
}
5. Wrap App (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>
);
6. Environment Variables (.env.local)
VITE_NEON_AUTH_URL=https://your-auth.neon.tech
VITE_NEON_DATA_API_URL=https://your-data-api.neon.tech/rest/v1
CSS & Styling
Import Options
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.
Dark Mode
<NeonAuthUIProvider
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);
--border: oklch(0.9 0 0);
--radius: 0.5rem;
}
.dark {
--background: oklch(0.15 0 0);
--foreground: oklch(0.98 0 0);
}
NeonAuthUIProvider Props
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>
Database Queries
Select
// 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();
Insert
// 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();
Update
const { data, error } = await neonClient
.from('todos')
.update({ completed: true })
.eq('id', todoId)
.select()
.single();
Delete
const { error } = await neonClient
.from('todos')
.delete()
.eq('id', todoId);
Upsert
const { data, error } = await neonClient
.from('profiles')
.upsert({ user_id: userId, bio: 'Updated bio' })
.select()
.single();
Filters
// 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
Ordering & Pagination
const { data, error } = await neonClient
.from('posts')
.select('*')
.order('created_at', { ascending: false })
.range(0, 9) // First 10 items
.limit(10);
Auth Methods
Default API (BetterAuth)
// 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();
With SupabaseAuthAdapter
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'
});
With BetterAuthReactAdapter
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();
Session Hook
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;
};
}
UI Components
AuthView - Main Auth Interface
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
Conditional Rendering
import {
SignedIn,
SignedOut,
AuthLoading,
RedirectToSignIn,
} from '@neondatabase/neon-js/auth/react';
function MyPage() {
return (
<>
<AuthLoading>
<LoadingSpinner />
</AuthLoading>
<SignedIn>
<Dashboard />
</SignedIn>
<SignedOut>
<LandingPage />
</SignedOut>
<RedirectToSignIn />
</>
);
}
UserButton
import { UserButton } from '@neondatabase/neon-js/auth/react';
function Header() {
return (
<header>
<UserButton />
</header>
);
}
Account Management
import {
AccountSettingsCards,
SecuritySettingsCards,
SessionsCard,
ChangePasswordCard,
ChangeEmailCard,
DeleteAccountCard,
ProvidersCard,
} from '@neondatabase/neon-js/auth/react';
Organization Components
import {
OrganizationSwitcher,
OrganizationSettingsCards,
OrganizationMembersCard,
} from '@neondatabase/neon-js/auth/react';
Social/OAuth Providers
Configuration
<NeonAuthUIProvider
social={{
providers: ['google'],
}}
>
Programmatic Sign-In
await neonClient.auth.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
});
Supported Providers
google, github, twitter, discord, apple, microsoft, facebook, linkedin, spotify, twitch, gitlab, bitbucket
Protected Routes
// 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>
</>
);
}
Advanced Features
Anonymous Access
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('*');
Get JWT Token
const token = await neonClient.auth.getJWTToken();
// Use for external API calls
const response = await fetch('/api/external', {
headers: { Authorization: `Bearer ${token}` },
});
Identity Linking
// 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' });
Auth State Events (Supabase Adapter)
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();
Cross-Tab Sync
Automatic via BroadcastChannel. Sign out in one tab signs out all tabs.
Error Handling
Query Errors
const { data, error } = await neonClient.from('todos').select('*');
if (error) {
console.error('Query failed:', error.message);
return;
}
// Use data safely
console.log(data);
Auth Errors
const { error } = await neonClient.auth.signIn.email({ email, password });
if (error) {
toast.error(error.message);
return;
}
Common Errors
| 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 |
FAQ / Troubleshooting
Anonymous access not working?
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);
"permission denied for table" error?
- Check RLS is enabled:
ALTER TABLE posts ENABLE ROW LEVEL SECURITY; - Create appropriate policies for authenticated users
- Grant permissions:
GRANT SELECT, INSERT ON public.posts TO authenticated;
Database types out of date?
Regenerate types after schema changes:
npx neon-js gen-types --db-url "postgresql://..." --output src/database.types.ts
OAuth not working in iframe?
OAuth automatically uses popup flow in iframes. Ensure popups aren't blocked.
Session not persisting?
- Cookies enabled?
- Auth URL correct in
.env.local? - Not in incognito with cookies blocked?
Performance Notes
- Session caching: 60-second TTL
- Request deduplication: Concurrent calls share single request
- Auto token injection: JWT automatically added to all queries
- Cross-tab sync: <50ms via BroadcastChannel