neon-js-react
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNeon 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 as a starting point and add Data API configuration, or see
neon-auth-nextjs)examples/nextjs-neon-auth/
在以下场景使用本技能:
- 在React应用(Vite、CRA等)中配置Neon认证+数据库
- 用户同时需要认证和数据库查询功能
- 用户提及“neon-js”、“neon auth + database”或“full neon SDK”
- 用户未使用Next.js(若使用Next.js,可从入手并添加Data API配置,或查看
neon-auth-nextjs)examples/nextjs-neon-auth/
Critical Rules
关键规则
-
Adapter Factory Pattern: Always call adapters with
()typescriptadapter: SupabaseAuthAdapter() // Correct adapter: SupabaseAuthAdapter // Wrong - missing () -
React Adapter Import: NOT exported from main - use subpathtypescript
import { BetterAuthReactAdapter } from '@neondatabase/neon-js/auth/react/adapters'; -
Type Safety: Always use Database generic for type-safe queriestypescript
const client = createClient<Database>({...}); -
CSS Import: Choose ONE - eitherOR
/ui/css, never both/ui/tailwind
-
适配器工厂模式:调用适配器时必须添加
()typescriptadapter: SupabaseAuthAdapter() // 正确 adapter: SupabaseAuthAdapter // 错误 - 缺少() -
React适配器导入:不从主包导出,需使用子路径typescript
import { BetterAuthReactAdapter } from '@neondatabase/neon-js/auth/react/adapters'; -
类型安全:始终使用Database泛型实现类型安全查询typescript
const client = createClient<Database>({...}); -
CSS导入:二选一 - 要么导入,要么导入
/ui/css,切勿同时导入两者/ui/tailwind
Setup
配置步骤
1. Install
1. 安装
bash
npm install @neondatabase/neon-jsbash
npm install @neondatabase/neon-js2. Generate Database Types
2. 生成数据库类型
bash
npx neon-js gen-types --db-url "postgresql://user:pass@host:5432/db" --output src/database.types.tsCLI 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.tsCLI选项:
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)
undefined3. Create Client (src/client.ts
)
src/client.ts3. 创建客户端(src/client.ts
)
src/client.tstypescript
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
)
src/providers.tsx4. 创建Provider(src/providers.tsx
)
src/providers.tsxtypescript
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
)
src/main.tsx5. 包裹应用(src/main.tsx
)
src/main.tsxtypescript
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
)
.env.local6. 环境变量(.env.local
)
.env.localVITE_NEON_AUTH_URL=https://your-auth.neon.tech
VITE_NEON_DATA_API_URL=https://your-data-api.neon.tech/rest/v1VITE_NEON_AUTH_URL=https://your-auth.neon.tech
VITE_NEON_DATA_API_URL=https://your-data-api.neon.tech/rest/v1CSS & 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) // paginationtypescript
// 相等判断
.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-insign-upforgot-passwordreset-passwordcallbacksign-outtypescript
import { AuthView } from '@neondatabase/neon-js/auth/react';
// 路由:/auth/:pathname
function AuthPage() {
const { pathname } = useParams();
return <AuthView pathname={pathname} />;
}路径名称:、、、、、
sign-insign-upforgot-passwordreset-passwordcallbacksign-outConditional 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
支持的提供商
googlegithubtwitterdiscordapplemicrosoftfacebooklinkedinspotifytwitchgitlabbitbucketgooglegithubtwitterdiscordapplemicrosoftfacebooklinkedinspotifytwitchgitlabbitbucketProtected 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
常见错误
| Error | Cause |
|---|---|
| Wrong email/password |
| Email registered |
| Missing RLS policy or GRANT |
| Token needs refresh |
| 错误信息 | 原因 |
|---|---|
| 邮箱/密码错误 |
| 邮箱已注册 |
| 缺少RLS策略或GRANT权限 |
| 令牌需要刷新 |
FAQ / Troubleshooting
FAQ / 故障排除
Anonymous access not working?
匿名访问无法工作?
Grant permissions to the role in your database:
anonymoussql
-- 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);在数据库中为角色授予权限:
anonymoussql
-- 为特定表授予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”错误?
- Check RLS is enabled:
ALTER TABLE posts ENABLE ROW LEVEL SECURITY; - Create appropriate policies for authenticated users
- Grant permissions:
GRANT SELECT, INSERT ON public.posts TO authenticated;
- 检查RLS是否启用:
ALTER TABLE posts ENABLE ROW LEVEL SECURITY; - 为认证用户创建合适的策略
- 授予权限:
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.tsOAuth not working in iframe?
OAuth在iframe中无法工作?
OAuth automatically uses popup flow in iframes. Ensure popups aren't blocked.
OAuth在iframe中会自动使用弹窗流程。确保弹窗未被阻止。
Session not persisting?
会话无法持久化?
- Cookies enabled?
- Auth URL correct in ?
.env.local - Not in incognito with cookies blocked?
- 已启用Cookie?
- 中的认证URL是否正确?
.env.local - 是否在阻止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