neon-js-react

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Neon JS for React

用于 React 的 Neon JS

Help developers set up @neondatabase/neon-js with authentication AND database queries in React applications (Vite, CRA, etc.).
帮助开发者在React应用(Vite、CRA等)中配置@neondatabase/neon-js,同时实现认证与数据库查询功能。

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/
    )
在以下场景使用本技能:
  • 在React应用(Vite、CRA等)中配置Neon认证+数据库
  • 用户同时需要认证和数据库查询功能
  • 用户提及“neon-js”、“neon auth + database”或“full neon SDK”
  • 用户未使用Next.js(若使用Next.js,可从
    neon-auth-nextjs
    入手并添加Data API配置,或查看
    examples/nextjs-neon-auth/

Critical Rules

关键规则

  1. Adapter Factory Pattern: Always call adapters with
    ()
    typescript
    adapter: SupabaseAuthAdapter()  // Correct
    adapter: SupabaseAuthAdapter    // Wrong - missing ()
  2. React Adapter Import: NOT exported from main - use subpath
    typescript
    import { BetterAuthReactAdapter } from '@neondatabase/neon-js/auth/react/adapters';
  3. Type Safety: Always use Database generic for type-safe queries
    typescript
    const client = createClient<Database>({...});
  4. CSS Import: Choose ONE - either
    /ui/css
    OR
    /ui/tailwind
    , never both

  1. 适配器工厂模式:调用适配器时必须添加
    ()
    typescript
    adapter: SupabaseAuthAdapter()  // 正确
    adapter: SupabaseAuthAdapter    // 错误 - 缺少()
  2. React适配器导入:不从主包导出,需使用子路径
    typescript
    import { BetterAuthReactAdapter } from '@neondatabase/neon-js/auth/react/adapters';
  3. 类型安全:始终使用Database泛型实现类型安全查询
    typescript
    const client = createClient<Database>({...});
  4. CSS导入:二选一 - 要么导入
    /ui/css
    ,要么导入
    /ui/tailwind
    ,切勿同时导入两者

Setup

配置步骤

1. Install

1. 安装

bash
npm install @neondatabase/neon-js
bash
npm install @neondatabase/neon-js

2. Generate Database Types

2. 生成数据库类型

bash
npx neon-js gen-types --db-url "postgresql://user:pass@host:5432/db" --output src/database.types.ts
CLI Options:
bash
npx neon-js gen-types --db-url <url> [options]
bash
npx neon-js gen-types --db-url "postgresql://user:pass@host:5432/db" --output src/database.types.ts
CLI选项:
bash
npx neon-js gen-types --db-url <url> [options]

Required

必填项

--db-url <url> Database connection string
--db-url <url> 数据库连接字符串

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)
undefined
--output, -o <path> 输出文件路径(默认:database.types.ts) --schema, -s <name> 要包含的模式(可重复指定,默认:public) --postgrest-v9-compat 禁用一对一关系检测 --query-timeout <duration> 查询超时时间(例如:30s、1m,默认:15s)
undefined

3. Create Client (
src/client.ts
)

3. 创建客户端(
src/client.ts

typescript
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,
  },
});
typescript
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, // 启用后支持无需登录的RLS访问
  },
  dataApi: {
    url: import.meta.env.VITE_NEON_DATA_API_URL,
  },
});

4. Create Provider (
src/providers.tsx
)

4. 创建Provider(
src/providers.tsx

typescript
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>
  );
}
typescript
import { NeonAuthUIProvider } from '@neondatabase/neon-js/auth/react';
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { neonClient } from './client';

// 导入CSS(二选一)
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
)

5. 包裹应用(
src/main.tsx

typescript
import { BrowserRouter } from 'react-router-dom';
import { Providers } from './providers';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <BrowserRouter>
    <Providers>
      <App />
    </Providers>
  </BrowserRouter>
);
typescript
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
)

6. 环境变量(
.env.local

VITE_NEON_AUTH_URL=https://your-auth.neon.tech
VITE_NEON_DATA_API_URL=https://your-data-api.neon.tech/rest/v1

VITE_NEON_AUTH_URL=https://your-auth.neon.tech
VITE_NEON_DATA_API_URL=https://your-data-api.neon.tech/rest/v1

CSS & Styling

CSS与样式

Import Options

导入选项

Without Tailwind (pre-built CSS bundle ~47KB):
typescript
// In provider or main.tsx
import '@neondatabase/neon-js/ui/css';
With Tailwind CSS v4:
css
@import 'tailwindcss';
@import '@neondatabase/neon-js/ui/tailwind';
IMPORTANT: Never import both - causes duplicate styles.
不使用Tailwind(预构建CSS包约47KB):
typescript
// 在Provider或main.tsx中导入
import '@neondatabase/neon-js/ui/css';
使用Tailwind CSS v4
css
@import 'tailwindcss';
@import '@neondatabase/neon-js/ui/tailwind';
重要提示:切勿同时导入两者,否则会导致样式重复。

Dark Mode

深色模式

typescript
<NeonAuthUIProvider
  defaultTheme="system" // 'light' | 'dark' | 'system'
  // ...
>
typescript
<NeonAuthUIProvider
  defaultTheme="system" // 'light' | 'dark' | 'system'
  // ...
>

Custom Theming

自定义主题

Override CSS variables in your stylesheet:
css
: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);
}

在样式表中覆盖CSS变量:
css
: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

NeonAuthUIProvider 属性

Full configuration:
typescript
<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>

完整配置:
typescript
<NeonAuthUIProvider
  // 必填项
  authClient={neonClient.auth}  // 注意:neonClient的.auth属性

  // 导航配置
  navigate={navigate}
  Link={({href, children}) => <Link to={href}>{children}</Link>}
  redirectTo="/dashboard"

  // 社交/OAuth配置
  social={{
    providers: ['google'],
  }}

  // 功能开关
  emailOTP={true}
  emailVerification={true}
  magicLink={false}
  multiSession={false}
  credentials={{ forgotPassword: true }}

  // 注册字段
  signUp={{ fields: ['name'] }}

  // 账户字段
  account={{ fields: ['image', 'name', 'company'] }}

  // 组织配置
  organization={{}}

  // 深色模式
  defaultTheme="system"

  // 自定义标签
  localization={{
    SIGN_IN: '欢迎回来',
    SIGN_UP: '创建账户',
  }}
>
  {children}
</NeonAuthUIProvider>

Database Queries

数据库查询

Select

查询数据

typescript
// 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();
typescript
// 基础查询
const { data, error } = await neonClient
  .from('todos')
  .select('*');

// 带过滤条件的查询
const { data, error } = await neonClient
  .from('todos')
  .select('*')
  .eq('user_id', userId)
  .order('created_at', { ascending: false });

// 关联查询
const { data, error } = await neonClient
  .from('posts')
  .select(`
    *,
    author:users(name, avatar),
    comments(id, content)
  `);

// 查询单行数据
const { data, error } = await neonClient
  .from('todos')
  .select('*')
  .eq('id', todoId)
  .single();

Insert

插入数据

typescript
// 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();
typescript
// 插入单行
const { data, error } = await neonClient
  .from('todos')
  .insert({ title: 'New todo', user_id: userId })
  .select()
  .single();

// 批量插入
const { data, error } = await neonClient
  .from('todos')
  .insert([
    { title: 'Todo 1', user_id: userId },
    { title: 'Todo 2', user_id: userId },
  ])
  .select();

Update

更新数据

typescript
const { data, error } = await neonClient
  .from('todos')
  .update({ completed: true })
  .eq('id', todoId)
  .select()
  .single();
typescript
const { data, error } = await neonClient
  .from('todos')
  .update({ completed: true })
  .eq('id', todoId)
  .select()
  .single();

Delete

删除数据

typescript
const { error } = await neonClient
  .from('todos')
  .delete()
  .eq('id', todoId);
typescript
const { error } = await neonClient
  .from('todos')
  .delete()
  .eq('id', todoId);

Upsert

插入或更新数据(Upsert)

typescript
const { data, error } = await neonClient
  .from('profiles')
  .upsert({ user_id: userId, bio: 'Updated bio' })
  .select()
  .single();
typescript
const { data, error } = await neonClient
  .from('profiles')
  .upsert({ user_id: userId, bio: 'Updated bio' })
  .select()
  .single();

Filters

过滤条件

typescript
// 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
typescript
// 相等判断
.eq('column', value)
.neq('column', value)

// 比较判断
.gt('column', value)      // 大于
.gte('column', value)     // 大于等于
.lt('column', value)      // 小于
.lte('column', value)     // 小于等于

// 模式匹配
.like('column', '%pattern%')
.ilike('column', '%pattern%')  // 不区分大小写

// 数组相关
.in('column', [1, 2, 3])
.contains('tags', ['javascript'])
.containedBy('tags', ['javascript', 'typescript'])

// 空值判断
.is('column', null)
.not('column', 'is', null)

// 范围分页
.range(0, 9)  // 分页

Ordering & Pagination

排序与分页

typescript
const { data, error } = await neonClient
  .from('posts')
  .select('*')
  .order('created_at', { ascending: false })
  .range(0, 9)  // First 10 items
  .limit(10);

typescript
const { data, error } = await neonClient
  .from('posts')
  .select('*')
  .order('created_at', { ascending: false })
  .range(0, 9)  // 前10条数据
  .limit(10);

Auth Methods

认证方法

Default API (BetterAuth)

默认API(BetterAuth)

typescript
// 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();
typescript
// 注册
await neonClient.auth.signUp.email({ email, password, name });

// 登录
await neonClient.auth.signIn.email({ email, password });

// OAuth登录
await neonClient.auth.signIn.social({
  provider: 'google',
  callbackURL: '/dashboard',
});

// 获取会话
const session = await neonClient.auth.getSession();

// 登出
await neonClient.auth.signOut();

With SupabaseAuthAdapter

使用SupabaseAuthAdapter

typescript
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'
});
typescript
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风格的方法
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();

// 事件监听
neonClient.auth.onAuthStateChange((event, session) => {
  console.log(event); // 'SIGNED_IN', 'SIGNED_OUT', 'TOKEN_REFRESHED'
});

With BetterAuthReactAdapter

使用BetterAuthReactAdapter

typescript
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();

typescript
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,
  },
});

// 包含useSession()钩子
const { data, isPending, error } = neonClient.auth.useSession();

Session Hook

会话钩子

typescript
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:
typescript
{
  user: {
    id: string;
    email: string;
    name: string;
    image?: string;
    emailVerified: boolean;
  };
  session: {
    id: string;
    token: string;
    expiresAt: Date;
  };
}

typescript
function MyComponent() {
  const { data: session, isPending, error, refetch } = neonClient.auth.useSession();

  if (isPending) return <div>加载中...</div>;
  if (error) return <div>错误:{error.message}</div>;
  if (!session) return <div>未登录</div>;

  return (
    <div>
      <p>你好,{session.user.name}</p>
      <p>邮箱:{session.user.email}</p>
    </div>
  );
}
会话结构
typescript
{
  user: {
    id: string;
    email: string;
    name: string;
    image?: string;
    emailVerified: boolean;
  };
  session: {
    id: string;
    token: string;
    expiresAt: Date;
  };
}

UI Components

UI组件

AuthView - Main Auth Interface

AuthView - 主认证界面

typescript
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
typescript
import { AuthView } from '@neondatabase/neon-js/auth/react';

// 路由:/auth/:pathname
function AuthPage() {
  const { pathname } = useParams();
  return <AuthView pathname={pathname} />;
}
路径名称
sign-in
sign-up
forgot-password
reset-password
callback
sign-out

Conditional Rendering

条件渲染

typescript
import {
  SignedIn,
  SignedOut,
  AuthLoading,
  RedirectToSignIn,
} from '@neondatabase/neon-js/auth/react';

function MyPage() {
  return (
    <>
      <AuthLoading>
        <LoadingSpinner />
      </AuthLoading>

      <SignedIn>
        <Dashboard />
      </SignedIn>

      <SignedOut>
        <LandingPage />
      </SignedOut>

      <RedirectToSignIn />
    </>
  );
}
typescript
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

UserButton

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

function Header() {
  return (
    <header>
      <UserButton />
    </header>
  );
}
typescript
import { UserButton } from '@neondatabase/neon-js/auth/react';

function Header() {
  return (
    <header>
      <UserButton />
    </header>
  );
}

Account Management

账户管理

typescript
import {
  AccountSettingsCards,
  SecuritySettingsCards,
  SessionsCard,
  ChangePasswordCard,
  ChangeEmailCard,
  DeleteAccountCard,
  ProvidersCard,
} from '@neondatabase/neon-js/auth/react';
typescript
import {
  AccountSettingsCards,
  SecuritySettingsCards,
  SessionsCard,
  ChangePasswordCard,
  ChangeEmailCard,
  DeleteAccountCard,
  ProvidersCard,
} from '@neondatabase/neon-js/auth/react';

Organization Components

组织组件

typescript
import {
  OrganizationSwitcher,
  OrganizationSettingsCards,
  OrganizationMembersCard,
} from '@neondatabase/neon-js/auth/react';

typescript
import {
  OrganizationSwitcher,
  OrganizationSettingsCards,
  OrganizationMembersCard,
} from '@neondatabase/neon-js/auth/react';

Social/OAuth Providers

社交/OAuth提供商

Configuration

配置

typescript
<NeonAuthUIProvider
  social={{
    providers: ['google'],
  }}
>
typescript
<NeonAuthUIProvider
  social={{
    providers: ['google'],
  }}
>

Programmatic Sign-In

程序化登录

typescript
await neonClient.auth.signIn.social({
  provider: 'google',
  callbackURL: '/dashboard',
});
typescript
await neonClient.auth.signIn.social({
  provider: 'google',
  callbackURL: '/dashboard',
});

Supported Providers

支持的提供商

google
,
github
,
twitter
,
discord
,
apple
,
microsoft
,
facebook
,
linkedin
,
spotify
,
twitch
,
gitlab
,
bitbucket

google
github
twitter
discord
apple
microsoft
facebook
linkedin
spotify
twitch
gitlab
bitbucket

Protected Routes

受保护路由

typescript
// 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>
    </>
  );
}

typescript
// routes.tsx
import { Routes, Route } from 'react-router-dom';

export function AppRoutes() {
  return (
    <Routes>
      {/* 公开路由 */}
      <Route path="/" element={<HomePage />} />

      {/* 认证路由 */}
      <Route path="/auth/:pathname" element={<AuthPage />} />

      {/* 受保护路由 */}
      <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:
typescript
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('*');
启用基于RLS的数据访问,支持未认证用户:
typescript
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,
  },
});

// 无需登录即可执行查询(使用匿名JWT)
const { data } = await neonClient.from('public_posts').select('*');

Get JWT Token

获取JWT令牌

typescript
const token = await neonClient.auth.getJWTToken();

// Use for external API calls
const response = await fetch('/api/external', {
  headers: { Authorization: `Bearer ${token}` },
});
typescript
const token = await neonClient.auth.getJWTToken();

// 用于外部API调用
const response = await fetch('/api/external', {
  headers: { Authorization: `Bearer ${token}` },
});

Identity Linking

身份关联

typescript
// 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' });
typescript
// 列出已关联的账户
const { data } = await neonClient.auth.getUserIdentities();

// 关联新的提供商
await neonClient.auth.linkIdentity({
  provider: 'github',
  options: { redirectTo: '/account/security' },
});

// 解除关联提供商
await neonClient.auth.unlinkIdentity({ identity_id: 'id' });

Auth State Events (Supabase Adapter)

认证状态事件(Supabase Adapter)

typescript
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();
typescript
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;
  }
});

// 清理
subscription.unsubscribe();

Cross-Tab Sync

跨标签页同步

Automatic via BroadcastChannel. Sign out in one tab signs out all tabs.

通过BroadcastChannel自动实现。在一个标签页登出,所有标签页都会同步登出。

Error Handling

错误处理

Query Errors

查询错误

typescript
const { data, error } = await neonClient.from('todos').select('*');

if (error) {
  console.error('Query failed:', error.message);
  return;
}

// Use data safely
console.log(data);
typescript
const { data, error } = await neonClient.from('todos').select('*');

if (error) {
  console.error('查询失败:', error.message);
  return;
}

// 安全使用数据
console.log(data);

Auth Errors

认证错误

typescript
const { error } = await neonClient.auth.signIn.email({ email, password });

if (error) {
  toast.error(error.message);
  return;
}
typescript
const { error } = await neonClient.auth.signIn.email({ email, password });

if (error) {
  toast.error(error.message);
  return;
}

Common Errors

常见错误

ErrorCause
Invalid credentials
Wrong email/password
User already exists
Email registered
permission denied for table
Missing RLS policy or GRANT
JWT expired
Token needs refresh

错误信息原因
Invalid credentials
邮箱/密码错误
User already exists
邮箱已注册
permission denied for table
缺少RLS策略或GRANT权限
JWT expired
令牌需要刷新

FAQ / Troubleshooting

FAQ / 故障排除

Anonymous access not working?

匿名访问无法工作?

Grant permissions to the
anonymous
role in your database:
sql
-- 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);
在数据库中为
anonymous
角色授予权限:
sql
-- 为特定表授予SELECT权限
GRANT SELECT ON public.posts TO anonymous;
GRANT SELECT ON public.products TO anonymous;

-- 为匿名访问创建RLS策略
CREATE POLICY "任何人都可以阅读已发布的帖子"
  ON public.posts FOR SELECT
  USING (published = true);

"permission denied for table" error?

出现“permission denied for table”错误?

  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;
  1. 检查RLS是否启用:
    ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
  2. 为认证用户创建合适的策略
  3. 授予权限:
    GRANT SELECT, INSERT ON public.posts TO authenticated;

Database types out of date?

数据库类型过时?

Regenerate types after schema changes:
bash
npx neon-js gen-types --db-url "postgresql://..." --output src/database.types.ts
在模式变更后重新生成类型:
bash
npx neon-js gen-types --db-url "postgresql://..." --output src/database.types.ts

OAuth not working in iframe?

OAuth在iframe中无法工作?

OAuth automatically uses popup flow in iframes. Ensure popups aren't blocked.
OAuth在iframe中会自动使用弹窗流程。确保弹窗未被阻止。

Session not persisting?

会话无法持久化?

  1. Cookies enabled?
  2. Auth URL correct in
    .env.local
    ?
  3. Not in incognito with cookies blocked?

  1. 已启用Cookie?
  2. .env.local
    中的认证URL是否正确?
  3. 是否在阻止Cookie的隐身模式下操作?

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
  • 会话缓存:60秒TTL
  • 请求去重:并发请求共享单个请求
  • 自动令牌注入:JWT自动添加到所有查询中
  • 跨标签页同步:通过BroadcastChannel实现,延迟<50ms