neon-auth-react
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNeon Auth for React
适用于React的Neon Auth
Help developers set up @neondatabase/auth (authentication only, no database) in React applications with Vite, Create React App, or similar bundlers.
帮助开发者在使用Vite、Create React App或类似打包工具的React应用中,配置@neondatabase/auth(仅认证功能,无需数据库)。
When to Use
适用场景
Use this skill when:
- Setting up auth-only in React (no database needed)
- User already has a database solution
- User mentions "@neondatabase/auth" without "neon-js"
- User is NOT using Next.js (use skill for Next.js)
neon-auth-nextjs
在以下场景中使用本技能:
- 仅在React中设置认证功能(无需数据库)
- 用户已拥有数据库解决方案
- 用户提到"@neondatabase/auth"但未提及"neon-js"
- 用户未使用Next.js(Next.js请使用技能)
neon-auth-nextjs
Critical Rules
重要规则
- Adapter Factory Pattern: Always call adapters with - they are factory functions
() - React Adapter Import: Use subpath
@neondatabase/auth/react/adapters - createAuthClient takes URL as first arg:
createAuthClient(url, config) - CSS Import: Choose ONE - either OR
/ui/css, never both/ui/tailwind
- 适配器工厂模式:始终使用调用适配器——它们是工厂函数
() - React适配器导入:使用子路径
@neondatabase/auth/react/adapters - createAuthClient首个参数为URL:
createAuthClient(url, config) - CSS导入:二选一——要么导入,要么导入
/ui/css,切勿同时导入/ui/tailwind
Setup
配置步骤
1. Install
1. 安装
bash
npm install @neondatabase/authbash
npm install @neondatabase/auth2. Create Client (src/auth-client.ts
)
src/auth-client.ts2. 创建客户端(src/auth-client.ts
)
src/auth-client.tstypescript
import { createAuthClient } from '@neondatabase/auth';
import { BetterAuthReactAdapter } from '@neondatabase/auth/react/adapters';
export const authClient = createAuthClient(
import.meta.env.VITE_AUTH_URL,
{
adapter: BetterAuthReactAdapter(),
// allowAnonymous: true, // Enable for RLS access without login
}
);typescript
import { createAuthClient } from '@neondatabase/auth';
import { BetterAuthReactAdapter } from '@neondatabase/auth/react/adapters';
export const authClient = createAuthClient(
import.meta.env.VITE_AUTH_URL,
{
adapter: BetterAuthReactAdapter(),
// allowAnonymous: true, // 启用后支持无需登录的RLS访问
}
);3. Create Provider (src/providers.tsx
)
src/providers.tsx3. 创建Provider(src/providers.tsx
)
src/providers.tsxtypescript
import { NeonAuthUIProvider } from '@neondatabase/auth/react/ui';
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { authClient } from './auth-client';
// Import CSS (choose one)
import '@neondatabase/auth/ui/css';
export function Providers({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
return (
<NeonAuthUIProvider
authClient={authClient}
navigate={navigate}
redirectTo="/dashboard"
Link={({ children, href }) => <Link to={href}>{children}</Link>}
>
{children}
</NeonAuthUIProvider>
);
}typescript
import { NeonAuthUIProvider } from '@neondatabase/auth/react/ui';
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { authClient } from './auth-client';
// 导入CSS(二选一)
import '@neondatabase/auth/ui/css';
export function Providers({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
return (
<NeonAuthUIProvider
authClient={authClient}
navigate={navigate}
redirectTo="/dashboard"
Link={({ children, href }) => <Link to={href}>{children}</Link>}
>
{children}
</NeonAuthUIProvider>
);
}4. Wrap App (src/main.tsx
)
src/main.tsx4. 包裹应用(src/main.tsx
)
src/main.tsxtypescript
import { BrowserRouter } from 'react-router-dom';
import { Providers } from './providers';
import App from './App';
ReactDOM.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';
ReactDOM.createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<Providers>
<App />
</Providers>
</BrowserRouter>
);CSS & Styling
CSS与样式
Import Options
导入选项
Without Tailwind (pre-built CSS bundle ~47KB):
css
/* In your main CSS file or import in provider */
@import '@neondatabase/auth/ui/css';With Tailwind CSS v4:
css
@import 'tailwindcss';
@import '@neondatabase/auth/ui/tailwind';IMPORTANT: Never import both - causes duplicate styles.
不使用Tailwind(预构建CSS包约47KB):
css
/* 在主CSS文件中导入,或在Provider中导入 */
@import '@neondatabase/auth/ui/css';使用Tailwind CSS v4:
css
@import 'tailwindcss';
@import '@neondatabase/auth/ui/tailwind';重要提示:切勿同时导入两者——会导致样式重复。
Dark Mode
深色模式
The provider includes for dark mode. Control via prop:
next-themesdefaultThemetypescript
<NeonAuthUIProvider
authClient={authClient}
defaultTheme="system" // 'light' | 'dark' | 'system'
// ...
>Provider内置以支持深色模式,可通过属性控制:
next-themesdefaultThemetypescript
<NeonAuthUIProvider
authClient={authClient}
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);
--card-foreground: oklch(0.1 0 0);
--border: oklch(0.9 0 0);
--input: oklch(0.9 0 0);
--ring: oklch(0.7 0 0);
--radius: 0.5rem;
/* See theme.css for full list */
}
.dark {
--background: oklch(0.15 0 0);
--foreground: oklch(0.98 0 0);
/* Dark mode overrides */
}在样式表中覆盖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);
--card-foreground: oklch(0.1 0 0);
--border: oklch(0.9 0 0);
--input: oklch(0.9 0 0);
--ring: oklch(0.7 0 0);
--radius: 0.5rem;
/* 完整变量列表请查看theme.css */
}
.dark {
--background: oklch(0.15 0 0);
--foreground: oklch(0.98 0 0);
/* 深色模式覆盖变量 */
}NeonAuthUIProvider Props
NeonAuthUIProvider属性
Full configuration options:
typescript
<NeonAuthUIProvider
// Required
authClient={authClient}
// Navigation (required for React Router)
navigate={navigate} // Router's navigate function
Link={({href, children}) => <Link to={href}>{children}</Link>} // Router's Link component
redirectTo="/dashboard" // Where to redirect after auth
// Social/OAuth Providers
social={{
providers: ['google'],
}}
// Feature Flags
emailOTP={true} // Enable email OTP sign-in
emailVerification={true} // Require email verification
magicLink={false} // Magic link (disabled by default)
multiSession={false} // Multiple sessions (disabled)
// Credentials Configuration
credentials={{
forgotPassword: true, // Show forgot password link
}}
// Sign Up Fields
signUp={{
fields: ['name'], // Additional fields: 'name', 'username', etc.
}}
// Account Settings Fields
account={{
fields: ['image', 'name', 'company', 'age', 'newsletter'],
}}
// Avatar Configuration
avatar={{
size: 256,
extension: 'webp',
}}
// Organization Features
organization={{}} // Enable org features
// Dark Mode
defaultTheme="system" // 'light' | 'dark' | 'system'
// Custom Labels
localization={{
SIGN_IN: 'Welcome Back',
SIGN_IN_DESCRIPTION: 'Sign in to your account',
SIGN_UP: 'Create Account',
SIGN_UP_DESCRIPTION: 'Join us today',
FORGOT_PASSWORD: 'Forgot Password?',
OR_CONTINUE_WITH: 'or continue with',
// See better-auth-ui docs for full list
}}
>
{children}
</NeonAuthUIProvider>完整配置选项:
typescript
<NeonAuthUIProvider
// 必填项
authClient={authClient}
// 导航配置(React Router必填)
navigate={navigate} // 路由的navigate函数
Link={({href, children}) => <Link to={href}>{children}</Link>} // 路由的Link组件
redirectTo="/dashboard" // 认证完成后重定向的地址
// 社交/OAuth提供商
social={{
providers: ['google'],
}}
// 功能开关
emailOTP={true} // 启用邮箱OTP登录
emailVerification={true} // 要求邮箱验证
magicLink={false} // 魔法链接(默认禁用)
multiSession={false} // 多会话支持(默认禁用)
// 凭证配置
credentials={{
forgotPassword: true, // 显示忘记密码链接
}}
// 注册字段
signUp={{
fields: ['name'], // 额外字段:'name', 'username'等
}}
// 账户设置字段
account={{
fields: ['image', 'name', 'company', 'age', 'newsletter'],
}}
// 头像配置
avatar={{
size: 256,
extension: 'webp',
}}
// 组织功能
organization={{}} // 启用组织功能
// 深色模式
defaultTheme="system" // 可选值:'light' | 'dark' | 'system'
// 自定义标签
localization={{
SIGN_IN: '欢迎回来',
SIGN_IN_DESCRIPTION: '登录您的账户',
SIGN_UP: '创建账户',
SIGN_UP_DESCRIPTION: '立即加入我们',
FORGOT_PASSWORD: '忘记密码?',
OR_CONTINUE_WITH: '或通过以下方式继续',
// 完整列表请查看better-auth-ui文档
}}
>
{children}
</NeonAuthUIProvider>UI Components
UI组件
AuthView - Main Auth Interface
AuthView - 主认证界面
Handles sign-in, sign-up, forgot password, and callback routes:
typescript
import { AuthView } from '@neondatabase/auth/react/ui';
// Route: /auth/:pathname
function AuthPage() {
const { pathname } = useParams(); // 'sign-in', 'sign-up', 'forgot-password', etc.
return <AuthView pathname={pathname} />;
}Supported pathnames: , , , , ,
sign-insign-upforgot-passwordreset-passwordcallbacksign-out处理登录、注册、忘记密码和回调路由:
typescript
import { AuthView } from '@neondatabase/auth/react/ui';
// 路由:/auth/:pathname
function AuthPage() {
const { pathname } = useParams(); // 可选值:'sign-in', 'sign-up', 'forgot-password'等
return <AuthView pathname={pathname} />;
}支持的pathname:, , , , ,
sign-insign-upforgot-passwordreset-passwordcallbacksign-outConditional Rendering
条件渲染
typescript
import {
SignedIn,
SignedOut,
AuthLoading,
RedirectToSignIn
} from '@neondatabase/auth/react/ui';
function MyPage() {
return (
<>
{/* Show while checking auth state */}
<AuthLoading>
<LoadingSpinner />
</AuthLoading>
{/* Show only when authenticated */}
<SignedIn>
<Dashboard />
</SignedIn>
{/* Show only when NOT authenticated */}
<SignedOut>
<LandingPage />
</SignedOut>
{/* Redirect to sign-in if not authenticated */}
<RedirectToSignIn />
</>
);
}typescript
import {
SignedIn,
SignedOut,
AuthLoading,
RedirectToSignIn
} from '@neondatabase/auth/react/ui';
function MyPage() {
return (
<>
{/* 认证状态检查时显示 */}
<AuthLoading>
<LoadingSpinner />
</AuthLoading>
{/* 仅在已认证时显示 */}
<SignedIn>
<Dashboard />
</SignedIn>
{/* 仅在未认证时显示 */}
<SignedOut>
<LandingPage />
</SignedOut>
{/* 未认证时重定向到登录页 */}
<RedirectToSignIn />
</>
);
}UserButton
UserButton
Dropdown menu with user avatar, name, and sign-out:
typescript
import { UserButton } from '@neondatabase/auth/react/ui';
function Header() {
return (
<header>
<nav>...</nav>
<UserButton />
</header>
);
}包含用户头像、名称和退出登录选项的下拉菜单:
typescript
import { UserButton } from '@neondatabase/auth/react/ui';
function Header() {
return (
<header>
<nav>...</nav>
<UserButton />
</header>
);
}Account Management Components
账户管理组件
typescript
import {
AccountSettingsCards, // Profile info (avatar, name, email)
SecuritySettingsCards, // Security options (linked accounts)
SessionsCard, // Active sessions management
ChangePasswordCard, // Password change form
ChangeEmailCard, // Email change form
DeleteAccountCard, // Account deletion
ProvidersCard, // Linked OAuth providers
} from '@neondatabase/auth/react/ui';
function AccountPage() {
const { view } = useParams(); // 'settings', 'security', 'sessions'
return (
<>
<RedirectToSignIn />
<SignedIn>
{view === 'settings' && <AccountSettingsCards />}
{view === 'security' && (
<>
<ChangePasswordCard />
<SecuritySettingsCards />
</>
)}
{view === 'sessions' && <SessionsCard />}
</SignedIn>
</>
);
}typescript
import {
AccountSettingsCards, // 个人资料信息(头像、名称、邮箱)
SecuritySettingsCards, // 安全选项(关联账户)
SessionsCard, // 活跃会话管理
ChangePasswordCard, // 密码修改表单
ChangeEmailCard, // 邮箱修改表单
DeleteAccountCard, // 账户删除
ProvidersCard, // 关联的OAuth提供商
} from '@neondatabase/auth/react/ui';
function AccountPage() {
const { view } = useParams(); // 可选值:'settings', 'security', 'sessions'
return (
<>
<RedirectToSignIn />
<SignedIn>
{view === 'settings' && <AccountSettingsCards />}
{view === 'security' && (
<>
<ChangePasswordCard />
<SecuritySettingsCards />
</>
)}
{view === 'sessions' && <SessionsCard />}
</SignedIn>
</>
);
}Organization Components
组织组件
typescript
import {
OrganizationSwitcher, // Switch between orgs
OrganizationSettingsCards, // Org settings
OrganizationMembersCard, // Member management
AcceptInvitationCard, // Accept org invite
} from '@neondatabase/auth/react/ui';typescript
import {
OrganizationSwitcher, // 在组织间切换
OrganizationSettingsCards, // 组织设置
OrganizationMembersCard, // 成员管理
AcceptInvitationCard, // 接受组织邀请
} from '@neondatabase/auth/react/ui';Adapter Options
适配器选项
BetterAuthReactAdapter (Recommended for React)
BetterAuthReactAdapter(React推荐使用)
Native Better Auth API with React hooks:
typescript
import { BetterAuthReactAdapter } from '@neondatabase/auth/react/adapters';
const authClient = createAuthClient(url, {
adapter: BetterAuthReactAdapter(),
});
// Methods
await authClient.signIn.email({ email, password });
await authClient.signUp.email({ email, password, name });
await authClient.signIn.social({ provider: 'google', callbackURL: '/dashboard' });
await authClient.signOut();
const session = await authClient.getSession();
// React Hook
const { data, isPending, error } = authClient.useSession();原生Better Auth API搭配React Hooks:
typescript
import { BetterAuthReactAdapter } from '@neondatabase/auth/react/adapters';
const authClient = createAuthClient(url, {
adapter: BetterAuthReactAdapter(),
});
// 方法示例
await authClient.signIn.email({ email, password });
await authClient.signUp.email({ email, password, name });
await authClient.signIn.social({ provider: 'google', callbackURL: '/dashboard' });
await authClient.signOut();
const session = await authClient.getSession();
// React Hook
const { data, isPending, error } = authClient.useSession();SupabaseAuthAdapter (Supabase-compatible API)
SupabaseAuthAdapter(兼容Supabase API)
For migrating from Supabase or familiar API:
typescript
import { SupabaseAuthAdapter } from '@neondatabase/auth/vanilla/adapters';
const authClient = createAuthClient(url, {
adapter: SupabaseAuthAdapter(),
});
// Supabase-style methods
await authClient.signUp({ email, password, options: { data: { name } } });
await authClient.signInWithPassword({ email, password });
await authClient.signInWithOAuth({ provider: 'google', options: { redirectTo } });
await authClient.signOut();
const { data: session } = await authClient.getSession();
// Event listener
authClient.onAuthStateChange((event, session) => {
console.log(event); // 'SIGNED_IN', 'SIGNED_OUT', 'TOKEN_REFRESHED', 'USER_UPDATED'
});适用于从Supabase迁移,或熟悉Supabase API的场景:
typescript
import { SupabaseAuthAdapter } from '@neondatabase/auth/vanilla/adapters';
const authClient = createAuthClient(url, {
adapter: SupabaseAuthAdapter(),
});
// Supabase风格方法
await authClient.signUp({ email, password, options: { data: { name } } });
await authClient.signInWithPassword({ email, password });
await authClient.signInWithOAuth({ provider: 'google', options: { redirectTo } });
await authClient.signOut();
const { data: session } = await authClient.getSession();
// 事件监听器
authClient.onAuthStateChange((event, session) => {
console.log(event); // 可选值:'SIGNED_IN', 'SIGNED_OUT', 'TOKEN_REFRESHED', 'USER_UPDATED'
});BetterAuthVanillaAdapter (Non-React)
BetterAuthVanillaAdapter(非React场景)
For vanilla JS/TS without React hooks:
typescript
import { BetterAuthVanillaAdapter } from '@neondatabase/auth/vanilla/adapters';
const authClient = createAuthClient(url, {
adapter: BetterAuthVanillaAdapter(),
});
// Same API as BetterAuthReactAdapter, but no useSession() hook适用于无React Hooks的原生JS/TS场景:
typescript
import { BetterAuthVanillaAdapter } from '@neondatabase/auth/vanilla/adapters';
const authClient = createAuthClient(url, {
adapter: BetterAuthVanillaAdapter(),
});
// API与BetterAuthReactAdapter一致,但无useSession() HookSocial/OAuth Providers
社交/OAuth提供商
Configuration
配置
Enable providers in NeonAuthUIProvider:
typescript
<NeonAuthUIProvider
social={{
providers: ['google'],
}}
>在NeonAuthUIProvider中启用提供商:
typescript
<NeonAuthUIProvider
social={{
providers: ['google'],
}}
>Programmatic OAuth Sign-In
程序化OAuth登录
typescript
// BetterAuth API
await authClient.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
scopes: ['email', 'profile'], // Optional
});
// Supabase API
await authClient.signInWithOAuth({
provider: 'google',
options: {
redirectTo: '/dashboard',
scopes: 'email profile',
},
});typescript
// BetterAuth API
await authClient.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
scopes: ['email', 'profile'], // 可选
});
// Supabase API
await authClient.signInWithOAuth({
provider: 'google',
options: {
redirectTo: '/dashboard',
scopes: 'email profile',
},
});Supported Providers
支持的提供商
googlegithubtwitterdiscordapplemicrosoftfacebooklinkedinspotifytwitchgitlabbitbucketgooglegithubtwitterdiscordapplemicrosoftfacebooklinkedinspotifytwitchgitlabbitbucketOAuth in Iframes
iframe中的OAuth
OAuth automatically uses popup flow when running in iframes (due to X-Frame-Options restrictions). No configuration needed.
在iframe中运行时,OAuth会自动使用弹窗流程(受X-Frame-Options限制),无需额外配置。
Session Hook
Session Hook
typescript
function MyComponent() {
const { data: session, isPending, error, refetch } = authClient.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>
<p>ID: {session.user.id}</p>
<img src={session.user.image} alt="Avatar" />
</div>
);
}Session object shape:
typescript
{
user: {
id: string;
email: string;
name: string;
image?: string;
emailVerified: boolean;
createdAt: Date;
updatedAt: Date;
};
session: {
id: string;
token: string; // JWT token
expiresAt: Date;
ipAddress?: string;
userAgent?: string;
};
}typescript
function MyComponent() {
const { data: session, isPending, error, refetch } = authClient.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>
<p>ID:{session.user.id}</p>
<img src={session.user.image} alt="头像" />
</div>
);
}Session对象结构:
typescript
{
user: {
id: string;
email: string;
name: string;
image?: string;
emailVerified: boolean;
createdAt: Date;
updatedAt: Date;
};
session: {
id: string;
token: string; // JWT令牌
expiresAt: Date;
ipAddress?: string;
userAgent?: string;
};
}Advanced Features
高级功能
Anonymous Access
匿名访问
Enable RLS-based data access for unauthenticated users:
typescript
// Client setup
const authClient = createAuthClient(url, {
adapter: BetterAuthReactAdapter(),
allowAnonymous: true,
});
// Get token (returns anonymous JWT if not signed in)
const token = await authClient.getJWTToken?.();为未认证用户启用基于RLS的数据访问:
typescript
// 客户端配置
const authClient = createAuthClient(url, {
adapter: BetterAuthReactAdapter(),
allowAnonymous: true,
});
// 获取令牌(未登录时返回匿名JWT)
const token = await authClient.getJWTToken?.();Get JWT Token (for API calls)
获取JWT令牌(用于API调用)
typescript
const token = await authClient.getJWTToken();
const response = await fetch('/api/data', {
headers: {
Authorization: `Bearer ${token}`,
},
});typescript
const token = await authClient.getJWTToken();
const response = await fetch('/api/data', {
headers: {
Authorization: `Bearer ${token}`,
},
});Password Reset Flow
密码重置流程
typescript
// 1. Request reset email (Supabase API)
await authClient.resetPasswordForEmail(email, {
redirectTo: '/auth/reset-password',
});
// 2. User clicks link, lands on reset page
// 3. Verify OTP and set new password
await authClient.verifyOtp({
email,
token: otpFromUrl,
type: 'recovery',
});
// Then call password update
await authClient.updateUser({ password: newPassword });typescript
// 1. 请求重置邮件(Supabase API)
await authClient.resetPasswordForEmail(email, {
redirectTo: '/auth/reset-password',
});
// 2. 用户点击链接,进入重置页面
// 3. 验证OTP并设置新密码
await authClient.verifyOtp({
email,
token: otpFromUrl,
type: 'recovery',
});
// 然后调用密码更新
await authClient.updateUser({ password: newPassword });Update User Profile
更新用户资料
typescript
// BetterAuth API
await authClient.updateUser({
name: 'New Name',
image: 'https://...',
// Custom fields defined in account.fields
});
// Supabase API
await authClient.updateUser({
data: {
name: 'New Name',
avatar_url: 'https://...',
},
});typescript
// BetterAuth API
await authClient.updateUser({
name: '新名称',
image: 'https://...',
// 自定义字段需在account.fields中定义
});
// Supabase API
await authClient.updateUser({
data: {
name: '新名称',
avatar_url: 'https://...',
},
});Identity/Account Linking
身份/账户关联
typescript
// List linked accounts
const { data } = await authClient.getUserIdentities();
// Returns: { identities: [{ provider: 'google', ... }] }
// Link new provider
await authClient.linkIdentity({
provider: 'google',
options: { redirectTo: '/account/security' },
});
// Unlink provider
await authClient.unlinkIdentity({
identity_id: 'identity-uuid',
});typescript
// 列出已关联账户
const { data } = await authClient.getUserIdentities();
// 返回值:{ identities: [{ provider: 'google', ... }] }
// 关联新提供商
await authClient.linkIdentity({
provider: 'google',
options: { redirectTo: '/account/security' },
});
// 解除关联提供商
await authClient.unlinkIdentity({
identity_id: 'identity-uuid',
});Auth State Events (Supabase Adapter)
认证状态事件(Supabase Adapter)
typescript
const { data: { subscription } } = authClient.onAuthStateChange((event, session) => {
switch (event) {
case 'SIGNED_IN':
console.log('User signed in:', session?.user);
break;
case 'SIGNED_OUT':
console.log('User signed out');
break;
case 'TOKEN_REFRESHED':
console.log('Token refreshed');
break;
case 'USER_UPDATED':
console.log('User profile updated');
break;
}
});
// Cleanup
subscription.unsubscribe();typescript
const { data: { subscription } } = authClient.onAuthStateChange((event, session) => {
switch (event) {
case 'SIGNED_IN':
console.log('用户已登录:', session?.user);
break;
case 'SIGNED_OUT':
console.log('用户已退出登录');
break;
case 'TOKEN_REFRESHED':
console.log('令牌已刷新');
break;
case 'USER_UPDATED':
console.log('用户资料已更新');
break;
}
});
// 清理订阅
subscription.unsubscribe();Cross-Tab Synchronization
跨标签页同步
Automatic via BroadcastChannel. Sign out in one tab signs out all tabs.
通过BroadcastChannel自动实现,在一个标签页退出登录时,所有标签页都会同步退出。
Protected Routes
受保护路由
Pattern with React Router
React Router实现方案
typescript
// routes.tsx
import { Routes, Route } from 'react-router-dom';
export function AppRoutes() {
return (
<Routes>
{/* Public */}
<Route path="/" element={<HomePage />} />
{/* Auth routes */}
<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>
</>
);
}Auth Page Setup
认证页面配置
typescript
// pages/AuthPage.tsx
import { useParams } from 'react-router-dom';
import { AuthView } from '@neondatabase/auth/react/ui';
export function AuthPage() {
const { pathname } = useParams();
return <AuthView pathname={pathname} />;
}typescript
// pages/AuthPage.tsx
import { useParams } from 'react-router-dom';
import { AuthView } from '@neondatabase/auth/react/ui';
export function AuthPage() {
const { pathname } = useParams();
return <AuthView pathname={pathname} />;
}Account Page Setup
账户页面配置
typescript
// pages/AccountPage.tsx
import { useParams } from 'react-router-dom';
import {
SignedIn,
RedirectToSignIn,
AccountSettingsCards,
SecuritySettingsCards,
SessionsCard,
ChangePasswordCard,
} from '@neondatabase/auth/react/ui';
export function AccountPage() {
const { view = 'settings' } = useParams();
return (
<>
<RedirectToSignIn />
<SignedIn>
{view === 'settings' && <AccountSettingsCards />}
{view === 'security' && (
<>
<ChangePasswordCard />
<SecuritySettingsCards />
</>
)}
{view === 'sessions' && <SessionsCard />}
</SignedIn>
</>
);
}typescript
// pages/AccountPage.tsx
import { useParams } from 'react-router-dom';
import {
SignedIn,
RedirectToSignIn,
AccountSettingsCards,
SecuritySettingsCards,
SessionsCard,
ChangePasswordCard,
} from '@neondatabase/auth/react/ui';
export function AccountPage() {
const { view = 'settings' } = useParams();
return (
<>
<RedirectToSignIn />
<SignedIn>
{view === 'settings' && <AccountSettingsCards />}
{view === 'security' && (
<>
<ChangePasswordCard />
<SecuritySettingsCards />
</>
)}
{view === 'sessions' && <SessionsCard />}
</SignedIn>
</>
);
}Error Handling
错误处理
Error Response Shape
错误响应结构
typescript
const result = await authClient.signIn.email({ email, password });
if (result.error) {
console.error(result.error.message); // Human-readable message
console.error(result.error.status); // HTTP status code
}typescript
const result = await authClient.signIn.email({ email, password });
if (result.error) {
console.error(result.error.message); // 人类可读的错误信息
console.error(result.error.status); // HTTP状态码
}Common Errors
常见错误
| Error | Cause |
|---|---|
| Wrong email/password |
| Email already registered |
| Verification required |
| Expired or invalid session |
| Too many requests |
| 错误信息 | 原因 |
|---|---|
| 邮箱/密码错误 |
| 邮箱已注册 |
| 需验证邮箱 |
| 会话过期或无效 |
| 请求过于频繁 |
Try-Catch Pattern
Try-Catch模式
typescript
try {
const { error } = await authClient.signIn.email({ email, password });
if (error) {
// Handle auth-specific errors
toast.error(error.message);
return;
}
// Success - redirect
navigate('/dashboard');
} catch (err) {
// Handle network/unexpected errors
toast.error('Something went wrong');
}typescript
try {
const { error } = await authClient.signIn.email({ email, password });
if (error) {
// 处理认证相关错误
toast.error(error.message);
return;
}
// 认证成功 - 重定向
navigate('/dashboard');
} catch (err) {
// 处理网络或意外错误
toast.error('出现未知错误');
}Performance Notes
性能说明
- Session caching: 60-second TTL, automatic JWT expiration handling
- Request deduplication: Concurrent calls share single network request
- Cold start: ~200ms (single request)
- Cross-tab sync: <50ms via BroadcastChannel
- 会话缓存:TTL为60秒,自动处理JWT过期
- 请求去重:并发请求共享单个网络请求
- 冷启动:约200ms(单次请求)
- 跨标签页同步:通过BroadcastChannel实现,延迟<50ms
FAQ / Troubleshooting
FAQ / 故障排除
Anonymous access not working?
匿名访问无法工作?
When using , you must grant permissions to the role in your database:
allowAnonymous: trueanonymoussql
-- Grant SELECT on specific tables
GRANT SELECT ON public.posts TO anonymous;
GRANT SELECT ON public.products TO anonymous;
-- Or grant on all tables in schema (be careful!)
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anonymous;
-- For INSERT/UPDATE/DELETE (if needed)
GRANT INSERT, UPDATE ON public.comments TO anonymous;Your RLS policies must also allow the anonymous role:
sql
-- Example: Allow anonymous users to read published posts
CREATE POLICY "Anyone can read published posts"
ON public.posts FOR SELECT
USING (published = true);
-- Example: Allow anonymous users to read products
CREATE POLICY "Anyone can read products"
ON public.products FOR SELECT
USING (true);当启用时,必须在数据库中为角色授予权限:
allowAnonymous: trueanonymoussql
-- 为特定表授予SELECT权限
GRANT SELECT ON public.posts TO anonymous;
GRANT SELECT ON public.products TO anonymous;
-- 或为schema下所有表授予权限(请谨慎操作!)
GRANT SELECT ON ALL TABLES IN SCHEMA public TO anonymous;
-- 如需INSERT/UPDATE/DELETE权限(按需配置)
GRANT INSERT, UPDATE ON public.comments TO anonymous;同时,RLS策略也必须允许anonymous角色:
sql
-- 示例:允许匿名用户查看已发布的文章
CREATE POLICY "Anyone can read published posts"
ON public.posts FOR SELECT
USING (published = true);
-- 示例:允许匿名用户查看产品
CREATE POLICY "Anyone can read products"
ON public.products FOR SELECT
USING (true);OAuth redirect not working in iframe?
OAuth重定向在iframe中无法工作?
OAuth automatically uses popup flow in iframes. Make sure:
- Your auth server allows the popup callback URL
- Popups aren't blocked by the browser
在iframe中运行时,OAuth会自动使用弹窗流程,请确保:
- 认证服务器允许弹窗回调URL
- 浏览器未阻止弹窗
Session not persisting after refresh?
刷新页面后会话未保留?
Check that:
- Cookies are enabled
- Your auth server URL matches your domain (or has proper CORS)
- You're not in incognito mode with cookies blocked
请检查以下项:
- 已启用Cookie
- 认证服务器URL与域名匹配(或已配置正确的CORS)
- 未在阻止Cookie的隐身模式下运行
"Invalid credentials" but password is correct?
提示“Invalid credentials”但密码正确?
- Email might not be verified (check setting)
emailVerification - Account might be locked after too many failed attempts
- Check if using correct adapter API (Supabase vs BetterAuth style)
- 邮箱可能未验证(请检查设置)
emailVerification - 多次失败尝试后账户可能被锁定
- 请确认使用的适配器API是否正确(Supabase风格 vs BetterAuth风格)