neon-js-react

작성자: neondatabase

React 앱(Vite, CRA)에서 인증 및 데이터베이스 쿼리를 포함한 전체 Neon SDK를 설정합니다. 타입화된 클라이언트를 생성하고, 데이터베이스 타입을 생성하며, 구성합니다…

npx skills add https://github.com/neondatabase/neon-js --skill neon-js-react

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

  1. Adapter Factory Pattern: Always call adapters with ()

    adapter: SupabaseAuthAdapter()  // Correct
    adapter: SupabaseAuthAdapter    // Wrong - missing ()
    
  2. React Adapter Import: NOT exported from main - use subpath

    import { BetterAuthReactAdapter } from '@neondatabase/neon-js/auth/react/adapters';
    
  3. Type Safety: Always use Database generic for type-safe queries

    const client = createClient<Database>({...});
    
  4. CSS Import: Choose ONE - either /ui/css OR /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

ErrorCause
Invalid credentialsWrong email/password
User already existsEmail registered
permission denied for tableMissing RLS policy or GRANT
JWT expiredToken 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?

  1. Check RLS is enabled: ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
  2. Create appropriate policies for authenticated users
  3. 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?

  1. Cookies enabled?
  2. Auth URL correct in .env.local?
  3. 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

neondatabase의 다른 스킬

claimable-postgres
neondatabase
로컬 개발, 데모, 프로토타이핑 및 테스트 환경을 위한 즉시 사용 가능한 Postgres 데이터베이스입니다. 계정이 필요하지 않습니다. 데이터베이스는 Neon 계정에 클레임되지 않으면 72시간 후에 만료됩니다.
official
neon-postgres-branches
neondatabase
이 스킬의 결과는 생성된 Neon 브랜치(또는 생성이 진행될 수 없는 경우 명확하고 실행 가능한 다음 단계)여야 합니다. 올바른 브랜치 유형을 선택한 다음 MCP 또는 CLI를 통해 브랜치 생성을 실행합니다.
official
neon-postgres-egress-optimizer
neondatabase
사용자가 Postgres 데이터베이스에서 과도한 데이터 전송(이그레스)을 유발하는 애플리케이션 측 쿼리 패턴을 진단하고 수정하도록 안내합니다. 대부분의 높은 이그레스 비용은 애플리케이션이 실제 사용하는 데이터보다 더 많은 데이터를 가져오기 때문에 발생합니다.
official
plugin-manager
neondatabase
이 저장소의 Cursor와 Claude Code 전반에 걸쳐 플러그인 구조와 구성을 관리합니다. 플러그인 폴더를 생성, 업데이트 또는 검토할 때 사용하세요…
official
skill-creator
neondatabase
효과적인 스킬을 생성하기 위한 가이드입니다. 이 스킬은 사용자가 Claude의 기능을 확장하는 새로운 스킬을 만들거나 기존 스킬을 업데이트하려 할 때 사용해야 합니다.
official
add-neon-docs
neondatabase
사용자가 Neon에 대한 문서 추가, 문서 추가, 참조 추가, 또는 문서 설치를 요청할 때 이 스킬을 사용하세요. Neon 모범 사례 참조 링크를 추가합니다…
official
neon-auth
neondatabase
애플리케이션에 Neon Auth를 설정합니다. 인증을 구성하고, 인증 라우트를 생성하며, UI 컴포넌트를 생성합니다. Next.js에 인증을 추가할 때 사용합니다.
official
neon-drizzle
neondatabase
완전한 기능을 갖춘 Drizzle ORM 설정을 프로비저닝된 Neon 데이터베이스와 함께 생성합니다. 종속성을 설치하고, 데이터베이스 자격 증명을 프로비저닝하며, 연결을 구성하고,…
official