Labsco
neondatabase logo

neon-js-react

✓ Official13

by neondatabase · part of neondatabase/neon-js

Sets up the full Neon SDK with authentication AND database queries in React apps (Vite, CRA). Creates typed client, generates database types, and configures…

🔥🔥🔥✓ VerifiedFreeQuick setup
🧰 Not standalone. This skill ships with neondatabase/neon-js and only works together with that tool — install the tool first, then add this skill.

This is the playbook your agent receives when the skill activates — you don't need to read it to use the skill, but it's here to audit before installing.

by neondatabase

Sets up the full Neon SDK with authentication AND database queries in React apps (Vite, CRA). Creates typed client, generates database types, and configures… npx skills add https://github.com/neondatabase/neon-js --skill neon-js-react Download ZIPGitHub13

Neon 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-nextjs as a starting point and add Data API configuration, or see examples/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 ({...});

CSS Import: Choose ONE - either /ui/css OR /ui/tailwind, never both

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

 

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:

 {children} }
 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}
 

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

 return (
 
 Hello, {session.user.name}

 Email: {session.user.email}

 

 );
}

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 ;
}

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

 
 
 

 
 
 

 
 
 );
}

UserButton

import { UserButton } from '@neondatabase/neon-js/auth/react';

function Header() {
 return (
 
 
 
 );
}

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

 

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 (
 
 {/* Public */}
 } />

 {/* Auth */}
 } />

 {/* Protected */}
 } />
 } />
 
 );
}

// ProtectedRoute.tsx
function ProtectedRoute({ children }: { children: React.ReactNode }) {
 return (
 <>
 
 
 {children} 
 
 );
}

Advanced Features

Anonymous Access

Enable RLS-based data access for unauthenticated users:

const neonClient = createClient ({
 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

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