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 資料庫,適用於本地開發、展示、原型設計與測試環境。無需註冊帳號。資料庫在 72 小時後到期,除非認領至 Neon 帳號。
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