Loading...
Loading...
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.
npx skill4agent add neondatabase/neon-js neon-js-reactneon-auth-nextjsexamples/nextjs-neon-auth/()adapter: SupabaseAuthAdapter() // Correct
adapter: SupabaseAuthAdapter // Wrong - missing ()import { BetterAuthReactAdapter } from '@neondatabase/neon-js/auth/react/adapters';const client = createClient<Database>({...});/ui/css/ui/tailwindnpm install @neondatabase/neon-jsnpx neon-js gen-types --db-url "postgresql://user:pass@host:5432/db" --output src/database.types.tsnpx 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.tsimport { 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.tsximport { 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.tsximport { BrowserRouter } from 'react-router-dom';
import { Providers } from './providers';
import App from './App';
createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<Providers>
<App />
</Providers>
</BrowserRouter>
);.env.localVITE_NEON_AUTH_URL=https://your-auth.neon.tech
VITE_NEON_DATA_API_URL=https://your-data-api.neon.tech/rest/v1// In provider or main.tsx
import '@neondatabase/neon-js/ui/css';@import 'tailwindcss';
@import '@neondatabase/neon-js/ui/tailwind';<NeonAuthUIProvider
defaultTheme="system" // 'light' | 'dark' | 'system'
// ...
>: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
// 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) // paginationconst { 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>
);
}{
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} />;
}sign-insign-upforgot-passwordreset-passwordcallbacksign-outimport {
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',
});googlegithubtwitterdiscordapplemicrosoftfacebooklinkedinspotifytwitchgitlabbitbucket// 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>
</>
);
}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();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 |
|---|---|
| Wrong email/password |
| Email registered |
| Missing RLS policy or GRANT |
| Token needs refresh |
anonymous-- 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;npx neon-js gen-types --db-url "postgresql://..." --output src/database.types.ts.env.local