# Agent Skills (/docs/agent-skills)
Better Auth UI ships skills for React, Solid, shadcn/ui, HeroUI, Zaidan, core data APIs, and localization.
Choose either TanStack Intent or skills.sh. Both use the same skill files, with different update behavior.
| Install path | Skill source | Updates |
| --------------- | --------------------- | -------------------------------------------------- |
| TanStack Intent | Installed npm package | Skills change with the library version. |
| skills.sh | GitHub branch or tag | Skills change independently of installed packages. |
## Install through TanStack Intent [#install-through-tanstack-intent]
Install the Better Auth UI packages for your application first. Skills are included in package releases that contain a `skills` directory.
Add the Better Auth UI scope to your application's Intent allowlist in `package.json`. Preserve any existing entries:
```json
{
"intent": {
"skills": ["@better-auth-ui/*"]
}
}
```
Install the agent guidance, then list the available skills:
```bash
bunx @tanstack/intent@latest install
bunx @tanstack/intent@latest list
```
Load the skill for your package. For example:
```bash
bunx @tanstack/intent@latest load @better-auth-ui/react#better-auth-ui-react
```
Intent reads the skill from the installed package version. If the package has no skills, update it or use the GitHub install path.
Read the [TanStack Intent documentation](https://tanstack.com/intent/latest/docs/overview) for supported agents and project configuration.
## Install through skills.sh [#install-through-skillssh]
Select the skill for your application:
```bash
bunx skills@latest add better-auth-ui/better-auth-ui --full-depth --skill better-auth-ui-react
```
Replace `better-auth-ui-react` with a name from this table:
| Skill | Covers | npm package |
| ----------------------------- | ---------------------------------------------------------------------------- | ------------------------- |
| `better-auth-ui-react` | React hooks, providers, SSR, and copied shadcn/ui components | `@better-auth-ui/react` |
| `better-auth-ui-solid` | Solid hooks, providers, SSR, and copied Zaidan components | `@better-auth-ui/solid` |
| `better-auth-ui-heroui` | Packaged HeroUI components and integration | `@better-auth-ui/heroui` |
| `better-auth-ui-core` | Query factories, mutations, server helpers, and explicit organization access | `@better-auth-ui/core` |
| `better-auth-ui-localization` | Locales, message overrides, language matching, and email localization | `@better-auth-ui/locales` |
You can select multiple skills after `--skill`. Each skill also works on its own.
`--full-depth` finds skills inside this monorepo's package directories. `--skill` selects public guidance without installing the repository's internal Nx skills.
To browse without installation, run:
```bash
bunx skills@latest add better-auth-ui/better-auth-ui --full-depth --list
```
This list also includes contributor skills. Select the `better-auth-ui-*` names for application development.
For a specific release, use its GitHub tag URL and package directory instead of the repository shorthand. That tag must contain the skills.
The default GitHub install follows the default branch. It does not automatically match the installed Better Auth UI version.
Read the [skills CLI documentation](https://github.com/vercel-labs/skills#install-a-skill) for agent selection and update commands.
## Documentation for agents [#documentation-for-agents]
Skills provide task guidance and common mistakes. The documentation endpoints provide reference material:
* [llms.txt](https://better-auth-ui.com/llms.txt) lists documentation pages and their Markdown URLs.
* [llms-full.txt](https://better-auth-ui.com/llms-full.txt) contains the complete documentation text.
When website examples differ from installed package exports, use the installed package as the API authority.
# (/docs/heroui/components/auth/auth-redirect)
`` powers the `/auth/redirect` view. It checks the current
session, then continues to the `redirectTo` query parameter.
```tsx
import { Auth } from "@better-auth-ui/heroui"
export default function AuthPage({ path }: { path: string }) {
return
}
```
Open the view with an encoded, same-origin destination:
```text
/auth/redirect?redirectTo=%2Fsettings%2Faccount
```
Authenticated users continue immediately. Signed-out users go to sign in and
return to the redirect view after authentication. The final redirect uses a
full-page request, so the destination can be an API callback.
Only root-relative paths and same-origin HTTP(S) URLs are accepted. Unsafe,
cross-origin, malformed, or self-referencing targets fall back to `/`.
## Use with account deletion emails [#use-with-account-deletion-emails]
Better Auth requires the user who follows a deletion link to have a matching
session. Wrap the generated deletion URL with the redirect view before sending
the email:
```ts
import { betterAuth } from "better-auth"
export const auth = betterAuth({
user: {
deleteUser: {
enabled: true,
sendDeleteAccountVerification: async ({ user, url }) => {
const appURL = new URL(process.env.BETTER_AUTH_URL!)
const deleteURL = new URL(url)
const redirectURL = new URL("/auth/redirect", appURL)
redirectURL.searchParams.set(
"redirectTo",
`${deleteURL.pathname}${deleteURL.search}${deleteURL.hash}`,
)
await sendDeleteAccountEmail({
to: user.email,
url: redirectURL.toString(),
})
},
},
},
})
```
The Better Auth callback and the redirect view must share an origin.
## Props [#props]
# (/docs/heroui/components/auth/auth)
## Usage [#usage]
```tsx file=/src/demos/heroui/auth/auth.tsx
import { Auth } from "@better-auth-ui/heroui"
export function AuthDemo() {
return
}
```
### Built-in views [#built-in-views]
| `view` | Default path |
| ---------------- | ----------------------- |
| `callback` | `/auth/callback` |
| `error` | `/auth/error` |
| `redirect` | `/auth/redirect` |
| `signIn` | `/auth/sign-in` |
| `signUp` | `/auth/sign-up` |
| `signOut` | `/auth/sign-out` |
| `forgotPassword` | `/auth/forgot-password` |
| `resetPassword` | `/auth/reset-password` |
| `resetLinkSent` | `/auth/reset-link-sent` |
| `verifyEmail` | `/auth/verify-email` |
Registered plugins can contribute more views through their own `viewPaths.auth`, such as `magicLink` and `magicLinkSent`.
### Callback results [#callback-results]
Set Better Auth `onAPIError.errorURL` and each social sign-in `errorCallbackURL` to `/auth/error`. The view reads Better Auth's `error` query parameter and gives the user a suitable recovery action.
Use `/auth/callback?result=email_verified` as an email verification callback. The result view also supports `account_linked`, `password_reset`, `signup_complete`, and `cancelled`. Add `flow=email-verification`, `account-linking`, `password-reset`, or `oauth` when `result=success` needs more context. A local `redirectTo` path adds a Continue action.
## Props [#props]
# (/docs/heroui/components/auth/forgot-password)
## Usage [#usage]
```tsx file=/src/demos/heroui/auth/forgot-password.tsx
import { ForgotPassword } from "@better-auth-ui/heroui"
export function ForgotPasswordDemo() {
return
}
```
## Props [#props]
# (/docs/heroui/components/auth/reset-link-sent)
`` renders this view at `/auth/reset-link-sent` after `` successfully requests a reset link. It reads the submitted email from session storage, shows an email-provider shortcut when one is available, and links back to sign-in.
Hover or focus the email-provider button to show a QR code for the same provider URL. Users can scan it when their email account is available on another device.
## Usage [#usage]
```tsx
import { ResetLinkSent } from "@better-auth-ui/heroui"
```
## Props [#props]
# (/docs/heroui/components/auth/reset-password)
## Usage [#usage]
```tsx file=/src/demos/heroui/auth/reset-password.tsx
import { ResetPassword } from "@better-auth-ui/heroui"
export function ResetPasswordDemo() {
return
}
```
New-password and confirmation fields each provide a localized show/hide control while preserving their current values.
## Props [#props]
# (/docs/heroui/components/auth/sign-in)
## Usage [#usage]
```tsx file=/src/demos/heroui/auth/sign-in.tsx
import { SignIn } from "@better-auth-ui/heroui"
export function SignInDemo() {
return
}
```
The password starts masked. Its localized show/hide button changes only the field presentation, so the submitted password value stays unchanged.
## Props [#props]
# (/docs/heroui/components/auth/sign-out)
## Usage [#usage]
```tsx file=/src/demos/heroui/auth/sign-out.tsx
import { SignOut } from "@better-auth-ui/heroui"
export function SignOutDemo() {
return
}
```
## Props [#props]
# (/docs/heroui/components/auth/sign-up)
## Usage [#usage]
```tsx file=/src/demos/heroui/auth/sign-up.tsx
import { SignUp } from "@better-auth-ui/heroui"
export function SignUpDemo() {
return
}
```
Password and confirmation fields each provide a localized show/hide control while preserving their current values.
## Props [#props]
# (/docs/heroui/components/auth/verify-email)
Hover or focus the email-provider button to show a QR code for the same provider URL. Users can scan it when their email account is available on another device.
## Usage [#usage]
```tsx file=/src/demos/heroui/auth/verify-email.tsx
import { VerifyEmail } from "@better-auth-ui/heroui"
export function VerifyEmailDemo() {
return
}
```
## Props [#props]
# (/docs/heroui/components/auth-provider)
## Usage [#usage]
```tsx file=/../../examples/start-heroui-example/src/components/providers.tsx
import { AuthProvider } from "@better-auth-ui/heroui"
import { themePlugin } from "@better-auth-ui/heroui/plugins/theme"
import { Toast } from "@heroui/react"
import { useNavigate } from "@tanstack/react-router"
import { ThemeProvider, useTheme } from "next-themes"
import type { ReactNode } from "react"
import { authClient } from "@/lib/auth-client"
export function Providers({ children }: { children: ReactNode }) {
const navigate = useNavigate()
return (
{children}
)
}
```
## Localization [#localization]
Install the locale package:
```bash
bun add @better-auth-ui/locales
```
Import one locale and pass it to `AuthProvider`:
```tsx title="components/providers.tsx"
import { deDE } from "@better-auth-ui/locales/de-DE"
{children}
```
Locale bundles include the core messages and all built-in plugin messages. The locale also controls date, number, currency, and relative-time formatting in HeroUI components.
Use `localization` for product-specific text. These values take priority over the selected locale:
```tsx
{children}
```
### Match the browser language [#match-the-browser-language]
In a client-only application, import the supported locales and match `navigator.languages` against that list:
```tsx
import { matchAuthLocale } from "@better-auth-ui/locales"
import { deDE } from "@better-auth-ui/locales/de-DE"
import { enUS } from "@better-auth-ui/locales/en-US"
const locale = matchAuthLocale({
requested: navigator.languages,
supported: [enUS, deDE],
fallback: enUS
})
```
For server rendering, resolve the same locale from a user preference or the `Accept-Language` header. Pass that locale during the first render to prevent a hydration mismatch.
Changing the `locale` prop updates mounted auth components. Email components do not read `AuthProvider`; pass their localization on the server.
## Custom and Generic OAuth providers [#custom-and-generic-oauth-providers]
Built-in providers use their Better Auth ID as a string. For a custom or Generic OAuth provider, pass its ID, visible label, and optional icon.
```tsx title="components/providers.tsx"
import { Briefcase } from "@gravity-ui/icons"
}
]}
>
{children}
```
The same metadata appears on sign-in, sign-up, and linked-account views. BAUI sends only `id` to Better Auth.
Better Auth 1.7 registers Generic OAuth providers as normal social providers. Configure the same ID on the server:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { genericOAuth } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
genericOAuth({
config: [
{
providerId: "company-oauth",
clientId: process.env.COMPANY_OAUTH_CLIENT_ID!,
clientSecret: process.env.COMPANY_OAUTH_CLIENT_SECRET!,
discoveryUrl:
"https://id.example.com/.well-known/openid-configuration"
}
]
})
]
})
```
Register `/api/auth/callback/company-oauth` with the provider. See the [Better Auth Generic OAuth guide](https://www.better-auth.com/docs/plugins/generic-oauth) for endpoint and profile options.
## Popup social sign-in [#popup-social-sign-in]
Set `socialSignInMode="popup"` to keep the current page open during social sign-in. Redirect mode remains the default.
Better Auth marks this API as experimental. Configure the server and client plugins before you enable it:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { bearer, oauthPopup } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [bearer(), oauthPopup()]
})
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { oauthPopupClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [oauthPopupClient()]
})
```
Then select popup mode on the provider:
```tsx
{children}
```
Popup mode uses the same provider buttons and redirect target. It returns control to the current page, refreshes the session, and then runs the configured navigation.
## Props [#props]
# (/docs/heroui/components/email/change-email-confirmation-email)
## Usage [#usage]
```tsx file=/src/demos/heroui/email/change-email-confirmation-email.tsx#L13-
import { ChangeEmailConfirmationEmail } from "@better-auth-ui/heroui/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Better Auth setup [#better-auth-setup]
Render this template from `user.changeEmail.sendChangeEmailConfirmation` and send it to `user.email`. Better Auth supplies the approval URL and requested address.
```tsx
sendChangeEmailConfirmation: async ({ user, newEmail, url }) => {
const html = await render(
)
await sendEmail({
to: user.email,
subject: "Approve your email change",
html
})
}
```
Use `` separately if you also send a notification after the address has changed.
## Props [#props]
## Features [#features]
* Shows the current and requested email addresses
* Includes an approval button and fallback URL
* Explains that ignoring the message leaves the address unchanged
* Supports expiration details, theming, branding, and localization
# (/docs/heroui/components/email/delete-account-verification-email)
## Usage [#usage]
```tsx file=/src/demos/heroui/email/delete-account-verification-email.tsx#L13-
import { DeleteAccountVerificationEmail } from "@better-auth-ui/heroui/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Better Auth setup [#better-auth-setup]
Render this template from `user.deleteUser.sendDeleteAccountVerification`. Better Auth supplies the verification URL and user whose account is being deleted.
```tsx
sendDeleteAccountVerification: async ({ user, url }) => {
const html = await render(
)
await sendEmail({
to: user.email,
subject: "Confirm account deletion",
html
})
}
```
## Props [#props]
## Features [#features]
* Clearly states that account deletion is permanent
* Includes a verification button and fallback URL
* Explains that ignoring the message keeps the account active
* Supports expiration details, theming, branding, and localization
# (/docs/heroui/components/email/email-changed-email)
## Usage [#usage]
```tsx file=/src/demos/heroui/email/email-changed-email.tsx#L13-
import { EmailChangedEmail } from "@better-auth-ui/heroui/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Props [#props]
## Features [#features]
* Email change notification
* Shows previous and new email addresses
* Revert action button if unauthorized
* Support contact information
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/heroui/components/email/email-verification-email)
## Usage [#usage]
```tsx file=/src/demos/heroui/email/email-verification-email.tsx#L13-
import { EmailVerificationEmail } from "@better-auth-ui/heroui/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Props [#props]
## Features [#features]
* Verification button and fallback URL
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/heroui/components/email/magic-link-email)
## Usage [#usage]
```tsx file=/src/demos/heroui/email/magic-link-email.tsx#L13-
import { MagicLinkEmail } from "@better-auth-ui/heroui/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Props [#props]
## Features [#features]
* Sign-in button with magic link
* Fallback URL for manual copy/paste
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/heroui/components/email/new-device-email)
## Usage [#usage]
```tsx file=/src/demos/heroui/email/new-device-email.tsx#L13-
import { NewDeviceEmail } from "@better-auth-ui/heroui/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Props [#props]
## Features [#features]
* Device information display (browser, OS, location, IP)
* Timestamp of the sign-in
* Security action button if unauthorized
* Support contact information
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/heroui/components/email/organization-invitation-email)
## Usage [#usage]
```tsx file=/src/demos/heroui/email/organization-invitation-email.tsx#L13-
import { OrganizationInvitationEmail } from "@better-auth-ui/heroui/email"
import { render } from "@react-email/render"
const html = await render(
// biome-ignore lint/a11y/useValidAriaRole: `role` is a prop on the email component, not an ARIA role.
)
```
## Server setup [#server-setup]
Wire the email into the Better Auth `organization` plugin via `sendInvitationEmail`. Point `url` at the direct invitation view registered by `organizationPlugin()`.
```tsx title="auth.ts"
import { OrganizationInvitationEmail } from "@better-auth-ui/heroui/email"
import { render } from "@react-email/render"
import { betterAuth } from "better-auth"
import { organization } from "better-auth/plugins"
const baseUrl = process.env.BETTER_AUTH_URL!
export const auth = betterAuth({
plugins: [
organization({
async sendInvitationEmail(data) {
const html = await render(
)
await sendEmail({
to: data.email,
subject: `You're invited to ${data.organization.name}`,
html
})
}
})
]
})
```
Add `organizationPlugin().viewPaths.auth.acceptInvitation` to your auth
route allow-list. Use `{baseUrl}/settings/organizations` only when you want
the invitation email to open the full pending-invitations list instead.
## Props [#props]
## Features [#features]
* Inviter name and email display
* Organization name and optional organization logo
* Role being offered (for example member, admin, owner)
* Accept invitation button linking to the direct invitation view
* Fallback URL for manual copy/paste
* Optional expiration time
* Security notice for unexpected invitations
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/heroui/components/email/otp-email)
## Usage [#usage]
```tsx file=/src/demos/heroui/email/otp-email.tsx#L13-
import { OtpEmail } from "@better-auth-ui/heroui/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Props [#props]
## Features [#features]
* Large, prominently displayed verification code
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/heroui/components/email/password-changed-email)
## Usage [#usage]
```tsx file=/src/demos/heroui/email/password-changed-email.tsx#L13-
import { PasswordChangedEmail } from "@better-auth-ui/heroui/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Props [#props]
## Features [#features]
* Password change notification
* Timestamp of the change
* Security action button if unauthorized
* Support contact information
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/heroui/components/email/reset-password-email)
## Usage [#usage]
```tsx file=/src/demos/heroui/email/reset-password-email.tsx#L13-
import { ResetPasswordEmail } from "@better-auth-ui/heroui/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Props [#props]
## Features [#features]
* Password reset button and fallback URL
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/heroui/components/settings/account/account-settings)
## Usage [#usage]
```tsx file=/src/demos/heroui/settings/account/account-settings.tsx
import { AccountSettings } from "@better-auth-ui/heroui"
export function AccountSettingsDemo() {
return (
)
}
```
Current, new, and confirmation password fields provide independent localized show/hide controls.
When a user without a credential account requests a set-password email, the email-provider button appears after the request succeeds. Hover or focus it to show a QR code for opening the same provider URL on another device.
## Props [#props]
# (/docs/heroui/components/settings/security/linked-accounts)
## Usage [#usage]
```tsx file=/src/demos/heroui/settings/security/linked-accounts.tsx
import { LinkedAccounts } from "@better-auth-ui/heroui"
export function LinkedAccountsDemo() {
return (
)
}
```
## Props [#props]
## Fresh-session checks [#fresh-session-checks]
Sensitive session operations can return `SESSION_NOT_FRESH`. The active sessions card shows an inline password prompt for this response. After the user signs in again, it retries the session query. If password sign-in is disabled, the prompt links to the configured sign-in route.
# (/docs/heroui/components/settings/settings)
## Usage [#usage]
```tsx file=/src/demos/heroui/settings/settings.tsx
import { Settings } from "@better-auth-ui/heroui"
export function SettingsDemo() {
return (
)
}
```
## Props [#props]
# (/docs/heroui/components/user/user-avatar)
## Usage [#usage]
```tsx file=/src/demos/heroui/user/user-avatar.tsx
import { UserAvatar } from "@better-auth-ui/heroui"
export function UserAvatarDemo() {
return
}
```
## Props [#props]
# (/docs/heroui/components/user/user-button)
## Usage [#usage]
```tsx file=/src/demos/heroui/user/user-button.tsx
import { UserButton } from "@better-auth-ui/heroui"
export function UserButtonDemo() {
return
}
```
## Icon [#icon]
```tsx file=/src/demos/heroui/user/user-button-icon.tsx
import { UserButton } from "@better-auth-ui/heroui"
export function UserButtonIconDemo() {
return
}
```
## Custom links [#custom-links]
Use the `links` prop to add entries above the built-in items. Each entry is either a `{ label, href, icon?, variant?, visibility? }` descriptor or a fully rendered React element. `visibility` defaults to `"always"` and accepts `"authenticated" | "unauthenticated" | "always"`. Pass `hideSettings` to remove the built-in Settings link.
```tsx file=/src/demos/heroui/user/user-button-links.tsx
import { UserButton } from "@better-auth-ui/heroui"
import { LayoutCells, Persons } from "@gravity-ui/icons"
export function UserButtonLinksDemo() {
return (
,
visibility: "authenticated"
},
{
label: "Team",
href: "/team",
icon:
}
]}
/>
)
}
```
For interactive items shared across the app (for example a theme toggle or account switcher), prefer a plugin's `userMenuItems` slot over `links`.
## Props [#props]
# (/docs/heroui/components/user/user-view)
## Usage [#usage]
```tsx file=/src/demos/heroui/user/user-view.tsx
import { UserView } from "@better-auth-ui/heroui"
export function UserViewDemo() {
return
}
```
## Props [#props]
# Additional Fields (/docs/heroui/concepts/additional-fields)
`additionalFields` is an `AuthProvider` config option that declares extra user fields to render on the sign-up form and the user profile. Each field describes its data type, label, and optional UI rendering. Better Auth UI then handles rendering, parsing, and submitting the value through `signUp.email` (sign-up) and `updateUser` (profile).
Define the same fields in your Better Auth server config under `user.additionalFields`. The UI's `additionalFields` only controls rendering and form submission: the server still owns persistence and validation.
## Usage [#usage]
Pass an array of field configurations to ``:
```tsx
import { AuthProvider } from "@better-auth-ui/heroui"
{children}
```
Fields default to rendering on the user profile only. Set `signUp: true` to also render the field on the sign-up form.
Additional sign-up fields without `required: true` include an ` (optional)` suffix in their label. Override the complete suffix with `localization.auth.optional`, or set it to an empty string to remove the indicator.
## Field types [#field-types]
The `type` controls the data type of the field. The default `inputType` is inferred from `type`, but you can override it for a different look.
| `type` | Default `inputType` | Submitted as |
| ----------- | ------------------- | ------------ |
| `"string"` | `"input"` | `string` |
| `"number"` | `"number"` | `number` |
| `"boolean"` | `"switch"` | `boolean` |
| `"date"` | `"date"` | `Date` |
## Input types [#input-types]
Override the visual rendering with `inputType`:
| `inputType` | Renders |
| ------------ | ----------------------------------------------- |
| `"input"` | Single-line text input |
| `"textarea"` | Multi-line text input |
| `"number"` | Number field with increment / decrement buttons |
| `"slider"` | Slider with live value output |
| `"switch"` | Toggle switch |
| `"checkbox"` | Checkbox |
| `"select"` | Select dropdown |
| `"combobox"` | Searchable combo box |
| `"date"` | Date picker |
| `"datetime"` | Date picker with time field |
| `"hidden"` | Hidden input (submitted but not rendered) |
## Examples [#examples]
### Numeric formatting [#numeric-formatting]
`number` fields accept `Intl.NumberFormatOptions` via `formatOptions`:
```ts
{
name: "hourlyRate",
type: "number",
label: "Hourly rate",
formatOptions: { style: "currency", currency: "USD" }
}
{
name: "commissionRate",
type: "number",
label: "Commission rate",
formatOptions: { style: "percent", maximumFractionDigits: 2 }
}
```
Use `min`, `max`, and `step` to bound the value:
```ts
{
name: "yearsExperience",
type: "number",
label: "Years of experience",
min: 0,
max: 50,
step: 1
}
```
### Slider [#slider]
`inputType: "slider"` honors `min`, `max`, `step`, and `formatOptions`:
```ts
{
name: "budget",
type: "number",
label: "Budget",
inputType: "slider",
min: 0,
max: 5000,
step: 50,
defaultValue: 1000,
formatOptions: { style: "currency", currency: "USD" }
}
```
### Select / Combobox [#select--combobox]
Both accept an `options` array of `{ label, value }` objects:
```ts
{
name: "country",
type: "string",
label: "Country",
inputType: "select",
options: [
{ label: "United States", value: "us" },
{ label: "Canada", value: "ca" },
{ label: "United Kingdom", value: "gb" }
]
}
```
### Prefix / suffix [#prefix--suffix]
String inputs render inside an `InputGroup` when `prefix` or `suffix` is set:
```ts
{
name: "website",
type: "string",
label: "Website",
prefix: "https://",
suffix: ".com"
}
```
### Copy button [#copy-button]
Set `copyable: true` to add a copy button to the input. The button copies the current value, including an edited value.
Use this option with `readOnly: true` for fields such as `id`:
```ts
{
name: "id",
type: "string",
label: "User ID",
readOnly: true,
copyable: true
}
```
### Hidden value [#hidden-value]
`inputType: "hidden"` submits a value without rendering anything visible. Combine with `defaultValue` to attach a server-side preset:
```ts
{
name: "referralSource",
type: "string",
label: "Referral source",
inputType: "hidden",
defaultValue: "demo-app",
signUp: true
}
```
### Custom validation [#custom-validation]
Provide a `validate` callback to check a value before submission. If validation fails, throw an `Error`.
The interface shows the error message in a toast:
```ts
{
name: "nickname",
type: "string",
label: "Nickname",
signUp: true,
required: true,
validate: (value) => {
if (typeof value === "string" && !/^[a-zA-Z0-9_]+$/.test(value)) {
throw new Error(
"Nickname must only contain letters, numbers, and underscores"
)
}
}
}
```
## Where fields render [#where-fields-render]
| Flag | Default | Effect |
| ---------------- | ------- | -------------------------------------------- |
| `signUp: true` | `false` | Render on the sign-up form |
| `profile: false` | `true` | Hide on the user profile |
| `readOnly: true` | `false` | Render but exclude the value from submission |
## Type reference [#type-reference]
# Password Strength (/docs/heroui/concepts/passwords)
Every form that sets a *new* password renders a four-segment strength meter under the field: sign-up, reset password, change password, and the OTP and phone-number reset variants. The score is computed in the browser as the user types.
The meter is a hint, not a security control. It never blocks submission and it never reaches your server. Your Better Auth password rules stay the only thing that decides what is acceptable.
## Turning it off [#turning-it-off]
The meter is on by default. Switch it off through the `emailAndPassword` config:
```tsx title="components/providers.tsx"
{children}
```
## How the score works [#how-the-score-works]
`evaluatePasswordStrength` scores length first, then character variety, then marks the password down for patterns that read as strong but are not:
* Length at or above `minPasswordLength`, then again at `+4`, then again at 16 characters.
* Three or more of lowercase, uppercase, digits, and symbols. Using all four scores again.
* A password built from one or two distinct characters loses two points.
* A run of four or more characters from the alphabet, the digits, or the top keyboard row loses one point. `abcd`, `4321`, and `qwer` all count, in either direction.
Anything shorter than `minPasswordLength` is capped at **Weak**, so the meter never disagrees with the rule the form itself enforces.
You can call the same function directly if you need the score somewhere else:
```ts
import { evaluatePasswordStrength } from "@better-auth-ui/core"
const { score, level } = evaluatePasswordStrength(password, { minLength: 8 })
// score: 0 | 1 | 2 | 3 | 4
// level: "empty" | "weak" | "fair" | "good" | "strong"
```
## Breached passwords [#breached-passwords]
Better Auth's [`haveIBeenPwned`](https://www.better-auth.com/docs/plugins/have-i-been-pwned) plugin rejects passwords that appear in a known breach corpus. Add it on the server:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { haveIBeenPwned } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
haveIBeenPwned() // [!code highlight]
]
})
```
No UI plugin is needed. The rejection arrives as a `PASSWORD_COMPROMISED` error, and Better Auth UI renders it against the password field rather than as a toast, because it is something the user can fix right there. `` skips the code for the same reason.
Reword it through localization:
```tsx
{children}
```
To detect the same rejection in your own code, use the exported guard:
```ts
import { isPasswordCompromisedError } from "@better-auth-ui/core"
```
# Quick Start (/docs/heroui)
## Prerequisites [#prerequisites]
Install these prerequisites in your project:
* [Better Auth](https://www.better-auth.com/docs/installation)
* [HeroUI](https://heroui.com/docs/quick-start)
## Installation [#installation]
### Install the libraries [#install-the-libraries]
Install `@better-auth-ui/heroui`, `@better-auth-ui/react`, and `@better-auth-ui/core` using your preferred package manager.
npm
pnpm
yarn
bun
```bash
npm install @better-auth-ui/heroui@latest @better-auth-ui/react@latest @better-auth-ui/core@latest
```
```bash
pnpm add @better-auth-ui/heroui@latest @better-auth-ui/react@latest @better-auth-ui/core@latest
```
```bash
yarn add @better-auth-ui/heroui@latest @better-auth-ui/react@latest @better-auth-ui/core@latest
```
```bash
bun add @better-auth-ui/heroui@latest @better-auth-ui/react@latest @better-auth-ui/core@latest
```
### Configure Tailwind CSS [#configure-tailwind-css]
Add this `@import` to the global CSS file:
```css title="styles/app.css"
@import "@better-auth-ui/heroui/styles";
```
The `` router includes the built-in authentication views.
## Next steps [#next-steps]
Follow a framework-specific guide to integrate Better Auth UI into your project.
Integrate Better Auth UI with TanStack Start
Integrate Better Auth UI with Next.js
## React Reference [#react-reference]
Each HeroUI component uses the shared `@better-auth-ui/react` data layer. Read the React reference to access or change authentication state directly.
Hooks, queries, and mutations for every Better Auth endpoint.
# Next.js (/docs/heroui/integrations/nextjs)
## Prerequisites [#prerequisites]
Complete the [Quick Start](/docs/heroui) guide first.
## Integration [#integration]
### Create the QueryClient [#create-the-queryclient]
Create a shared `QueryClient` factory with the standard Next.js SSR pattern. Create one client per server request and one browser singleton.
```ts title="lib/query-client.ts" file=/../../examples/next-heroui-example/src/lib/query-client.ts
import { environmentManager, QueryClient } from "@tanstack/react-query"
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 5000
}
}
})
}
let browserQueryClient: QueryClient | undefined
export function getQueryClient() {
if (environmentManager.isServer()) {
// Server: always make a new query client
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
```
On the server, each call returns a new `QueryClient`. This design keeps the request cache separate for each user.
In the browser, the singleton preserves the React Query cache across navigation. It also gives `HydrationBoundary` a stable hydration target.
### Configure AuthProvider [#configure-authprovider]
Configure `AuthProvider` with Next.js navigation. Then wrap it in `QueryClientProvider` so it uses the shared client.
```tsx title="components/providers.tsx" file=/../../examples/next-heroui-example/src/components/providers.tsx
"use client"
import { AuthProvider } from "@better-auth-ui/heroui"
import { deleteUserPlugin } from "@better-auth-ui/heroui/plugins/delete-user"
import { Toast } from "@heroui/react"
import { QueryClientProvider } from "@tanstack/react-query"
import { useRouter } from "next/navigation"
import type { ReactNode } from "react"
import { authClient } from "@/lib/auth-client"
import { getQueryClient } from "@/lib/query-client"
export function Providers({ children }: { children: ReactNode }) {
const router = useRouter()
const queryClient = getQueryClient()
return (
replace ? router.replace(to) : router.push(to)
}
plugins={[deleteUserPlugin()]}
>
{children}
)
}
```
The `navigate` prop connects Better Auth UI to Next.js navigation. It accepts `{ to, replace }` options.
### Update the Root Layout [#update-the-root-layout]
Wrap your application with the `Providers` component in your root layout.
```tsx title="app/layout.tsx" file=/../../examples/next-heroui-example/src/app/layout.tsx
import type { Metadata } from "next"
import { Geist, Geist_Mono } from "next/font/google"
import { ThemeProvider } from "next-themes"
import type { ReactNode } from "react"
import "@/styles/app.css"
import { Header } from "@/components/header"
import { Providers } from "@/components/providers"
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"]
})
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"]
})
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app"
}
export default function RootLayout({
children
}: Readonly<{
children: ReactNode
}>) {
return (
{children}
)
}
```
### Create the Auth Page [#create-the-auth-page]
Create a dynamic auth page that renders the appropriate authentication view based on the path:
```tsx title="app/auth/[path]/page.tsx" file=/../../examples/next-heroui-example/src/app/auth/[path]/page.tsx
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { notFound } from "next/navigation"
export default async function AuthPage({
params
}: {
params: Promise<{
path: string
}>
}) {
const { path } = await params
if (!Object.values(viewPaths.auth).includes(path)) {
notFound()
}
return (
)
}
```
The built-in `viewPaths.auth` object contains the standard authentication paths. These include `redirect`, `sign-in`, `sign-up`, `sign-out`, and password recovery paths.
If a plugin adds authentication paths, spread its `viewPaths.auth` into the validation set. See the [Magic Link guide](/docs/heroui/plugins/magic-link) for an example.
### Create the Settings page [#create-the-settings-page]
Create a dynamic settings route for the URL segment.
The async server component validates the path and session. It redirects unauthenticated users before it sends HTML.
The component also adds the session query to `HydrationBoundary`. As a result, child hooks skip their loading state during hydration.
```tsx title="app/settings/[path]/page.tsx" file=/../../examples/next-heroui-example/src/app/settings/[path]/page.tsx
import { viewPaths } from "@better-auth-ui/core"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { Settings } from "@better-auth-ui/heroui"
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"
import { headers } from "next/headers"
import { notFound, redirect } from "next/navigation"
import { auth } from "@/lib/auth"
import { getQueryClient } from "@/lib/query-client"
export default async function SettingsPage({
params
}: {
params: Promise<{
path: string
}>
}) {
const { path } = await params
if (!Object.values(viewPaths.settings).includes(path)) {
notFound()
}
const requestHeaders = await headers()
const queryClient = getQueryClient()
const session = await ensureSessionServer(queryClient, auth, {
headers: requestHeaders
})
if (!session) {
redirect(
`/auth/sign-in?redirectTo=${encodeURIComponent(`/settings/${path}`)}`
)
}
return (
)
}
```
The built-in `viewPaths.settings` object contains `account` and `security`. If a plugin adds settings views, spread its `viewPaths.settings` into the validation set.
## Protecting Routes [#protecting-routes]
Better Auth UI provides separate protection patterns for server-rendered and prerendered routes.
### Server-rendered routes (async server component) [#server-rendered-routes-async-server-component]
For an SSR route, read the session in an async server component. This redirects unauthenticated users before the server streams HTML.
Call `ensureSessionServer` from `@better-auth-ui/core/server` with a separate `QueryClient` for each request. This helper calls `auth.api` directly.
Wrap the rendered children in `HydrationBoundary`. Then downstream `useSession` calls read `authQueryKeys.session` from the hydrated cache.
```tsx title="app/dashboard/page.tsx"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"
import { headers } from "next/headers"
import Link from "next/link"
import { redirect } from "next/navigation"
import { auth } from "@/lib/auth"
import { getQueryClient } from "@/lib/query-client"
export default async function Dashboard() {
const queryClient = getQueryClient()
const session = await ensureSessionServer(queryClient, auth, {
headers: await headers()
})
if (!session) {
redirect("/auth/sign-in?redirectTo=/dashboard")
}
return (
Hello, {session.user.email}
Sign Out
)
}
```
`ensureSession` adds the session to the query cache during SSR. `HydrationBoundary` sends this state to the browser.
As a result, child components that call `useSession` render without a loading state.
If a client component outside the protected route reads the session, prefetch the session in its server parent. Then wrap that subtree in `HydrationBoundary`.
This pattern commonly applies to a header or sidebar that contains `UserButton`. Without the prefetch, the component starts a new browser request.
See the [example header](https://github.com/better-auth-ui/better-auth-ui/blob/main/examples/next-heroui-example/src/components/header.tsx) for the complete pattern.
### Reactive protection and prerendered routes (`useAuthenticate`) [#reactive-protection-and-prerendered-routes-useauthenticate]
Server-side session checks only occur when the route loads. They do not detect session changes while the page remains mounted.
These changes include token expiration, sign-out in another tab, and server-side session revocation. Prerendered and client-rendered routes also have no server check.
The `useAuthenticate` hook covers both cases. It subscribes to `useSession` and redirects the user when the session ends.
The hook preserves the current URL in the `redirectTo` query parameter.
Use it in two situations:
1. **Alongside an async server component** for server-rendered routes, as a second layer that keeps the UI in sync after the initial load.
2. **On its own** for prerendered or client-rendered routes that have no server-side session access.
```tsx title="app/dashboard/page.tsx"
"use client"
import { authClient } from "@/lib/auth-client"
import { useAuthenticate } from "@better-auth-ui/react"
import { Spinner } from "@heroui/react"
import Link from "next/link"
export default function Dashboard() {
const { data: session } = useAuthenticate(authClient)
if (!session) {
return (
)
}
return (
Hello, {session.user.email}
Sign Out
)
}
```
The async server component protects the initial render and hydrates the session. Then `useAuthenticate` reacts to later session changes.
## Example Project [#example-project]
For a complete example, see [next-heroui-example](https://github.com/better-auth-ui/better-auth-ui/tree/main/examples/next-heroui-example) in the repository.
## Next Steps [#next-steps]
Read about the shared React hooks and query primitives that power each Better Auth UI component.
Hooks, queries, and mutations for every Better Auth endpoint.
Every auth read, with usage and server-side recipes.
Every auth write, with mutation keys and cache side effects.
# TanStack Start (/docs/heroui/integrations/tanstack-start)
## Prerequisites [#prerequisites]
Complete the [Quick Start](/docs/heroui) guide first.
## Integration [#integration]
### Configure AuthProvider [#configure-authprovider]
Configure `AuthProvider` with TanStack Router's navigation.
```tsx title="components/providers.tsx" file=/../../examples/start-heroui-example/src/components/providers.tsx
import { AuthProvider } from "@better-auth-ui/heroui"
import { themePlugin } from "@better-auth-ui/heroui/plugins/theme"
import { Toast } from "@heroui/react"
import { useNavigate } from "@tanstack/react-router"
import { ThemeProvider, useTheme } from "next-themes"
import type { ReactNode } from "react"
import { authClient } from "@/lib/auth-client"
export function Providers({ children }: { children: ReactNode }) {
const navigate = useNavigate()
return (
{children}
)
}
```
The `navigate` prop connects Better Auth UI to TanStack Router. It accepts `{ to, replace }` options.
### Update the Root Route [#update-the-root-route]
Wrap your application with the `Providers` component in your root route.
```tsx title="routes/__root.tsx" file=/../../examples/start-heroui-example/src/routes/__root.tsx
import { TanStackDevtools } from "@tanstack/react-devtools"
import type { QueryClient } from "@tanstack/react-query"
import {
createRootRouteWithContext,
HeadContent,
Scripts
} from "@tanstack/react-router"
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"
import type { ReactNode } from "react"
import { Header } from "@/components/header"
import { Providers } from "@/components/providers"
import appCss from "@/styles/app.css?url"
export const Route = createRootRouteWithContext<{
queryClient: QueryClient
}>()({
head: () => ({
meta: [
{
charSet: "utf-8"
},
{
name: "viewport",
content: "width=device-width, initial-scale=1"
},
{
title: "Start HeroUI Example"
}
],
links: [
{
rel: "stylesheet",
href: appCss
}
]
}),
shellComponent: RootDocument
})
function RootDocument({ children }: { children: ReactNode }) {
return (
{children}
}
]}
/>
)
}
```
### Create the Auth Page [#create-the-auth-page]
Create a dynamic auth page that renders the appropriate authentication view based on the path:
```tsx title="routes/auth/$path.tsx" file=/../../examples/start-heroui-example/src/routes/auth/$path.tsx
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { magicLinkPlugin } from "@better-auth-ui/heroui/plugins/magic-link"
import { createFileRoute, notFound } from "@tanstack/react-router"
/** Keep in sync with `magicLinkPlugin(...)` in `providers.tsx` if you customize `path`. */
const validAuthPathSegments = new Set([
...Object.values(viewPaths.auth),
magicLinkPlugin().viewPaths.auth.magicLink
])
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (!validAuthPathSegments.has(path)) {
throw notFound()
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return (
)
}
```
The built-in `viewPaths.auth` object contains the standard authentication paths. These include `redirect`, `sign-in`, `sign-up`, `sign-out`, and password recovery paths.
If a plugin adds authentication paths, spread its `viewPaths.auth` into the validation set. See the [Magic Link guide](/docs/heroui/plugins/magic-link) for an example.
### Create the Settings page [#create-the-settings-page]
Create a dynamic settings route for the URL segment. Validate the segment against `viewPaths.settings`.
Return a 404 response for an unknown path.
```tsx title="routes/settings/$path.tsx" file=/../../examples/start-heroui-example/src/routes/settings/$path.tsx
import { ensureSession, viewPaths } from "@better-auth-ui/core"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { Settings } from "@better-auth-ui/heroui"
import { organizationPlugin } from "@better-auth-ui/heroui/plugins/organization"
import { createFileRoute, notFound, redirect } from "@tanstack/react-router"
import { createIsomorphicFn } from "@tanstack/react-start"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
import { authClient } from "@/lib/auth-client"
/** Same pattern as magic-link: spread plugin `viewPaths.settings` into the allowed segment set. */
const validSettingsPaths = [
...Object.values(viewPaths.settings),
...Object.values(organizationPlugin().viewPaths.settings)
]
export const Route = createFileRoute("/settings/$path")({
async beforeLoad({ params: { path }, context: { queryClient }, location }) {
if (!validSettingsPaths.includes(path)) {
throw notFound()
}
const ensureSessionIso = createIsomorphicFn()
.server(() =>
ensureSessionServer(queryClient, auth, { headers: getRequestHeaders() })
)
.client(() => ensureSession(queryClient, authClient))
const session = await ensureSessionIso()
if (!session) {
throw redirect({
to: "/auth/$path",
params: { path: "sign-in" },
search: { redirectTo: location.href }
})
}
return { session }
},
component: SettingsPage
})
function SettingsPage() {
const { path } = Route.useParams()
return (
)
}
```
The built-in `viewPaths.settings` object contains `account` and `security`. If a plugin adds settings views, spread its `viewPaths.settings` into the validation set.
## Protecting Routes [#protecting-routes]
Better Auth UI provides separate protection patterns for server-rendered and prerendered routes.
### Server-rendered routes (`beforeLoad`) [#server-rendered-routes-beforeload]
For an SSR route, read the session in `beforeLoad`. This redirects unauthenticated users before a component renders.
Use `createIsomorphicFn` to call `ensureSessionServer` on the server and `ensureSession` in the browser. The server helper calls `auth.api` directly.
Both helpers use `authQueryKeys.session` in the same TanStack Query cache. Child `useSession` calls can reuse the hydrated session.
```tsx title="routes/dashboard.tsx"
import { ensureSession } from "@better-auth-ui/core"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { createFileRoute, Link, redirect } from "@tanstack/react-router"
import { createIsomorphicFn } from "@tanstack/react-start"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
import { authClient } from "@/lib/auth-client"
export const Route = createFileRoute("/dashboard")({
async beforeLoad({ context: { queryClient }, location }) {
const ensureSessionIso = createIsomorphicFn()
.server(() =>
ensureSessionServer(queryClient, auth, { headers: getRequestHeaders() })
)
.client(() => ensureSession(queryClient, authClient))
const session = await ensureSessionIso()
if (!session) {
throw redirect({
to: "/auth/$path",
params: { path: "sign-in" },
search: { redirectTo: location.href }
})
}
return { session }
},
component: Dashboard
})
function Dashboard() {
const { session } = Route.useRouteContext()
return (
Hello, {session.user.email}
Sign Out
)
}
```
Child routes and components can read the returned `{ session }` through `Route.useRouteContext()`.
`ensureSessionServer` also adds the session to the query cache during SSR. Downstream `useSession` calls then render without a loading state.
### Reactive protection and prerendered routes (`useAuthenticate`) [#reactive-protection-and-prerendered-routes-useauthenticate]
`beforeLoad` only runs when the route loads. It does not detect session changes while the page remains mounted.
These changes include token expiration, sign-out in another tab, and server-side session revocation. Prerendered and client-rendered routes also have no server check.
The `useAuthenticate` hook covers both cases. It subscribes to `useSession` and redirects the user when the session ends.
The hook preserves the current URL in the `redirectTo` query parameter.
Use it in two situations:
1. **Alongside `beforeLoad`** for server-rendered routes, as a second layer that keeps the UI in sync after the initial load.
2. **On its own** for prerendered or client-rendered routes that have no server-side session access.
```tsx title="routes/dashboard.tsx"
import { authClient } from "@/lib/auth-client"
import { useAuthenticate } from "@better-auth-ui/react"
import { Spinner } from "@heroui/react"
import { createFileRoute, Link } from "@tanstack/react-router"
export const Route = createFileRoute("/dashboard")({
component: Dashboard
})
function Dashboard() {
const { data: session } = useAuthenticate(authClient)
if (!session) {
return (
)
}
return (
Hello, {session.user.email}
Sign Out
)
}
```
`beforeLoad` protects the initial render and hydrates the session. Then `useAuthenticate` reacts to later session changes.
## Example Project [#example-project]
For a complete example, see [start-heroui-example](https://github.com/better-auth-ui/better-auth-ui/tree/main/examples/start-heroui-example) in the repository.
## Next Steps [#next-steps]
Read about the shared React hooks and query primitives that power each Better Auth UI component.
Hooks, queries, and mutations for every Better Auth endpoint.
Every auth read, with usage and server-side recipes.
Every auth write, with mutation keys and cache side effects.
# Admin (/docs/heroui/plugins/admin)
The Admin plugin adds a static `/admin/users` page and a user-detail drawer.
It also adds a "Stop impersonating" action to ``.
## Setup [#setup]
### Enable the Better Auth admin plugin [#enable-the-better-auth-admin-plugin]
Add `admin()` to the server configuration:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { admin } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
admin() // [!code highlight]
]
})
```
Update your database schema after enabling the plugin. Better Auth adds admin
fields to users and an `impersonatedBy` field to sessions.
### Add the matching client plugin [#add-the-matching-client-plugin]
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { adminClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [adminClient()] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { adminPlugin } from "@better-auth-ui/heroui/plugins" // [!code highlight]
{children}
```
## Add the users route [#add-the-users-route]
Create one static route for the users page:
```tsx title="app/admin/users/page.tsx"
import { Admin } from "@better-auth-ui/heroui"
export default function AdminUsersPage() {
return
}
```
Use `` when a parent route passes the final static path
segment. The user-detail drawer keeps user IDs out of the route contract.
Applications can control the drawer through ``:
```tsx
```
## Inspector tabs [#inspector-tabs]
The user inspector includes local Overview and Sessions tabs. Registered
plugins can add more tabs without adding routes.
The Dash integration adds an Activity tab when both UI plugins are registered.
Dash applies its own organization owner or admin access rules to this tab.
## User actions [#user-actions]
The users page can create users. The drawer can update a user's name and role,
set a password, ban or unban the user, impersonate the user, delete the user,
and revoke one or all of the user's sessions.
The UI checks the matching Admin client permission before it enables each
action. Dangerous actions require confirmation. The UI also disables actions
that would ban, delete, impersonate, or revoke sessions for the current user.
## Permissions and privacy [#permissions-and-privacy]
The users page calls the Better Auth permission API before it requests the
user list. Do not authorize the page from a role string alone.
The table searches one field per request. The supported fields are `email`
and `name`.
Passwords stay in local form state and never enter query keys. The forms clear
each password after the request or when the user closes the form. Session IP
addresses are hidden unless `showIpAddress` is `true`.
Configure custom roles with the same names in Better Auth and the UI plugin:
```ts
adminPlugin({
allowMultipleRoles: false,
defaultRole: "member",
impersonationRedirectTo: "/",
pageSize: 25,
roles: ["member", "support", "admin"],
showIpAddress: false
})
```
Set `allowMultipleRoles` to `false` to make the create and edit forms accept
one role. This option does not change `adminRoles`, which controls administrator
access.
The public Admin client does not provide account disconnection, global
organization administration, or a Sentinel dashboard. These views are not
part of this integration.
## User button behavior [#user-button-behavior]
`adminPlugin()` contributes `` through the
`userMenuItems` slot. `` places it above sign out.
The action renders only when `session.session.impersonatedBy` is present.
Selecting it calls `authClient.admin.stopImpersonating()` and refreshes the
cached session before the pending state completes.
```tsx
import { StopImpersonating } from "@better-auth-ui/heroui/plugins"
```
## Options [#options]
```ts
adminPlugin({
localization: {
stopImpersonating: "Return to admin"
}
})
```
## Localization [#localization]
## Mutation API [#mutation-api]
```tsx
import { useStopImpersonating } from "@better-auth-ui/react/plugins/admin"
const stopImpersonating = useStopImpersonating(authClient)
```
Use the hook when you need the same behavior outside the user button. It
restores the admin session and awaits invalidation of the shared session query.
# Agent Auth (/docs/heroui/plugins/agent-auth)
The Agent Auth plugin adds the application-owned UI that the protocol does not render. It shows the requesting agent, host, mode, capabilities, and required approval strength. Users can allow selected capabilities or deny the request. A security settings card lists agents and revokes individual active grants.
## Setup [#setup]
### Configure Agent Auth on the server [#configure-agent-auth-on-the-server]
Point `deviceAuthorizationPage` at the BAUI route. Define each capability with a clear description and the approval strength it needs.
```ts title="lib/auth.ts"
import { agentAuth } from "@better-auth/agent-auth"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
plugins: [
agentAuth({
deviceAuthorizationPage: "/auth/agent-approval",
capabilities: [
{
name: "invoices:read",
description: "Read invoices and payment status",
approvalStrength: "session"
},
{
name: "invoices:pay",
description: "Pay an invoice with a saved method",
approvalStrength: "webauthn"
}
]
})
]
})
```
Apply the Agent Auth schema after enabling the plugin. See the [Better Auth Agent Auth guide](https://www.better-auth.com/docs/plugins/agent-auth).
### Add the client plugin and adapter [#add-the-client-plugin-and-adapter]
```ts title="lib/auth-client.ts"
import { agentAuthClient } from "@better-auth/agent-auth/client"
import { createAgentAuthClientAdapter } from "@better-auth-ui/core/plugins/agent-auth"
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
plugins: [agentAuthClient()]
})
export const agentAuthAdapter = createAgentAuthClientAdapter(authClient)
```
The native adapter combines the agent record, pending grants, and capability catalog into one presentation model. It also provides grant listing and revocation.
### Register the UI plugin [#register-the-ui-plugin]
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { agentAuthPlugin } from "@better-auth-ui/heroui/plugins/agent-auth"
{children}
```
### Allow the approval path [#allow-the-approval-path]
Your route that renders `` must accept `agent-approval`. Keep this path equal to `deviceAuthorizationPage`. The view preserves the full approval URL when it sends a signed-out user to sign in.
```tsx title="routes/auth/$path.tsx"
const validAuthPathSegments = new Set([
...Object.values(viewPaths.auth),
agentAuthPlugin({ adapter: agentAuthAdapter }).viewPaths.auth.agentApproval
])
```
## Passkey approvals [#passkey-approvals]
Capabilities with `approvalStrength: "webauthn"` return a WebAuthn challenge. Connect your passkey library through `authenticateWithPasskey`. BAUI retries the same approval with the signed response.
```ts title="lib/auth-client.ts"
export const agentAuthAdapter = createAgentAuthClientAdapter(authClient, {
authenticateWithPasskey: async (options) => {
return runPasskeyAuthentication(options)
}
})
```
The user must have a passkey before approving a capability that requires WebAuthn. Show your normal passkey enrollment flow when the server reports that no passkey is enrolled.
## Approval and grant views [#approval-and-grant-views]
`` reads `agent_id`, `approval_id`, and `code` from the current URL. It lets the user approve a subset of pending capabilities.
`` appears in security settings by default. Set `grants: false` to hide it. You can also render either component directly from `@better-auth-ui/heroui/plugins/agent-auth`.
## Custom adapters [#custom-adapters]
Implement `AgentAuthAdapter` when your application resolves autonomous-agent approval details through a server route or needs a custom policy layer. The UI does not depend on Better Auth response shapes after the adapter boundary.
## Options [#options]
## Localization [#localization]
# Anonymous (/docs/heroui/plugins/anonymous)
The anonymous UI plugin contributes one "Continue as guest" button to the authentication forms. A successful sign-in refreshes the session and follows the `redirectTo` configured on ``.
## Setup [#setup]
### Configure the Better Auth server plugin [#configure-the-better-auth-server-plugin]
Add Better Auth's [Anonymous](https://www.better-auth.com/docs/plugins/anonymous) plugin:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { anonymous } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
anonymous() // [!code highlight]
]
})
```
Run the Better Auth migration command so the user table includes `isAnonymous`:
```bash
bunx @better-auth/cli migrate
```
### Configure the Better Auth client plugin [#configure-the-better-auth-client-plugin]
Add `anonymousClient()` to the browser client:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { anonymousClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [
anonymousClient() // [!code highlight]
]
})
```
### Register the Better Auth UI plugin [#register-the-better-auth-ui-plugin]
Pass `anonymousPlugin()` to ``:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { anonymousPlugin } from "@better-auth-ui/heroui/plugins" // [!code highlight]
{children}
```
## Change the label [#change-the-label]
Override the plugin localization when the default wording does not fit your product:
```tsx
anonymousPlugin({
localization: {
continueAsGuest: "Explore as a guest"
}
})
```
This integration only adds guest entry. If your application later lets guests create a permanent account, configure Better Auth's server-side `onLinkAccount` callback to move application data to the new user.
# API Key (/docs/heroui/plugins/api-key)
The API key plugin adds programmatic API key management to your authentication UI. Users can create, copy, and revoke API keys from a security card in account settings. New keys can use a configurable expiration interval, and each listed key shows when it expires.
It contributes:
* An `` card to the security settings tab for user-owned keys
* An `` card to `` for organization-owned keys (opt-in via `apiKeyPlugin({ organization: true })`. Requires the [organization plugin](/docs/heroui/plugins/organization) and a matching server-side API key config)
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Install the [`@better-auth/api-key`](https://www.better-auth.com/docs/plugins/api-key) package and add it to your Better Auth server config:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
apiKey() // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `apiKeyClient()` to your auth client so `authClient.apiKey.*` methods are available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { apiKeyClient } from "@better-auth/api-key/client" // [!code highlight]
export const authClient = createAuthClient({
plugins: [apiKeyClient()] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Pass `apiKeyPlugin()` to ``:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { apiKeyPlugin } from "@better-auth-ui/heroui/plugins/api-key" // [!code highlight]
import { authClient } from "@/lib/auth-client"
{children}
```
## Configure expiration [#configure-expiration]
The create dialog offers 30 days, 90 days, and Never by default. It initially selects 30 days. Configure the choices through `apiKeyPlugin()`:
```tsx title="components/providers.tsx"
apiKeyPlugin({
keyExpiration: {
intervals: [7, 30, 90],
defaultInterval: 30,
allowNever: true
}
})
```
`intervals` and `defaultInterval` use days. Better Auth receives the selected lifetime as seconds.
Keep the UI choices within the limits in your Better Auth server configuration:
```ts title="lib/auth.ts"
apiKey({
keyExpiration: {
minExpiresIn: 7,
maxExpiresIn: 90,
defaultExpiresIn: null
}
})
```
When `allowNever` is enabled, selecting Never sends no custom interval. Better Auth will still apply `defaultExpiresIn` if the server defines one, so set `allowNever: false` in the UI when your server always requires expiration.
To remove the expiration field and rely entirely on the server default:
```tsx title="components/providers.tsx"
apiKeyPlugin({ keyExpiration: false })
```
## Components [#components]
### `` [#apikeys-]
The `` security card is automatically rendered in `` when the plugin is registered. Pass `organizationId` to scope the list and create payload to an organization instead of the signed-in user.
**Usage**
```tsx
import { ApiKeys } from "@better-auth-ui/heroui/plugins/api-key"
```
**Props**
### `` [#organizationapikeys-]
A thin wrapper around `` that resolves the active organization via `useActiveOrganization` and forwards its id. Rendered inside `` only when the plugin is registered with `{ organization: true }`.
To enable, opt in on the UI plugin and add a matching API key configuration to your Better Auth server config. The plugin uses a fixed `configId` of `"organization"`, so the server entry **must** be `{ configId: "organization", references: "organization" }`:
```ts title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { apiKeyPlugin } from "@better-auth-ui/heroui/plugins/api-key"
import { authClient } from "@/lib/auth-client"
{children}
```
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
import { organization } from "better-auth/plugins"
export const auth = betterAuth({
// ...
plugins: [
organization(),
apiKey([
{ configId: "default", references: "user" },
{ configId: "organization", references: "organization" } // [!code highlight]
])
]
})
```
See the [Better Auth docs](https://www.better-auth.com/docs/plugins/api-key/advanced#organization-owned-api-keys) for role-based permissions on organization-owned keys.
**Usage**
```tsx
import { OrganizationApiKeys } from "@better-auth-ui/heroui/plugins/api-key"
```
**Props**
## Options [#options]
## Localization [#localization]
## Lifecycle controls [#lifecycle-controls]
`` lets users create, rename, and delete keys. The create form exposes the name, configuration, expiration, and organization. The list shows status, remaining requests, request usage, and the last request time as read-only values.
The built-in dialog does not show metadata because metadata belongs to the application. For metadata, build a custom form with `useCreateApiKey`. Map named fields or application state to the metadata object. Do not show a raw JSON editor. If metadata affects trusted behavior, validate it in a server route.
Better Auth reserves enablement, permissions, quotas, refill rules, and rate limits for server-side creation and updates. Configure those values in trusted server code instead of exposing them in the account UI.
Configure the choices that the UI exposes:
```tsx
apiKeyPlugin({
configurations: [
{ id: "default", label: "Personal", organization: false },
{ id: "organization", label: "Organization", organization: true }
],
pageSize: 20
})
```
The server must define every listed `configId`. Use `useUpdateApiKey` when you build a custom rename surface.
# Billing (/docs/heroui/plugins/billing)
The billing plugin adds a complete billing tab to personal or organization settings. Its components use a generic `BillingAdapter`, so the UI does not depend on provider-specific objects. BAUI includes adapters for Stripe, Polar, Autumn, Creem, Dodo Payments, and Commet.
The billing view includes pricing plans, checkout, subscription status, portal access, cancellation, restoration, seats, and metered usage.
## Setup [#setup]
### Configure billing in Better Auth [#configure-billing-in-better-auth]
Configure one of Better Auth's supported billing plugins. Add its client plugin and apply its database schema.
See the provider guides for [Stripe](https://www.better-auth.com/docs/beta/plugins/stripe), [Polar](https://www.better-auth.com/docs/plugins/polar), [Autumn](https://www.better-auth.com/docs/plugins/autumn), [Creem](https://www.better-auth.com/docs/plugins/creem), [Dodo Payments](https://docs.dodopayments.com/developer-resources/better-auth-adaptor), and [Commet](https://www.better-auth.com/docs/plugins/commet).
### Create a provider-neutral adapter [#create-a-provider-neutral-adapter]
```ts title="lib/billing.ts"
import {
type BillingPlan,
createStripeBillingAdapter
} from "@better-auth-ui/core/plugins/billing"
import { authClient } from "./auth-client"
const plans = [
{
id: "pro",
name: "Pro",
description: "For teams shipping production applications.",
prices: [
{ id: "pro-month", amount: 2000, currency: "USD", interval: "month" },
{ id: "pro-year", amount: 19200, currency: "USD", interval: "year" }
],
features: ["Unlimited projects", "Priority support"],
highlighted: true,
seatBased: true
}
] satisfies BillingPlan[]
export const billingAdapter = createStripeBillingAdapter(authClient, {
plans,
successUrl: "/settings/billing?checkout=success",
cancelUrl: "/settings/billing?checkout=canceled",
returnUrl: "/settings/billing"
})
```
Price amounts use the currency's smallest unit. For example, `2000` USD means `$20.00`.
### Register the HeroUI plugin [#register-the-heroui-plugin]
This snippet shows only the billing additions. The [TanStack Start](/docs/heroui/integrations/tanstack-start) and [Next.js](/docs/heroui/integrations/nextjs) guides cover the full provider, including where `authClient` and `navigate` come from.
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { billingPlugin } from "@better-auth-ui/heroui/plugins/billing"
import { organizationPlugin } from "@better-auth-ui/heroui/plugins/organization"
import { billingAdapter } from "@/lib/billing"
{children}
```
Personal billing is enabled by default. Set `organization: true` to add the organization billing tab.
## Polar adapter [#polar-adapter]
Use the Polar adapter with the same generic plan list. Map each BAUI plan to a Polar product ID or checkout slug.
```ts title="lib/billing.ts"
import { createPolarBillingAdapter } from "@better-auth-ui/core/plugins/billing"
export const billingAdapter = createPolarBillingAdapter(authClient, {
plans,
products: {
pro: { type: "product", value: "123e4567-e89b-12d3-a456-426614174000" }
},
successUrl: "/settings/billing?checkout=success",
cancelUrl: "/settings/billing?checkout=canceled",
returnUrl: "/settings/billing"
})
```
Polar handles cancellation, restoration, and seat changes in its customer portal. Its adapter marks those direct actions as unsupported, so BAUI shows a manage-billing action instead. Stripe uses Better Auth's subscription endpoints.
## Other bundled adapters [#other-bundled-adapters]
| Adapter | Checkout and state | Direct actions | Scope |
| ------------- | ---------------------------------------- | ---------------------------------------- | ------------------------------ |
| Stripe | Better Auth subscription API | Cancel, restore, seats | User and explicit organization |
| Polar | Checkout, subscriptions, usage | Portal fallback | User and explicit organization |
| Autumn | Attach, customer subscriptions, balances | Cancel, restore, optional license seats | User |
| Creem | Checkout and active subscription | Cancel | User |
| Dodo Payments | Checkout session and subscription list | Portal fallback | User |
| Commet | Portal and current subscription | Cancel, optional feature usage and seats | User |
```ts title="lib/billing.ts"
import {
createAutumnBillingAdapter,
createCommetBillingAdapter,
createCreemBillingAdapter,
createDodoPaymentsBillingAdapter
} from "@better-auth-ui/core/plugins/billing"
import { createAutumnClient } from "autumn-js/react"
const urls = {
successUrl: "/settings/billing?checkout=success",
cancelUrl: "/settings/billing?checkout=canceled",
returnUrl: "/settings/billing"
}
const autumnClient = createAutumnClient({
pathPrefix: "/api/auth/autumn",
includeCredentials: true
})
export const autumnAdapter = createAutumnBillingAdapter(autumnClient, {
plans,
seatLicensePlans: { pro: "team-seat" },
...urls
})
export const creemAdapter = createCreemBillingAdapter(authClient, {
plans,
products: { pro: "prod_creem_pro" },
...urls
})
export const dodoAdapter = createDodoPaymentsBillingAdapter(authClient, {
plans,
products: { pro: { type: "slug", value: "pro" } },
...urls
})
export const commetAdapter = createCommetBillingAdapter(authClient, {
plans,
planIds: { pro: "commet-plan-id" },
usage: true,
seatFeatureCode: "members",
...urls
})
```
Enable the matching provider sub-plugins. Dodo needs checkout and portal. Commet needs portal and subscriptions, plus features or seats when you enable those adapter options.
Autumn, Creem, Dodo Payments, and Commet resolve the signed-in customer. Their browser APIs do not accept an explicit organization ID. Their adapters declare `scopes.organization: false`. `billingPlugin` rejects an organization billing configuration instead of reading active organization state.
## Custom adapter [#custom-adapter]
Implement `BillingAdapter` to connect another billing service. The adapter receives an explicit user or organization scope for every operation.
```ts
import type { BillingAdapter } from "@better-auth-ui/core/plugins/billing"
export const billingAdapter: BillingAdapter = {
id: "custom",
scopes: { user: true, organization: true },
supports: { cancel: true, restore: true, seats: true },
listPlans: async (scope, signal) => billingApi.listPlans(scope, signal),
getState: async (scope, signal) => billingApi.getState(scope, signal),
checkout: async (scope, input) => billingApi.checkout(scope, input),
openPortal: async (scope) => billingApi.openPortal(scope),
cancel: async (scope, subscriptionId) =>
billingApi.cancel(scope, subscriptionId),
restore: async (scope, subscriptionId) =>
billingApi.restore(scope, subscriptionId),
updateSeats: async (scope, subscriptionId, seats) =>
billingApi.updateSeats(scope, subscriptionId, seats)
}
```
Validate organization membership and billing permissions on the server. Do not trust organization IDs or slugs from the browser.
## Components and hooks [#components-and-hooks]
```tsx
import {
BillingSettings,
OrganizationBillingSettings,
UserBillingSettings
} from "@better-auth-ui/heroui/plugins/billing"
```
For custom views, import the provider-neutral React hooks from `@better-auth-ui/react/plugins/billing`.
# Captcha (/docs/heroui/plugins/captcha)
The captcha plugin adds a widget to the sign-in, sign-up, and forgot-password forms. It sends the resolved token in the `x-captcha-response` header.
The plugin supports Cloudflare Turnstile, hCaptcha, CaptchaFox, and reCAPTCHA. Provide a `render` component that connects the provider callbacks to the plugin.
It contributes:
* A captcha widget rendered above the submit button on sign-in, sign-up, and forgot-password forms
* Automatic header management. The plugin clears the token after an error or expiration, or when the component unmounts.
* Automatic widget refresh after an unsuccessful submission. Captcha tokens are single-use, so each retry requires a new token.
## Social sign-in [#social-sign-in]
Provider buttons forward the current CAPTCHA token to `/sign-in/social`. Add this endpoint to the server CAPTCHA configuration to protect social sign-in. Failed requests clear the token and reset the widget before another attempt.
Use `socialSignInMode="redirect"` for CAPTCHA-protected social sign-in. Better Auth 1.7's experimental popup API does not accept `fetchOptions` or forward CAPTCHA headers. Popup failures reset the widget, but the popup flow cannot send the token.
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Add the [`captcha`](https://www.better-auth.com/docs/plugins/captcha) plugin to your Better Auth server config and pick a provider:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { captcha } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
captcha({ // [!code highlight]
provider: "cloudflare-turnstile", // or "hcaptcha", "captchafox", "google-recaptcha" // [!code highlight]
secretKey: process.env.TURNSTILE_SECRET_KEY as string // [!code highlight]
}) // [!code highlight]
]
})
```
By default, the Better Auth captcha plugin protects `/sign-up/email`, `/sign-in/email`, and `/request-password-reset`. The UI plugin shows the widget on these views.
Better Auth 1.7 and later matches complete authentication paths. Use an exact endpoint or an explicit wildcard such as `/sign-in/*`.
Do not use a partial prefix such as `/sign-in`. To protect username and social sign-in, add their endpoints explicitly:
```ts
captcha({
provider: "cloudflare-turnstile",
secretKey: process.env.TURNSTILE_SECRET_KEY as string,
endpoints: [ // [!code highlight]
"/sign-up/email", // [!code highlight]
"/sign-in/email", // [!code highlight]
"/sign-in/username", // [!code highlight]
"/sign-in/social", // [!code highlight]
"/request-password-reset" // [!code highlight]
] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Pass `captchaPlugin({ render })` to ``. `render` is a component that receives `setToken`, `clearToken`, and `setReset` and is responsible for mounting your provider's React widget.
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { captchaPlugin } from "@better-auth-ui/react/plugins/captcha" // [!code highlight]
import { TurnstileWidget } from "@/components/turnstile-widget" // [!code highlight]
{children}
```
The `render` component is mounted as a real React component, so hooks like `useTheme` work inside it. See the [Providers](#providers) section below for ready-to-use widgets.
## Providers [#providers]
### Cloudflare Turnstile [#cloudflare-turnstile]
npm
pnpm
yarn
bun
```bash
npm install @marsidev/react-turnstile
```
```bash
pnpm add @marsidev/react-turnstile
```
```bash
yarn add @marsidev/react-turnstile
```
```bash
bun add @marsidev/react-turnstile
```
```tsx title="components/turnstile-widget.tsx"
import type { CaptchaRenderProps } from "@better-auth-ui/react/plugins/captcha"
import { type TurnstileInstance, Turnstile } from "@marsidev/react-turnstile"
import { useEffect, useRef } from "react"
export function TurnstileWidget({
setToken,
clearToken,
setReset
}: CaptchaRenderProps) {
const ref = useRef(null)
useEffect(() => {
setReset(() => ref.current?.reset())
return () => setReset(null)
}, [setReset])
return (
)
}
```
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { captchaPlugin } from "@better-auth-ui/react/plugins/captcha"
import { TurnstileWidget } from "@/components/turnstile-widget"
{children}
```
### hCaptcha [#hcaptcha]
npm
pnpm
yarn
bun
```bash
npm install @hcaptcha/react-hcaptcha
```
```bash
pnpm add @hcaptcha/react-hcaptcha
```
```bash
yarn add @hcaptcha/react-hcaptcha
```
```bash
bun add @hcaptcha/react-hcaptcha
```
```tsx title="components/hcaptcha-widget.tsx"
import type { CaptchaRenderProps } from "@better-auth-ui/react/plugins/captcha"
import HCaptcha from "@hcaptcha/react-hcaptcha"
import { useTheme } from "next-themes"
import { useEffect, useRef } from "react"
export function HCaptchaWidget({
setToken,
clearToken,
setReset
}: CaptchaRenderProps) {
const { resolvedTheme } = useTheme()
const ref = useRef(null)
useEffect(() => {
setReset(() => ref.current?.resetCaptcha())
return () => setReset(null)
}, [setReset])
return (
)
}
```
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { captchaPlugin } from "@better-auth-ui/react/plugins/captcha"
import { HCaptchaWidget } from "@/components/hcaptcha-widget"
{children}
```
### CaptchaFox [#captchafox]
npm
pnpm
yarn
bun
```bash
npm install @captchafox/react
```
```bash
pnpm add @captchafox/react
```
```bash
yarn add @captchafox/react
```
```bash
bun add @captchafox/react
```
```tsx title="components/captchafox-widget.tsx"
import type { CaptchaRenderProps } from "@better-auth-ui/react/plugins/captcha"
import { CaptchaFox, type CaptchaFoxInstance } from "@captchafox/react"
import { useTheme } from "next-themes"
import { useEffect, useRef } from "react"
export function CaptchaFoxWidget({
setToken,
clearToken,
setReset
}: CaptchaRenderProps) {
const { resolvedTheme } = useTheme()
const ref = useRef(null)
useEffect(() => {
setReset(() => ref.current?.reset())
return () => setReset(null)
}, [setReset])
return (
)
}
```
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { captchaPlugin } from "@better-auth-ui/react/plugins/captcha"
import { CaptchaFoxWidget } from "@/components/captchafox-widget"
{children}
```
## Options [#options]
## Render props [#render-props]
The `render` component receives:
* Connect the provider's success callback to `setToken`. It adds the `x-captcha-response` header to the next Better Auth request.
* Connect the error and expiration callbacks to `clearToken`. It removes the header before the application sends a stale token.
* Connect the widget's `reset()` function to `setReset`. Better Auth consumes the token through `/siteverify` before the authentication handler completes.
A rejected request still consumes the token. Each protected form calls the registered `reset()` function from `onError` and clears the old token.
The plugin also clears the header when the component unmounts. The application does not need additional cleanup.
# Dash (/docs/heroui/plugins/dash)
The Dash integration adds Activity tabs to personal settings, organization settings, and the Admin user inspector. It reads audit logs through the public `dashClient()` API from `@better-auth/infra`.
Organization owners and admins see organization-wide activity. Other members see only their own activity in that organization. Every organization query uses the organization ID from the current route.
## Setup [#setup]
### Configure Dash on the server [#configure-dash-on-the-server]
Install `@better-auth/infra`, then add `dash()` to Better Auth. Dash records supported authentication and organization events automatically.
```ts title="lib/auth.ts"
import { dash } from "@better-auth/infra"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
plugins: [
dash({
apiUrl: process.env.BETTER_AUTH_API_URL,
kvUrl: process.env.BETTER_AUTH_KV_URL,
apiKey: process.env.BETTER_AUTH_API_KEY
})
]
})
```
See the [Dash plugin guide](https://better-auth.com/docs/infrastructure/plugins/dash) for infrastructure setup and available events.
### Add the client plugin [#add-the-client-plugin]
```ts title="lib/auth-client.ts"
import { dashClient } from "@better-auth/infra/client"
import { createAuthClient } from "better-auth/client"
import { organizationClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [organizationClient(), dashClient()]
})
```
The organization client is required only when you enable organization activity.
### Register the UI plugin [#register-the-ui-plugin]
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { dashPlugin } from "@better-auth-ui/heroui/plugins/dash"
import { organizationPlugin } from "@better-auth-ui/heroui/plugins/organization"
{children}
```
Both personal and organization activity are enabled by default.
## Static routes [#static-routes]
The plugin adds one static path named `activity`. Its default segment is `activity`, which produces routes such as `/settings/activity` and the matching organization activity path.
It does not use query parameters, nested plugin routes, or a catch-all route. Add the segment to both static path lists when the application validates or generates settings and organization routes.
```ts title="route-paths.ts"
import { viewPaths } from "@better-auth-ui/core"
import { dashPlugin } from "@better-auth-ui/heroui/plugins/dash"
import { organizationPlugin } from "@better-auth-ui/heroui/plugins/organization"
const activityPath = dashPlugin({ path: "activity" }).viewPaths.settings.activity
export const validSettingsPaths = [
...Object.values(viewPaths.settings),
...Object.values(organizationPlugin().viewPaths.settings ?? {}),
activityPath
]
export const validOrganizationPaths = [
...Object.values(organizationPlugin().viewPaths.organization ?? {}),
activityPath
]
```
Use the same Dash options in the provider and route configuration when you customize the segment.
## Admin user activity [#admin-user-activity]
If the Admin integration is present, Dash adds an Activity tab to its user inspector. The tab calls `getAllAuditLogs({ userId })` for the selected user.
Dash authorizes this endpoint for organization owners and admins. A Better Auth application-admin role does not grant Dash access by itself.
Set `admin: false` on `dashPlugin()` to remove this inspector tab.
## Access and privacy [#access-and-privacy]
* Personal settings call `getAuditLogs` for the signed-in user.
* Organization settings check the member role for the explicit organization ID.
* Owners and admins call `getAllAuditLogs` for that organization.
* Other members call `getAuditLogs` with the organization filter.
* IP addresses are hidden by default. Set `showIpAddress: true` only when your privacy policy permits it.
The empty state says that no retained activity matches the view. It does not claim that an event never occurred.
## Components and hooks [#components-and-hooks]
```tsx
import {
AdminUserActivity,
OrganizationActivity,
UserActivity
} from "@better-auth-ui/heroui/plugins/dash"
import {
useDashAllAuditLogs,
useDashAuditLogs,
useDashUserAuditLogs
} from "@better-auth-ui/react/plugins/dash"
```
The core package also exports query option factories and `ensure`, `prefetch`, and `fetch` helpers from `@better-auth-ui/core/plugins/dash`.
## Options [#options]
## Localization [#localization]
# Delete User (/docs/heroui/plugins/delete-user)
The delete-user plugin renders the UI for Better Auth's built-in [account deletion](https://www.better-auth.com/docs/concepts/users-accounts#delete-user) feature. Users can permanently delete their account with a confirmation dialog via the `` card, wrapped by `` in security settings.
## Setup [#setup]
### Enable account deletion in Better Auth [#enable-account-deletion-in-better-auth]
[Better Auth core includes account deletion](https://www.better-auth.com/docs/concepts/users-accounts#delete-user). You do not need another plugin.
Enable the feature by setting `user.deleteUser.enabled` to `true`:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
// ...
user: {
deleteUser: { // [!code highlight]
enabled: true // [!code highlight]
} // [!code highlight]
}
})
```
After you enable the feature, the client provides `authClient.deleteUser()`. You do not need a client plugin.
For an OAuth user without a password, provide `sendDeleteAccountVerification`. This callback lets the user confirm deletion by email.
### Register the UI plugin [#register-the-ui-plugin]
Pass `deleteUserPlugin()` to ``:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { deleteUserPlugin } from "@better-auth-ui/heroui/plugins/delete-user" // [!code highlight]
{children}
```
## Components [#components]
### `` [#dangerzone-]
The `` card is automatically rendered in `` when the plugin is registered. It renders a danger zone heading and the `` card below it.
**Usage**
```tsx
import { DangerZone } from "@better-auth-ui/heroui/plugins/delete-user"
```
**Props**
### `` [#deleteaccount-]
The delete account card with confirmation dialog. Used inside `` by default. Import it directly if you need a custom layout.
When credential confirmation is required, the password starts masked and includes a localized show/hide control.
**Usage**
```tsx
import { DeleteAccount } from "@better-auth-ui/heroui/plugins/delete-user"
```
**Props**
## Options [#options]
## Localization [#localization]
# Device Authorization (/docs/heroui/plugins/device-authorization)
The device-authorization plugin adds the browser half of Better Auth's device authorization flow. A user enters the short code shown by a CLI, TV, or another limited-input device, signs in if needed, then approves or denies access.
It contributes:
* A `` view at `/auth/device`
* Code verification with the `user_code` query parameter prefilled when present
* Sign-in redirection that preserves the pending code
* Approve and deny confirmation states
* `useVerifyDeviceCode`, `useApproveDevice`, and `useDenyDevice` mutation hooks
The requesting device remains responsible for calling Better Auth's `/device/code` endpoint and polling `/device/token`.
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Add the [Device Authorization](https://www.better-auth.com/docs/plugins/device-authorization) plugin to your Better Auth server. Set `verificationUri` to the public route that renders the UI view:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { deviceAuthorization } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
deviceAuthorization({ // [!code highlight]
verificationUri: "/auth/device" // [!code highlight]
}) // [!code highlight]
]
})
```
Generate or migrate the Better Auth schema after enabling the server plugin. It adds the `deviceCode` model used to track pending requests.
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `deviceAuthorizationClient()` to your browser auth client:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { deviceAuthorizationClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [deviceAuthorizationClient()] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Pass `deviceAuthorizationPlugin()` to ``:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { deviceAuthorizationPlugin } from "@better-auth-ui/heroui/plugins" // [!code highlight]
{children}
```
### Allow the new auth path [#allow-the-new-auth-path]
Include the plugin's path in the route that renders ``. Keep this path aligned with the server's `verificationUri`:
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { deviceAuthorizationPlugin } from "@better-auth-ui/heroui/plugins"
import { createFileRoute, notFound } from "@tanstack/react-router"
const validAuthPathSegments = new Set([
...Object.values(viewPaths.auth),
deviceAuthorizationPlugin().viewPaths.auth.deviceAuthorization
])
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (!validAuthPathSegments.has(path)) {
throw notFound()
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return
}
```
## Components [#components]
### `` [#deviceauthorization-]
Enter any eight-character code in the preview to exercise the approval flow.
The component is rendered automatically at `/auth/device` when the plugin is registered. You can also use it directly:
```tsx
import { DeviceAuthorization } from "@better-auth-ui/heroui/plugins"
```
## Options [#options]
```ts
deviceAuthorizationPlugin({
// Override the URL segment. Default: "device"
path: "activate",
// Match Better Auth's server-side userCodeLength. Default: 8
userCodeLength: 8,
localization: {
approveDevice: "Allow this device?"
}
})
```
Keep `userCodeLength` equal to the value passed to Better Auth's server plugin. A mismatched length prevents valid codes from being submitted.
## Localization [#localization]
Read these values from `useAuthPlugin(deviceAuthorizationPlugin).localization` inside custom plugin views.
## Sessions and revocation [#sessions-and-revocation]
The device token creates an ordinary Better Auth session after approval. There is no separate device registry in the device-authorization plugin. Use [``](/docs/heroui/components/settings/security/active-sessions) to list sessions and revoke access for an approved device.
# Email OTP (/docs/heroui/plugins/email-otp)
The email-OTP plugin swaps emailed links for short codes the user types back into the app. Every flow is opt-in, so you can keep the link-based views you like and replace only the ones you do not.
It contributes:
* An `` sign-in view at `/auth/email-otp` plus a "Continue with Email Code" button
* Code-based replacements for the verify-email, forgot-password, reset-password, and change-email surfaces
* Mutation hooks for every email-OTP endpoint (`useSendVerificationOtp`, `useSignInEmailOtp`, `useVerifyEmailOtp`, `useRequestPasswordResetOtp`, `useResetPasswordOtp`, `useRequestEmailChangeOtp`, `useChangeEmailOtp`)
When `emailAndPassword.enabled === false`, `` takes over `/auth/sign-in` as the primary passwordless surface.
Email-OTP sign-in **replaces** the password, it does not add a step after it. If you want "password, then an emailed code", that is the [two-factor plugin](/docs/heroui/plugins/two-factor) with `otpOptions`. Better Auth does not apply 2FA to passwordless methods, so email-OTP sign-in bypasses a configured second factor.
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Add the [Email OTP](https://www.better-auth.com/docs/plugins/email-otp) plugin to your server config and wire `sendVerificationOTP` to your email provider. One callback serves all four flows: `type` tells you which one:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { emailOTP } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
emailOTP({ // [!code highlight]
disableSignUp: true, // [!code highlight]
sendVerificationOTP: async ({ email, otp, type }) => { // [!code highlight]
// Send `otp` to `email`. `type` is "sign-in", "email-verification", // [!code highlight]
// "forget-password", or "change-email". // [!code highlight]
} // [!code highlight]
}) // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { emailOTPClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [emailOTPClient()] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Pass `emailOtpPlugin()` to `` and turn on the flows you want:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { emailOtpPlugin } from "@better-auth-ui/heroui/plugins" // [!code highlight]
{children}
```
### Allow the new view path [#allow-the-new-view-path]
The plugin contributes an `email-otp` segment to `viewPaths.auth`. Spread `emailOtpPlugin().viewPaths?.auth` into your auth route's allowed-paths set:
TanStack Start
Next.js
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { emailOtpPlugin } from "@better-auth-ui/heroui/plugins" // [!code highlight]
import { createFileRoute, notFound } from "@tanstack/react-router"
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (
!Object.values({
...viewPaths.auth,
...emailOtpPlugin().viewPaths?.auth // [!code highlight]
}).includes(path)
) {
throw notFound()
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return
}
```
```tsx title="app/auth/[path]/page.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { emailOtpPlugin } from "@better-auth-ui/heroui/plugins" // [!code highlight]
import { notFound } from "next/navigation"
export default async function AuthPage({
params
}: {
params: Promise<{ path: string }>
}) {
const { path } = await params
if (
!Object.values({
...viewPaths.auth,
...emailOtpPlugin().viewPaths?.auth // [!code highlight]
}).includes(path)
) {
notFound()
}
return
}
```
## Choosing which flows use codes [#choosing-which-flows-use-codes]
Each option replaces one link-based surface. Turn a flow on in the UI only when the matching server option is set, otherwise the user waits for a code that never arrives.
| Option | Replaces | Server option it needs |
| ------------------------- | -------------------------------------------- | ------------------------------------------- |
| `signIn` (default `true`) | adds `/auth/email-otp` | none |
| `emailVerification` | `` | `overrideDefaultEmailVerification: true` |
| `passwordReset` | `` and `` | none |
| `changeEmail` | the change-email card in account settings | `changeEmail: { enabled: true }` |
| `verifyCurrentEmail` | adds a step to the change-email flow | `changeEmail: { verifyCurrentEmail: true }` |
```tsx
emailOtpPlugin({
// Keep the emailed sign-in link, use codes for everything else.
signIn: false,
emailVerification: true,
passwordReset: true,
changeEmail: true
})
```
## Sign-up and account creation [#sign-up-and-account-creation]
Better Auth creates an account for any address that completes an email-OTP sign-in, unless you set `disableSignUp: true` on the server. The UI mirrors that with `disableSignUp` defaulting to `true`.
Collecting a name only for unregistered addresses reveals which addresses already have accounts. This creates an account-enumeration risk.
Keep sign-up on the password or magic-link path. Alternatively, build a flow that asks every user for the same fields.
## Components [#components]
### `` [#emailotp-]
The form has two states. First, enter an email. Then enter the code:
```tsx
import { EmailOtp } from "@better-auth-ui/heroui/plugins"
```
### `` [#verifyemailotp-]
Rendered at `/auth/verify-email` when `emailVerification` is on. Reads the pending address from session storage (sign-up and sign-in put it there) and asks for it when it is missing.
```tsx
import { VerifyEmailOtp } from "@better-auth-ui/heroui/plugins"
```
### `` and `` [#forgotpasswordotp--and-resetpasswordotp-]
With `passwordReset` on, `/auth/forgot-password` emails a code and sends the user straight to `/auth/reset-password`, which takes the code and the new password together. The reset-link-sent view is skipped.
```tsx
import {
ForgotPasswordOtp,
ResetPasswordOtp
} from "@better-auth-ui/heroui/plugins"
```
### `` [#changeemailotp-]
With `changeEmail` on, this replaces the built-in change-email card inside ``: no extra wiring needed. With `verifyCurrentEmail` it becomes a three-step flow: confirm the current address, then the new one.
```tsx
import { ChangeEmailOtp } from "@better-auth-ui/heroui/plugins"
```
## Options [#options]
```ts
emailOtpPlugin({
// Override the URL segment. Default: "email-otp"
path: "code",
// Match the server's `otpLength`. Default: 6
otpLength: 6,
// Override any of the plugin's localization strings.
localization: {
sendCode: "Email me a code"
}
})
```
## Localization [#localization]
Read these from `useAuthPlugin(emailOtpPlugin).localization` inside custom slot components.
## Email template [#email-template]
Pair the plugin with the [``](/docs/heroui/components/email/otp-email) component to send a styled code from your `sendVerificationOTP` callback.
# Last Login Method (/docs/heroui/plugins/last-login-method)
The last-login-method integration floats a compact "Last" indicator over the matching username, email, or social sign-in control. It reads the method after hydration, so server-rendered auth pages do not produce a hydration mismatch.
## Setup [#setup]
### Add the Better Auth server plugin [#add-the-better-auth-server-plugin]
Add Better Auth's [Last Login Method](https://www.better-auth.com/docs/plugins/last-login-method) plugin to your server configuration:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
lastLoginMethod() // [!code highlight]
]
})
```
### Add the matching client plugin [#add-the-matching-client-plugin]
Add `lastLoginMethodClient()` to the client passed to ``:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { lastLoginMethodClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [
lastLoginMethodClient() // [!code highlight]
]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Register `lastLoginMethodPlugin()` with the HeroUI auth provider:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { lastLoginMethodPlugin } from "@better-auth-ui/heroui/plugins" // [!code highlight]
{children}
```
The sign-in view now marks the username or email control, or the matching social provider, when Better Auth has stored a previous method. Sign-up controls do not show the indicator.
## Localization [#localization]
Override the full and compact labels through the UI plugin:
```tsx
lastLoginMethodPlugin({
localization: {
lastUsed: "Previously used",
lastUsedShort: "Previous"
}
})
```
## Custom methods [#custom-methods]
Better Auth tracks email and social providers by default. If `customResolveMethod` stores another method, place `` beside its sign-in control:
```tsx
import { LastUsedBadge } from "@better-auth-ui/heroui/plugins"
```
Pass an array when one control represents more than one stored method:
```tsx
```
## Cookie consent [#cookie-consent]
Whether the plugin's browser-readable cookie is non-essential and requires consent depends on your jurisdiction and how your application uses it. Consult qualified legal counsel for guidance. When consent is required, configure Better Auth's `beforeStoreCookie` option to return a stored user-consent flag or an equivalent condition. Authentication still works when the hook returns `false`.
# Magic Link (/docs/heroui/plugins/magic-link)
The magic-link plugin adds a passwordless email sign-in flow. The user enters their email, receives a one-time link, and is signed in when they click it.
It contributes:
* A `` view at `/auth/magic-link`
* A `` confirmation view at `/auth/magic-link-sent`
* A "Continue with Magic Link" button rendered alongside the password sign-in button
* A `useSignInMagicLink` mutation hook
When `emailAndPassword.enabled === false`, `` automatically takes over `/auth/sign-in` as the primary passwordless surface.
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Add the [Magic Link](https://www.better-auth.com/docs/plugins/magic-link) plugin to your Better Auth server config and wire up `sendMagicLink` to your email provider:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { magicLink } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
magicLink({ // [!code highlight]
sendMagicLink: async ({ email, url }) => { // [!code highlight]
// Send `url` to `email` via your email provider. // [!code highlight]
} // [!code highlight]
}) // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `magicLinkClient()` to your auth client so `authClient.signIn.magicLink` is available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { magicLinkClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [magicLinkClient()] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Pass `magicLinkPlugin()` to ``:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { magicLinkPlugin } from "@better-auth-ui/heroui/plugins/magic-link" // [!code highlight]
{children}
```
### Allow the new view path [#allow-the-new-view-path]
The plugin contributes `magic-link` and `magic-link-sent` segments to `viewPaths.auth`. Spread `magicLinkPlugin().viewPaths?.auth` into your auth route's allowed-paths set so both views resolve correctly:
TanStack Start
Next.js
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { magicLinkPlugin } from "@better-auth-ui/heroui/plugins/magic-link" // [!code highlight]
import { createFileRoute, notFound } from "@tanstack/react-router"
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (
!Object.values({
...viewPaths.auth,
...magicLinkPlugin().viewPaths?.auth // [!code highlight]
}).includes(path)
) {
throw notFound()
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return
}
```
```tsx title="app/auth/[path]/page.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { magicLinkPlugin } from "@better-auth-ui/heroui/plugins/magic-link" // [!code highlight]
import { notFound } from "next/navigation"
export default async function AuthPage({
params
}: {
params: Promise<{ path: string }>
}) {
const { path } = await params
if (
!Object.values({
...viewPaths.auth,
...magicLinkPlugin().viewPaths?.auth // [!code highlight]
}).includes(path)
) {
notFound()
}
return
}
```
## Components [#components]
### `` [#magiclink-]
The `` view is automatically rendered at `/auth/magic-link` when the plugin is registered.
**Usage**
```tsx
import { MagicLink } from "@better-auth-ui/heroui/plugins/magic-link"
```
**Props**
### `` [#magiclinksent-]
After a magic-link request succeeds, the form stores the submitted email in session storage and navigates to this confirmation view. It shows an email-provider shortcut when one is available. Hover or focus the button to show a QR code for opening the same provider URL on another device.
```tsx
import { MagicLinkSent } from "@better-auth-ui/heroui/plugins"
```
## Options [#options]
```ts
magicLinkPlugin({
// Override the URL segment. Default: "magic-link"
path: "email-link",
// Override the confirmation segment. Default: "magic-link-sent"
sentPath: "email-link-sent",
// Override any of the plugin's localization strings.
localization: {
sendMagicLink: "Email me a link"
}
})
```
## Localization [#localization]
Read these from `useAuthPlugin(magicLinkPlugin).localization` inside custom slot components.
## Email template [#email-template]
Pair the plugin with the [``](/docs/heroui/components/email/magic-link-email) component to send a styled email from your `sendMagicLink` callback.
## Passwordless-only flows [#passwordless-only-flows]
If you disable email and password authentication, the magic-link form becomes the primary sign-in view. No additional configuration is required:
```tsx
{children}
```
`/auth/sign-in` now renders ``, and the `signUp`, `forgotPassword`, `resetPassword`, and `resetLinkSent` routes redirect to it.
# Multi Session (/docs/heroui/plugins/multi-session)
The multi-session plugin enables users to maintain multiple active sessions simultaneously. Users can switch between accounts without signing out, manage all their device sessions, and quickly add new accounts from the user menu.
It contributes:
* A "Switch Account" submenu in the user button dropdown showing all active device sessions
* A `` card in account settings for viewing and revoking device sessions
* `useListDeviceSessions`, `useSetActiveSession`, and `useRevokeMultiSession` hooks
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Add the [Multi Session](https://www.better-auth.com/docs/plugins/multi-session) plugin to your Better Auth server config:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { multiSession } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
multiSession() // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `multiSessionClient()` to your auth client so `authClient.multiSession.*` methods are available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { multiSessionClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [multiSessionClient()] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Pass `multiSessionPlugin()` to ``:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { multiSessionPlugin } from "@better-auth-ui/heroui/plugins/multi-session" // [!code highlight]
{children}
```
## Components [#components]
### `` [#userbutton-]
The plugin adds a "Switch Account" submenu to ``. It shows all active device sessions.
Users can switch accounts, add an account, and identify the current account.
**Usage**
```tsx
import { SwitchAccountSubmenu } from "@better-auth-ui/heroui/plugins/multi-session"
```
**Props**
### `` [#manageaccounts-]
The `` card is automatically rendered in account settings when the plugin is registered.
**Usage**
```tsx
import { ManageAccounts } from "@better-auth-ui/heroui/plugins/multi-session"
```
**Props**
## Options [#options]
```ts
multiSessionPlugin({
// Override any of the plugin's localization strings.
localization: {
switchAccount: "Switch Account",
addAccount: "Add Account",
manageAccounts: "Manage Accounts"
}
})
```
## Localization [#localization]
## Session management [#session-management]
The plugin provides hooks for managing multiple sessions:
* `useListDeviceSessions` - List all device sessions for the current user
* `useSetActiveSession` - Switch to a different session
* `useRevokeMultiSession` - Sign out from a specific session
# OAuth Provider (/docs/heroui/plugins/oauth-provider)
The OAuth Provider plugin covers the user-facing screens Better Auth's [OAuth 2.1 Provider](https://better-auth.com/docs/plugins/oauth-provider) redirects to. It also provides connected application security settings and OAuth client developer settings.
## Client management [#client-management]
Enable personal client management to add an OAuth clients tab to user settings. The tab lists, creates, edits, deletes, and rotates secrets through Better Auth's signed-in client endpoints.
```tsx
oauthProviderPlugin({
clientManagement: true
})
```
The client secret appears only after creation or rotation. The user must copy it before closing the dialog.
Pass `clientManager` when personal clients also need server-only operations such as enable or disable. This manager uses `{ type: "user" }` as its owner. The UI shows the enable or disable control when the manager implements `setDisabled`.
Organization clients need an application-owned `OAuthClientManager`. Every operation receives both the organization ID and slug. Authorize both values in your server endpoint. Do not infer the organization from active session state.
```tsx
oauthProviderPlugin({
organizationClientManager: {
list: (owner, signal) => api.oauthClients.list(owner, signal),
create: (owner, input) => api.oauthClients.create(owner, input),
update: (owner, clientId, update) =>
api.oauthClients.update(owner, clientId, update),
delete: (owner, clientId) => api.oauthClients.delete(owner, clientId),
rotateSecret: (owner, clientId) =>
api.oauthClients.rotateSecret(owner, clientId),
setDisabled: (owner, clientId, disabled) =>
api.oauthClients.setDisabled(owner, clientId, disabled)
}
})
```
Better Auth 1.7 exposes enable or disable through server admin APIs, so BAUI does not call it from the default browser adapter.
It contributes:
* An `` view at `/auth/oauth-consent`
* An `` view at `/auth/oauth-sign-up`, for `prompt=create`
* An `` view at `/auth/select-account`, for `prompt=select_account`
* An `` card in security settings
* Public OAuth client metadata loading
* Scope labels as a map, a list, or a resolver
* Headless continuation through `useOAuthContinue`, for your own post-login screens
## How the redirect screens fit together [#how-the-redirect-screens-fit-together]
Better Auth owns the authorization request. When it needs user input, it redirects to one of your pages.
The redirect includes the signed authorization query. Call `oauth2.continue` after the user provides the input:
| Prompt | Page | Continuation |
| ---------------- | -------------------- | -------------------------------------- |
| `consent` | `consentPage` | `oauth2.consent({ accept })` |
| `create` | `signup.page` | `oauth2.continue({ created: true })` |
| `select_account` | `selectAccount.page` | `oauth2.continue({ selected: true })` |
| None | `postLogin.page` | `oauth2.continue({ postLogin: true })` |
Keep the query string on every one of those pages. Do not strip it, rebuild it from `redirect_uri`, or navigate to the requested redirect yourself. `oauthProviderClient()` forwards the signed query to Better Auth, and Better Auth validates it and completes the redirect.
## Setup [#setup]
### Configure the Better Auth server [#configure-the-better-auth-server]
Install the provider package:
```bash
bun add @better-auth/oauth-provider
```
Add the JWT and OAuth Provider plugins, and point each page option at the route that renders the matching view:
```ts title="lib/auth.ts"
import { oauthProvider } from "@better-auth/oauth-provider"
import { betterAuth } from "better-auth"
import { jwt, multiSession } from "better-auth/plugins"
export const auth = betterAuth({
disabledPaths: ["/token"],
plugins: [
jwt(),
multiSession(),
oauthProvider({
loginPage: "/auth/sign-in",
consentPage: "/auth/oauth-consent",
signup: {
page: "/auth/oauth-sign-up"
},
selectAccount: {
page: "/auth/select-account",
shouldRedirect: async () => true
}
})
]
})
```
`signup` and `selectAccount` both use `loginPage` by default. Set each `page` explicitly.
Each page uses a plugin route and does not replace `/auth/sign-up`. `selectAccount.shouldRedirect` controls when the application shows the chooser.
Return `true` to always show the chooser. Otherwise, use the session and scopes to make the decision.
Generate or migrate your Better Auth schema after enabling the server plugin:
```bash
bunx auth@latest migrate
```
### Configure the browser client [#configure-the-browser-client]
Add `oauthProviderClient()` to the auth client. It preserves Better Auth's signed authorization query when the user responds:
```ts title="lib/auth-client.ts"
import { oauthProviderClient } from "@better-auth/oauth-provider/client"
import { multiSessionClient } from "better-auth/client/plugins"
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
plugins: [oauthProviderClient(), multiSessionClient()]
})
```
`multiSessionClient()` is what makes the account chooser work: it lists the device sessions and switches the active one. Skip it if you do not use `prompt=select_account`.
### Register the UI plugin [#register-the-ui-plugin]
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { oauthProviderPlugin } from "@better-auth-ui/heroui/plugins"
{children}
```
### Allow the OAuth routes [#allow-the-oauth-routes]
Include the plugin paths in the route that renders ``:
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { oauthProviderPlugin } from "@better-auth-ui/heroui/plugins"
import { createFileRoute, notFound } from "@tanstack/react-router"
const validAuthPathSegments = new Set([
...Object.values(viewPaths.auth),
...Object.values(oauthProviderPlugin().viewPaths.auth)
])
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (!validAuthPathSegments.has(path)) {
throw notFound()
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return
}
```
Keep these paths aligned with the server's `consentPage`, `signup.page`, and `selectAccount.page`.
## Scope metadata [#scope-metadata]
`scopeMetadata` accepts three shapes. Every requested scope remains visible.
If a scope has no match, the plugin uses built-in metadata. The raw scope value is the final fallback.
### Map [#map]
The original form. Good when the scope set is known up front:
```tsx title="components/providers.tsx"
oauthProviderPlugin({
scopeMetadata: {
calendar: {
label: "View your calendar",
description: "Read your calendar events and availability."
}
}
})
```
### List [#list]
Convenient when metadata comes out of a database or an API and arrives as an array:
```tsx title="components/providers.tsx"
oauthProviderPlugin({
scopeMetadata: [
{ scope: "calendar", label: "View your calendar" },
{ scope: "files", label: "View your files" }
]
})
```
### Resolver [#resolver]
For labels that depend on the requesting client or the rest of the scope set:
```tsx title="components/providers.tsx"
oauthProviderPlugin({
scopeMetadata: (scope, { clientId, requestedScopes }) => {
if (scope.startsWith("project:")) {
return { label: `Access ${scope.slice("project:".length)}` }
}
if (scope === "admin" && requestedScopes.includes("offline_access")) {
return {
label: "Administer your workspace",
description: `${clientId} can act on your behalf indefinitely.`
}
}
// Fall back to the built-in or raw label.
return undefined
}
})
```
Returning `undefined` means "use the fallback", not "hide this scope".
Resolvers stay synchronous, so rendering is deterministic and behaves the same under SSR. If you need remote metadata, load it before render and pass a map or a list.
## Sign-up continuation [#sign-up-continuation]
`` lives at its own route and wraps the same `` component your app already uses: it does not replace the built-in sign-up view. Users who never go through OAuth never touch it.
When Better Auth redirects there with `prompt=create`, the ordinary sign-up implementation creates the account, and only after that succeeds does the view call:
```ts
authClient.oauth2.continue({ created: true })
```
If the continuation request fails, the form shows a retry action. The account already exists, so do not submit the sign-up form again.
Reached without `prompt=create`, it renders plain sign-up and redirects the way sign-up normally does.
### Limitations [#limitations]
Two flows deliberately do not continue on their own:
* **Email verification.** When `requireEmailVerification` is on, sign-up has no usable session yet, so the view sends the user to the verify-email screen instead. Resume after verification yourself with [`useOAuthContinue`](/docs/react/mutations/oauth-continue).
* **Social sign-up.** The provider redirect leaves and re-enters your app, so there is no "sign-up just succeeded" moment to hook into. Resume from your social callback route instead.
In both cases only call `{ created: true }` if the account really was created during this flow. An already signed-in user is not a newly created one.
## Account selection [#account-selection]
`` renders the device sessions from `multiSession` in a HeroUI `ListBox`. Choosing the account that is already active continues directly. Choosing a different one calls `multiSession.setActive()` first, then continues: the switch always lands before Better Auth resumes.
Sessions are compared by session ID, never by user ID or list position.
The chooser has no sign-out or revoke actions on purpose. Session management belongs in security settings, not in the middle of an authorization request.
## Post-login selection [#post-login-selection]
There is no post-login view to install. An application can select a workspace, tenant, team, project, role, or another resource.
Build this selection interface in the application. When the selection is complete, use the headless continuation helper:
```tsx title="routes/auth/select-workspace.tsx"
import { useAuth } from "@better-auth-ui/react"
import { useOAuthContinue } from "@better-auth-ui/react/plugins/oauth-provider"
type Workspace = { slug: string; name: string }
function SelectWorkspace({ workspaces }: { workspaces: Workspace[] }) {
const { authClient } = useAuth()
const oauthContinue = useOAuthContinue(authClient)
const select = async (slug: string) => {
// Persist the selection the way your app normally does — by slug or ID.
await setActiveWorkspaceSlug(slug)
await oauthContinue.mutateAsync({ postLogin: true })
}
return workspaces.map((workspace) => (
))
}
```
Point the server's `postLogin.page` at that route.
Do not use Better Auth active organizations here. Persist the selection with your own slug- or ID-based mechanism.
## Connected applications [#connected-applications]
`` is a security card for authorized applications. It shows the client name, logo, granted scopes, and latest authorization date.
The card provides a "Remove authorization" action that uses a HeroUI `AlertDialog` for confirmation.
Better Auth can store several consent records for one client, so records are grouped by client ID and rendered as a single application. Removing an application deletes every consent ID in that group. Each row loads its own client metadata, so one slow or missing application never blocks the rest of the card.
Turn the card off with:
```tsx
oauthProviderPlugin({ showConnectedApplications: false })
```
Removing an authorization deletes the stored consent record. The application needs the user's approval before it receives new access.
Existing access and refresh tokens stay valid until they expire. Better Auth does not provide complete token revocation through this endpoint.
Do not tell users that this action revokes existing tokens.
The card manages consent records only. It is not a session list or token list.
The card has no revoke-all control because Better Auth does not provide the required token operations.
## Consent behavior [#consent-behavior]
The consent view accepts or denies the complete requested scope set. It does not render per-scope controls. Omitting `scope` from the consent mutation tells Better Auth to accept the scopes from the original signed request.
The public client endpoint requires a signed-in session. Direct visits with missing request data, no session, or an unknown client render an invalid-request state.
Login reuses the existing `signIn` view and resumes automatically when Better Auth creates the session.
## Components [#components]
The plugin renders each view automatically at its configured path. You can also render them directly:
```tsx
import {
AuthorizedApplications,
OAuthConsent,
OAuthSelectAccount,
OAuthSignUp
} from "@better-auth-ui/heroui/plugins"
```
## Plugin options [#plugin-options]
## React APIs [#react-apis]
* [`usePublicOAuthClient`](/docs/react/queries/public-oauth-client) loads application metadata
* [`useOAuthConsent`](/docs/react/mutations/oauth-consent) submits the user's decision
* [`useOAuthContinue`](/docs/react/mutations/oauth-continue) resumes the request after a redirect screen
* [`useListOAuthConsents`](/docs/react/queries/list-oauth-consents) lists authorized applications
* [`useDeleteOAuthConsent`](/docs/react/mutations/delete-oauth-consent) removes a stored consent
Each hook also exports its TanStack Query options factory.
# One Tap (/docs/heroui/plugins/one-tap)
The One Tap UI plugin opens Better Auth's native One Tap flow when an authentication view mounts. It refreshes the session after success, follows the configured `redirectTo`, and continues into the two-factor view when the server requests a second factor.
Keep Google in `socialProviders` as a visible fallback. One Tap is a passive prompt, and browsers can decide not to show it.
## Setup [#setup]
### Configure the Better Auth server plugin [#configure-the-better-auth-server-plugin]
Add Better Auth's [One Tap](https://www.better-auth.com/docs/plugins/one-tap) plugin with the OAuth client ID from your Google Cloud project:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { oneTap } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
oneTap({
clientId: process.env.GOOGLE_CLIENT_ID as string
}) // [!code highlight]
]
})
```
### Configure the Better Auth client plugin [#configure-the-better-auth-client-plugin]
Use the same client ID in the browser client:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { oneTapClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [
oneTapClient({
clientId: import.meta.env.VITE_GOOGLE_CLIENT_ID,
promptOptions: {
baseDelay: 1_000,
maxAttempts: 3
}
}) // [!code highlight]
]
})
```
### Register the Better Auth UI plugin [#register-the-better-auth-ui-plugin]
Pass `oneTapPlugin()` to ``. The prompt opens on sign-in by default.
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { oneTapPlugin } from "@better-auth-ui/react/plugins/one-tap" // [!code highlight]
{children}
```
### Add your authorized origins [#add-your-authorized-origins]
Add every application origin that can render the prompt to the OAuth client's **Authorized JavaScript origins** in Google Cloud. Include the exact protocol, host, and development port, such as `http://localhost:3000`.
## Show One Tap on sign-up [#show-one-tap-on-sign-up]
Pass both auth views when you also want the prompt on sign-up:
```tsx
oneTapPlugin({
views: ["signIn", "signUp"]
})
```
The plugin sends the matching `signin` or `signup` context to Better Auth. This preserves server-side sign-up controls and redirects.
## Prompt options [#prompt-options]
Better Auth's prompt settings can be passed directly to the UI plugin:
```tsx
oneTapPlugin({
autoSelect: true,
cancelOnTapOutside: false,
onPromptNotification: (notification) => {
// Track when Google skips or dismisses the prompt.
}
})
```
The integration supports the stricter One Tap responses in Better Auth 1.7. Errors such as `EMAIL_NOT_VERIFIED` are sent through the normal authentication error handler instead of being hidden.
## Last login method [#last-login-method]
Better Auth's last-login-method plugin does not classify the One Tap callback as Google by default. If you use its badge, resolve the callback explicitly:
```ts title="lib/auth.ts"
lastLoginMethod({
customResolveMethod: (context) =>
context.path === "/one-tap/callback" ? "google" : null
})
```
# Organization (/docs/heroui/plugins/organization)
The organization plugin adds multi-tenant organization management to your authentication UI. Users can create, switch between, and manage organizations with members, invitations, and roles.
It contributes:
* An `organizations` tab to `` listing every organization the user belongs to plus pending invitations to them
* An `` shell mounted at `/organization/` with `settings` and `people` tabs
* An `` dropdown to switch the active organization, manage it, or create a new one
* An `organizationCards` plugin slot rendered inside `` so other plugins (for example [api-key](/docs/heroui/plugins/api-key)) can attach org-scoped cards
* Hooks and mutations for organization endpoints, including `useActiveOrganization`, `useListOrganizations`, and `useInviteMember`
## Setup [#setup]
### Install the server plugin [#install-the-server-plugin]
Add the [`organization`](https://www.better-auth.com/docs/plugins/organization) plugin to your Better Auth server config:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { organization } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
organization() // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `organizationClient()` to your auth client so `authClient.organization.*` methods are available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { organizationClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [organizationClient()] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Pass `organizationPlugin()` to `` so the organizations settings tab, the `` shell, and `` can read plugin localization and view paths:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { organizationPlugin } from "@better-auth-ui/heroui/plugins/organization" // [!code highlight]
import { authClient } from "@/lib/auth-client"
{children}
```
### Allow the invitation auth path [#allow-the-invitation-auth-path]
The organization plugin registers a direct invitation view at `/auth/accept-invitation`. Include its auth path in any route allow-list that wraps ``:
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { organizationPlugin } from "@better-auth-ui/heroui/plugins"
const validAuthPaths = [
...Object.values(viewPaths.auth),
...Object.values(organizationPlugin().viewPaths.auth) // [!code highlight]
]
```
Invitation links use `/auth/accept-invitation?invitationId=`. Signed-out recipients return to the invitation after signing in, and successful accept or reject actions continue to the configured `redirectTo`.
### Mount the organization switcher [#mount-the-organization-switcher]
Add `` to the application shell, usually next to `` in the header.
It shows the selected organization and provides switch and create actions:
```tsx title="components/header.tsx" {2,7}
import { UserButton } from "@better-auth-ui/heroui"
import { OrganizationSwitcher } from "@better-auth-ui/heroui/plugins/organization"
export function Header() {
return (
)
}
```
### Allow the `organizations` settings path [#allow-the-organizations-settings-path]
The plugin contributes an `organizations` segment to `viewPaths.settings`. Spread `organizationPlugin().viewPaths.settings` into your settings route's allowed-path set so `/settings/organizations` resolves correctly:
TanStack Start
Next.js
```tsx title="routes/settings/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Settings } from "@better-auth-ui/heroui"
import { organizationPlugin } from "@better-auth-ui/heroui/plugins/organization" // [!code highlight]
import { ensureSession } from "@better-auth-ui/core"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { createFileRoute, notFound, redirect } from "@tanstack/react-router"
import { createIsomorphicFn } from "@tanstack/react-start"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
import { authClient } from "@/lib/auth-client"
const validSettingsPaths = [
...Object.values(viewPaths.settings),
...Object.values(organizationPlugin().viewPaths.settings) // [!code highlight]
]
export const Route = createFileRoute("/settings/$path")({
async beforeLoad({ params: { path }, context: { queryClient }, location }) {
if (!validSettingsPaths.includes(path)) {
throw notFound()
}
const ensureSessionIso = createIsomorphicFn()
.server(() =>
ensureSessionServer(queryClient, auth, { headers: getRequestHeaders() })
)
.client(() => ensureSession(queryClient, authClient))
const session = await ensureSessionIso()
if (!session) {
throw redirect({
to: "/auth/$path",
params: { path: "sign-in" },
search: { redirectTo: location.href }
})
}
return { session }
},
component: SettingsPage
})
function SettingsPage() {
const { path } = Route.useParams()
return (
)
}
```
`/organization/settings` and `/organization/people` now render the org management UI. Internal links from `` and from `` (in the settings tab) wire up automatically.
Users can now create organizations from `/settings/organizations`. They can use the header switcher and manage members or invitations at `/organization/`.
## Slug-based routes [#slug-based-routes]
By default, the switcher stores the selected organization in the session. Enable **slug-based routing** to select the organization from the URL.
For example, use `acme.your-app.com` or `your-app.com/organization/acme/settings`.
This unlocks:
* Shareable, bookmarkable per-org URLs that do not depend on session state
* Server-side prefetching of the correct organization without round-tripping `setActive`
* Letting members keep multiple organizations open in separate tabs without thrashing the active organization
You wire this up by passing the URL slug into `organizationPlugin({ slug })`. The UI plugin then:
1. Drives `useActiveOrganization()` to fetch the org matching that slug (instead of reading the session's active org)
2. Rewrites every link from ``, ``, and the `` tabs to include `//`
3. Swaps the switcher's behavior from `setActive` to `navigate`: clicking an organization takes the user to its slug-prefixed route
Set `slugPrefix` to include a marker in the visible route, such as `/organization/@acme/settings`. Keep `slug` itself unprefixed.
For example, `/organization/@{$slug}/$path` captures `acme`. The plugin adds the prefix to fields, labels, and generated links.
### Read the slug from the URL in your providers [#read-the-slug-from-the-url-in-your-providers]
Read the slug param wherever you render `` and forward it to `organizationPlugin`.
TanStack Start
Next.js
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { organizationPlugin } from "@better-auth-ui/heroui/plugins/organization"
import { useNavigate, useParams } from "@tanstack/react-router"
import type { ReactNode } from "react"
import { authClient } from "@/lib/auth-client"
export function Providers({ children }: { children: ReactNode }) {
const navigate = useNavigate()
const { slug } = useParams({ strict: false }) // [!code highlight]
return (
{children}
)
}
```
```tsx title="components/providers.tsx"
"use client"
import { AuthProvider } from "@better-auth-ui/heroui"
import { organizationPlugin } from "@better-auth-ui/heroui/plugins/organization"
import { useParams, useRouter } from "next/navigation"
import type { ReactNode } from "react"
import { authClient } from "@/lib/auth-client"
export function Providers({ children }: { children: ReactNode }) {
const router = useRouter()
const params = useParams<{ slug?: string | string[] }>() // [!code highlight]
const slug = typeof params?.slug === "string" ? params.slug : null // [!code highlight]
return (
replace ? router.replace(to) : router.push(to)
}
plugins={[organizationPlugin({ slug })]} // [!code highlight]
>
{children}
)
}
```
On a non-slug page, set `slug` to `null`. This value tells the plugin to use the user's personal account.
Do not leave `slug` as `undefined`. That value uses the organization from the session and can show the wrong organization.
### Add the slug-prefixed organization route [#add-the-slug-prefixed-organization-route]
Move `/organization/$path` → `/organization/$slug/$path`. Validate both segments and gate on session as before.
TanStack Start
Next.js
```tsx title="routes/organization/$slug/$path.tsx"
import {
Organization,
organizationPlugin
} from "@better-auth-ui/heroui/plugins/organization"
import { ensureSession } from "@better-auth-ui/core"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { createFileRoute, notFound, redirect } from "@tanstack/react-router"
import { createIsomorphicFn } from "@tanstack/react-start"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
import { authClient } from "@/lib/auth-client"
const validOrganizationPaths = Object.values(
organizationPlugin().viewPaths.organization
)
export const Route = createFileRoute("/organization/$slug/$path")({
async beforeLoad({ params: { path }, context: { queryClient }, location }) {
if (!validOrganizationPaths.includes(path)) {
throw notFound()
}
const ensureSessionIso = createIsomorphicFn()
.server(() =>
ensureSessionServer(queryClient, auth, { headers: getRequestHeaders() })
)
.client(() => ensureSession(queryClient, authClient))
const session = await ensureSessionIso()
if (!session) {
throw redirect({
to: "/auth/$path",
params: { path: "sign-in" },
search: { redirectTo: location.href }
})
}
return { session }
},
component: OrganizationPage
})
function OrganizationPage() {
const { path } = Route.useParams()
return (
)
}
```
After you configure `organizationPlugin({ slug })`, internal organization links include `//` automatically.
These links include the switcher, organization rows, and the tab bar in ``.
### Customize where the switcher navigates [#customize-where-the-switcher-navigates]
When slug-based routing is enabled, clicking an organization in `` navigates to `/organization//settings` by default, and clicking the personal account navigates to `/settings/account`.
To use a custom destination such as `//dashboard`, pass the `setActive` prop. This prop replaces the plugin's default navigation.
The callback receives the selected `Organization`, or `null` for the personal account:
TanStack Start
Next.js
```tsx title="components/header.tsx"
import { OrganizationSwitcher } from "@better-auth-ui/heroui/plugins/organization"
import { useNavigate } from "@tanstack/react-router"
export function Header() {
const navigate = useNavigate()
return (
{
navigate({
to: organization ? `/${organization.slug}/dashboard` : "/dashboard"
})
}}
/>
)
}
```
```tsx title="components/header.tsx"
"use client"
import { OrganizationSwitcher } from "@better-auth-ui/heroui/plugins/organization"
import { useRouter } from "next/navigation"
export function Header() {
const router = useRouter()
return (
{
router.push(
organization ? `/${organization.slug}/dashboard` : "/dashboard"
)
}}
/>
)
}
```
Without slug-based routing, the default behavior is `authClient.organization.setActive`. Pass `setActive` to intercept the selection and apply custom behavior.
## Hide organization slugs [#hide-organization-slugs]
Set `hideSlug: true` to hide slugs in creation dialogs, profile forms, organization views, and switchers:
```ts
organizationPlugin({ hideSlug: true })
```
Creation still generates a slug from the organization name. Names without Latin letters or digits use a generated identifier instead. The server still validates the slug and its availability. Profile edits preserve the existing slug.
Each component's `hideSlug` prop overrides the plugin configuration. Without either option, forms and organization views show slugs, while switchers hide them.
## Options [#options]
```ts
organizationPlugin({
// Display slugs as @acme and include the prefix in generated URLs.
slugPrefix: "@",
// Disable the slug-availability check that runs while typing a slug.
checkSlug: false,
// Override path segments (defaults shown).
viewPaths: {
auth: { acceptInvitation: "accept-invitation" },
settings: { organizations: "organizations" },
organization: { settings: "settings", people: "people" }
},
// Disable logo upload, or customize the resize / size.
logo: { enabled: false },
// Replace the default role labels.
roles: { owner: "Owner", admin: "Admin", member: "Member" },
// Add labels for custom server roles without redefining built-ins.
additionalRoles: { billing: "Billing" },
// Limit invitations and members to one role.
allowMultipleRoles: false
})
```
## Localization [#localization]
Read these inside custom slot components via `useAuthPlugin(organizationPlugin).localization`.
## React Hooks [#react-hooks]
### Queries [#queries]
* `useActiveOrganization()`: Full organization for the active session (or the URL slug when `organizationPlugin({ slug })` is set)
* `useListOrganizations()`: All organizations the signed-in user belongs to
* `useListOrganizationMembers()`: Members of the active organization
* `useListOrganizationInvitations()`: Pending invitations for the active organization
* `useListUserInvitations()`: Pending invitations addressed to the signed-in user
* `useHasPermission({ permissions })`: Check the current member's permission against the active organization
### Mutations [#mutations]
* `useCreateOrganization()`: Create a new organization
* `useUpdateOrganization()`: Update name / slug / logo of the active organization
* `useDeleteOrganization()`: Delete an organization
* `useSetActiveOrganization()`: Switch the active organization (server-side, persists on session)
* `useLeaveOrganization()`: Leave an organization
* `useInviteMember()`: Invite a member by email
* `useRemoveMember()`: Remove a member
* `useUpdateMemberRole()`: Update a member's role
* `useCancelInvitation()`: Cancel a pending invitation
* `useAcceptInvitation()`: Accept an invitation
* `useRejectInvitation()`: Reject an invitation
* `useCheckSlug()`: Check whether an organization slug is available
## Components [#components]
### `` [#acceptinvitation-]
Direct invitation acceptance view registered by `organizationPlugin()` at `/auth/accept-invitation`. It reads `invitationId` from the query string, preserves the link through sign-in, and lets the recipient accept or reject the pending invitation.
### `` [#organizationswitcher-]
Dropdown that shows the active organization, lets the user switch between organizations, and exposes a "Create organization" entry. Drop it in your app shell: typically next to ``.
```tsx file=/src/demos/heroui/organization/organization-switcher.tsx
import { AuthProvider } from "@better-auth-ui/heroui"
import {
OrganizationSwitcher,
organizationPlugin
} from "@better-auth-ui/heroui/plugins/organization"
import { authClient } from "@/lib/auth-client"
export function OrganizationSwitcherDemo() {
return (
{}}
plugins={[organizationPlugin()]}
>
)
}
```
### `` [#organization-]
The full organization management shell mounted at `/organization/`. Renders `settings` (profile + danger zone) and `people` (members + invitations) tabs for the active organization.
```tsx file=/src/demos/heroui/organization/organization.tsx
import { Organization } from "@better-auth-ui/heroui/plugins/organization"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationDemo() {
return (
)
}
```
### `` [#organizationsettings-]
The contents of the `settings` tab: ``, any plugin-contributed `organizationCards` (for example `` from the [api-key plugin](/docs/heroui/plugins/api-key)), then ``. Drop it into a custom layout if you do not want the tabbed shell.
```tsx file=/src/demos/heroui/organization/organization-settings.tsx
import { OrganizationSettings } from "@better-auth-ui/heroui/plugins/organization"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationSettingsDemo() {
return (
)
}
```
### `` [#organizationprofile-]
Editable profile card for the active organization: logo, display name, and slug. Submits via `useUpdateOrganization`.
```tsx file=/src/demos/heroui/organization/organization-profile.tsx
import { OrganizationProfile } from "@better-auth-ui/heroui/plugins/organization"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationProfileDemo() {
return (
)
}
```
### `` [#organizationdangerzone-]
Danger-zone card with `` and `` rows.
```tsx file=/src/demos/heroui/organization/organization-danger-zone.tsx
import { OrganizationDangerZone } from "@better-auth-ui/heroui/plugins/organization"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationDangerZoneDemo() {
return (
)
}
```
### `` [#organizationpeople-]
The contents of the `people` tab: `` on top, `` below.
```tsx file=/src/demos/heroui/organization/organization-people.tsx
import { OrganizationPeople } from "@better-auth-ui/heroui/plugins/organization"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationPeopleDemo() {
return (
)
}
```
### `` [#organizationmembers-]
Searchable, sortable, filter-by-role table of the active organization's members with an invite control and per-row role / remove actions.
```tsx file=/src/demos/heroui/organization/organization-members.tsx
import { OrganizationMembers } from "@better-auth-ui/heroui/plugins/organization"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationMembersDemo() {
return (
)
}
```
### Paginating members [#paginating-members]
By default `` asks for the whole list and filters it in
the browser. That is fine for a handful of members and quietly wrong for a
large organization: `list-members` caps its response at 100 rows (or your
`membershipLimit`) and says nothing about the rest, so members past that point
do not appear.
Pass `pageSize` to move paging onto the server:
```tsx
```
In paged mode the component sends `limit` and `offset`, applies the role filter
server-side through `filterField`/`filterValue`, sorts by role through
`sortBy`/`sortDirection`, and reads the row count from the endpoint's `total`.
Two controls step aside in paged mode, because `list-members` cannot honour
them across pages:
* **The search box.** The endpoint has no search parameter. Name and email live
on the joined user row, which it does not query.
* **Sorting by member.** Same reason: it can only sort columns on the member
row itself.
The signed-in user's own role comes from `getActiveMemberRole` rather than from
scanning the loaded page because that member can be on a different page. This is
exposed as `useActiveMemberRole` for custom components.
`list-invitations` and `list-sessions` take no pagination parameters at all, so
those lists stay client-side until Better Auth adds them. `list-api-keys` does
support `limit` and `offset`. Wiring it up is a separate change.
### `` [#organizationinvitations-]
Pending invitations for the active organization as a table aligned with ``. Includes role / status filters, plus per-row resend and cancel actions.
Resending re-invites the same address with `resend: true`, which pushes the
invitation's expiry out and sends the email again instead of creating a second
invitation. The action needs the `invitation: ["create"]` permission, the same
one that gates ``.
```tsx file=/src/demos/heroui/organization/organization-invitations.tsx
import { OrganizationInvitations } from "@better-auth-ui/heroui/plugins/organization"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationInvitationsDemo() {
return (
)
}
```
### `` [#organizationssettings-]
The user-level organizations panel rendered at `/settings/organizations`. Lists every organization the user belongs to (with a "Create organization" button and empty state) plus invitations addressed to the user.
```tsx file=/src/demos/heroui/organization/organizations-settings.tsx
import { OrganizationsSettings } from "@better-auth-ui/heroui/plugins/organization"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationsSettingsDemo() {
return (
)
}
```
### `` [#organizations-]
List of organizations the user belongs to with a "Create organization" button and per-row Manage control. Embedded inside ``.
```tsx file=/src/demos/heroui/organization/organizations.tsx
import { Organizations } from "@better-auth-ui/heroui/plugins/organization"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationsDemo() {
return (
)
}
```
### `` [#userinvitations-]
Invitations addressed to the signed-in user across every organization, with Accept / Reject actions. Embedded inside ``.
```tsx file=/src/demos/heroui/organization/user-invitations.tsx
import { UserInvitations } from "@better-auth-ui/heroui/plugins/organization"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function UserInvitationsDemo() {
return (
)
}
```
### `` [#createorganizationdialog-]
Modal dialog with the new-organization form. Owned by `` and ``. Mount it directly when you want to open the create flow from your own surface.
## Multiple roles per member [#multiple-roles-per-member]
Better Auth accepts `role` as a string or an array. It stores multiple roles as
one comma-joined value (`"admin,member"`). The role pickers allow multiple
roles by default. Set `allowMultipleRoles: false` to limit invitations and
members to one role. The pickers always keep at least one role selected.
Anywhere you read a raw role, split it first. `@better-auth-ui/core/plugins/organization`
exports helpers for this:
```ts
import {
hasMemberRole,
memberRoleLabels,
parseMemberRoles
} from "@better-auth-ui/core/plugins/organization"
parseMemberRoles("admin,member") // ["admin", "member"]
hasMemberRole("owner,admin", "owner") // true
memberRoleLabels("admin,auditor", roles) // ["Admin", "auditor"]
```
`hasMemberRole` matters more than it looks: `member.role === "owner"` silently
misses an owner who also carries a second role, which is what the members list
used to do when deciding whether to show owner-only actions.
### `` [#invitememberdialog-]
Modal dialog for inviting a new member by email. Owned by `` and ``. Mount it directly to drive the invite flow from a custom action.
### `` [#deleteorganizationdialog-]
Confirmation dialog for deleting an organization (owner permission, server-side).
## Dynamic organization roles [#dynamic-organization-roles]
Enable dynamic access control in Better Auth before you enable the roles tab. The server, client, and UI must use the same resource and action keys.
```ts title="lib/organization-access.ts"
import { createAccessControl } from "better-auth/plugins/access"
export const organizationStatements = {
project: ["create", "read", "update", "delete"]
} as const
export const organizationAccess = createAccessControl(organizationStatements)
```
```ts title="lib/auth.ts"
import { organization } from "better-auth/plugins"
import { organizationAccess } from "./organization-access"
organization({
ac: organizationAccess,
dynamicAccessControl: { enabled: true }
})
```
```ts title="lib/auth-client.ts"
import { organizationClient } from "better-auth/client/plugins"
import { organizationAccess } from "./organization-access"
organizationClient({
ac: organizationAccess,
dynamicAccessControl: { enabled: true }
})
```
Configure the labels that the permission matrix shows:
```tsx title="components/providers.tsx"
organizationPlugin({
dynamicAccessControl: {
enabled: true,
permissions: {
project: {
label: "Projects",
actions: {
create: "Create projects",
read: "View projects",
update: "Edit projects",
delete: "Delete projects"
}
}
}
}
})
```
This adds the `roles` organization tab. The tab supports role creation, editing, and deletion. Member and invitation controls include both configured roles and roles returned by Better Auth. A role cannot be deleted while a member holds it.
The React hooks also expose the underlying endpoints. Always pass the organization ID:
```tsx
const roles = useListRoles(authClient, {
query: { organizationId }
})
const role = useRole(authClient, {
query: { organizationId, roleId }
})
const createRole = useCreateRole(authClient, organizationId)
const updateRole = useUpdateRole(authClient, organizationId)
const deleteRole = useDeleteRole(authClient, organizationId)
```
## Teams and policy controls [#teams-and-policy-controls]
Enable teams on the Better Auth server, client, and UI plugin:
```tsx
organization({ teams: { enabled: true } })
organizationClient({ teams: { enabled: true } })
organizationPlugin({
teams: {
maximumTeams: 5,
maximumMembersPerTeam: 20,
allowRemovingAllTeams: false
}
})
```
The organization shell adds a Teams tab. Users can create, rename, and delete teams. They can also add and remove team members. Invitations can assign a team.
The UI disables team creation and member assignment when these limits are reached. It also protects the final team unless `allowRemovingAllTeams` is true. Better Auth also accepts callback limits on the server. The browser cannot run those callbacks, so pass their resolved values into the UI configuration when the limits vary by organization.
The `useListTeams`, `useListTeamMembers`, `useCreateTeam`, `useUpdateTeam`, `useRemoveTeam`, `useAddTeamMember`, and `useRemoveTeamMember` hooks cover the team lifecycle.
### User team selection [#user-team-selection]
`TeamSwitcher` lists only the teams that the current user can access in the specified organization. The application owns the selected team. This keeps team routing independent from Better Auth active organizations.
```tsx
import { useState } from "react"
import { TeamSwitcher } from "@better-auth-ui/heroui/plugins/organization"
export function ProjectTeamFilter({ organizationId }: { organizationId: string }) {
const [teamId, setTeamId] = useState(null)
return (
setTeamId(team?.id ?? null)}
/>
)
}
```
Set `syncSession` when an application still reads `session.activeTeamId`. This opt-in mode calls Better Auth `setActiveTeam` after the callback.
The `useListUserTeams` and `useSetActiveTeam` hooks are also available for custom controls. Always pass `organizationId` to both APIs.
### Model-specific fields [#model-specific-fields]
Configure fields by the Better Auth model that stores them. The names and types must match the `schema` fields in the server organization plugin.
```tsx
organizationPlugin({
modelFields: {
organization: [
{ name: "billingCode", type: "string", label: "Billing code" }
],
member: [
{ name: "title", type: "string", label: "Job title" }
],
invitation: [
{ name: "department", type: "string", label: "Department" }
],
team: [
{ name: "description", type: "string", inputType: "textarea", label: "Description" }
],
role: [
{ name: "color", type: "string", label: "Color" }
]
}
})
```
Organization fields appear in creation and profile forms. Invitation, team, and role fields appear in their create and edit workflows. Member fields appear as read-only details because Better Auth does not provide browser client endpoints for creating or editing a member model directly.
Use `organizationLimit`, `membershipLimit`, and `invitationLimit` to disable actions when a configured limit is reached. Use `allowOrganizationCreation: false` to hide creation controls.
Plugins can add an organization settings tab through `organizationTabs`. Each tab receives `organizationId` and `organizationSlug` for the active organization.
# Passkey (/docs/heroui/plugins/passkey)
The passkey plugin adds passwordless authentication using WebAuthn. Users can sign in with their device authenticator (Touch ID, Face ID, Windows Hello) and manage registered passkeys from their security settings.
It contributes:
* A "Continue with Passkey" button rendered on the sign-in and magic-link views (hidden on sign-up)
* A `` security card for listing, adding, renaming, and deleting registered passkeys
* `useSignInPasskey`, `useAddPasskey`, `useUpdatePasskey`, `useDeletePasskey`, and `useListPasskeys` hooks
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Install the [`@better-auth/passkey`](https://www.better-auth.com/docs/plugins/passkey) package and add it to your Better Auth server config:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { passkey } from "@better-auth/passkey" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
passkey() // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `passkeyClient()` to your auth client so `authClient.signIn.passkey` and `authClient.passkey.*` are available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { passkeyClient } from "@better-auth/passkey/client" // [!code highlight]
export const authClient = createAuthClient({
plugins: [passkeyClient()] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Pass `passkeyPlugin()` to ``:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { passkeyPlugin } from "@better-auth-ui/heroui/plugins/passkey" // [!code highlight]
{children}
```
## Components [#components]
### `` [#signin-]
A "Continue with Passkey" button is automatically rendered on the `` and `` views when the plugin is registered (hidden on sign-up).
**Usage**
```tsx
import { PasskeyButton } from "@better-auth-ui/heroui/plugins/passkey"
```
**Props**
### `` [#passkeys-]
The `` security card is automatically rendered on the security settings page when the plugin is registered.
**Usage**
```tsx
import { Passkeys } from "@better-auth-ui/heroui/plugins/passkey"
```
**Props**
## Passkey registration policy [#passkey-registration-policy]
By default, the add-passkey dialog does not set `authenticatorAttachment`.
The browser and operating system show the available passkey options.
Set a preference in the plugin when all registrations must use one authenticator type:
```ts
passkeyPlugin({ authenticatorAttachment: "platform" })
passkeyPlugin({ authenticatorAttachment: "cross-platform" })
```
`"platform"` prefers the current device. `"cross-platform"` prefers a security key or another device. The dialog does not show an attachment selector.
`useAddPasskey` accepts every parameter exposed by `authClient.passkey.addPasskey`. Use this hook for custom registration flows:
```tsx
const { mutate: addPasskey } = useAddPasskey(
authClient as PasskeyAuthClient
)
addPasskey({
name: "Work laptop",
authenticatorAttachment: "platform",
extensions: { credProps: true },
returnWebAuthnResponse: true
})
```
`residentKey` and `userVerification` are server plugin policies. They are not
parameters of `authClient.passkey.addPasskey`, so the client UI does not expose
them.
## Passkey autofill [#passkey-autofill]
With the plugin registered, the sign-in form asks the browser to offer saved
passkeys straight from its autofill dropdown, so most people never press the
passkey button at all. This is the WebAuthn conditional UI flow.
Two pieces make it work, and the built-in components already handle both:
* The identifier and password fields carry `webauthn` as the last token of
their `autocomplete` attribute, added by `withPasskeyAutoFill`.
* `` calls `usePasskeyAutoFill`, which opens a conditionally
mediated request once the browser reports that it supports one.
Browsers without conditional mediation ignore the extra token and never get the
request, so the button stays as the fallback everywhere.
Turn the whole thing off with:
```ts
passkeyPlugin({ autoFill: false })
```
If you write your own sign-in form, add the token and start the conditional
request:
```tsx
import type { PasskeyAuthClient } from "@better-auth-ui/core/plugins/passkey"
import {
isPasskeyAutoFillEnabled,
withPasskeyAutoFill
} from "@better-auth-ui/core/plugins/passkey"
import { usePasskeyAutoFill } from "@better-auth-ui/react/plugins/passkey"
const { authClient, plugins } = useAuth()
const passkeyAutoFill = isPasskeyAutoFillEnabled(plugins)
usePasskeyAutoFill(authClient as PasskeyAuthClient)
```
`navigator.credentials.get()` accepts an `AbortSignal`. The bundled hook calls
the Better Auth passkey client, which does not expose that signal. Unmounting
the form only stops a pending availability probe. It does not cancel a request
that already started.
If your custom implementation calls `navigator.credentials.get()` directly,
pass an `AbortSignal` and abort it during cleanup.
## Options [#options]
```ts
passkeyPlugin({
// Omit this option to let the browser show all available choices.
authenticatorAttachment: "platform",
// Override any of the plugin's localization strings.
localization: {
passkeys: "Security Keys"
}
})
```
## Localization [#localization]
# Phone Number (/docs/heroui/plugins/phone-number)
The HeroUI phone-number plugin contributes a phone sign-in route, password recovery views, and a verified phone-number card for account settings. Its country selector formats national input as the user types, validates it, and sends an E.164 number to Better Auth.
## Setup [#setup]
### Configure Better Auth and your SMS provider [#configure-better-auth-and-your-sms-provider]
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { phoneNumber } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
phoneNumber({
otpLength: 6,
requireVerification: true,
sendOTP: ({ phoneNumber, code }) => {
void sms.send({ to: phoneNumber, body: `Your code is ${code}` })
},
sendPasswordResetOTP: ({ phoneNumber, code }) => {
void sms.send({ to: phoneNumber, body: `Your reset code is ${code}` })
}
})
]
})
```
Keep server-side validation as a trust boundary even though the UI normalizes numbers to E.164. Do not log codes in production. Better Auth recommends dispatching the SMS without waiting for the provider response.
### Update the schema [#update-the-schema]
Use your normal Better Auth schema generation or migration flow. The user model needs nullable `phoneNumber` and `phoneNumberVerified` fields, with `phoneNumber` kept unique.
### Add the client plugin [#add-the-client-plugin]
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { phoneNumberClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [phoneNumberClient()]
})
```
### Register the HeroUI plugin [#register-the-heroui-plugin]
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { phoneNumberPlugin } from "@better-auth-ui/heroui/plugins"
{children}
```
### Allow the new route segments [#allow-the-new-route-segments]
```ts
import { viewPaths } from "@better-auth-ui/core"
import { phoneNumberPlugin } from "@better-auth-ui/heroui/plugins"
const validAuthPaths = new Set([
...Object.values(viewPaths.auth),
...Object.values(phoneNumberPlugin().viewPaths.auth)
])
```
## Flow options [#flow-options]
| UI option | Default | Server requirement |
| ------------------- | ------: | ---------------------------------- |
| `signIn` | `true` | `sendOTP` |
| `passwordSignIn` | `false` | A password credential |
| `passwordReset` | `false` | `sendPasswordResetOTP` |
| `changePhoneNumber` | `true` | `sendOTP` |
| `otpLength` | `6` | Must match Better Auth `otpLength` |
Use `defaultCountry`, `countries`, and `locale` to control the selector. Supply an `adapter` when your application needs different formatting or validation rules.
When password sign-in reports `PHONE_NUMBER_NOT_VERIFIED`, the UI switches to the code step. Better Auth sends that verification code automatically.
Passwordless phone verification is not a second factor. Better Auth applies 2FA to phone number and password sign-in, but not to passwordless verification.
Enable Better Auth `signUpOnVerification` to create an account after it verifies an unknown number. If verification requires more user fields, use a custom view.
## Options and localization [#options-and-localization]
The implementation uses the [React phone-number mutations](/docs/react/mutations/send-phone-number-otp). See the [Better Auth phone-number plugin](https://better-auth.com/docs/plugins/phone-number) for all server options.
# Sign In With Ethereum (/docs/heroui/plugins/siwe)
The SIWE plugin adds an Ethereum wallet button to the sign-in form. It requests a nonce, builds an ERC-4361 message, signs it, and verifies it with Better Auth.
You can also add a wallet settings card. The card lists connected wallets, changes the primary wallet, and removes wallets.
## Setup [#setup]
### Configure Better Auth [#configure-better-auth]
Add `siwe()` to the server. Supply a secure nonce generator and an ERC-4361 message verifier.
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { siwe } from "better-auth/plugins"
import { verifyMessage } from "viem"
import { generateSiweNonce } from "viem/siwe"
export const auth = betterAuth({
plugins: [
siwe({
domain: "app.example.com",
getNonce: async () => generateSiweNonce(),
verifyMessage: async ({ message, signature, address }) =>
verifyMessage({
address: address as `0x${string}`,
message,
signature: signature as `0x${string}`
})
})
]
})
```
Apply the SIWE schema before using the plugin. See the [Better Auth SIWE guide](https://www.better-auth.com/docs/plugins/siwe).
### Add the client plugin [#add-the-client-plugin]
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { siweClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({ plugins: [siweClient()] })
```
### Register the UI plugin [#register-the-ui-plugin]
The included connector uses an injected EIP-1193 wallet. You can provide another connector for Wagmi or another wallet library.
```tsx title="components/providers.tsx"
import { createEip1193WalletConnector } from "@better-auth-ui/core/plugins/siwe"
import { AuthProvider } from "@better-auth-ui/heroui"
import { siwePlugin } from "@better-auth-ui/heroui/plugins/siwe"
{children}
```
## Wallet settings [#wallet-settings]
Better Auth does not expose browser endpoints for listing or changing SIWE wallets. Add authenticated server routes, then connect them with `SiweWalletManager`.
Never trust a user ID from the browser. Resolve the user from the server session for every wallet operation.
```ts title="lib/wallet-manager.ts"
import type {
SiweWalletAccount,
SiweWalletLinkChallenge,
SiweWalletManager
} from "@better-auth-ui/core/plugins/siwe"
const assertOk = async (response: Response) => {
if (!response.ok) {
throw new Error(`Wallet request failed with status ${response.status}.`)
}
}
const get = async (
url: string,
signal?: AbortSignal
): Promise => {
const response = await fetch(url, { signal })
await assertOk(response)
return response.json() as Promise
}
const post = async (
url: string,
body: unknown
): Promise => {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
})
await assertOk(response)
if (response.status === 204) return undefined as TResponse
return response.json() as Promise
}
export const walletManager: SiweWalletManager = {
list: (signal) => get("/api/wallets", signal),
createLinkChallenge: (wallet) =>
post("/api/wallets/link-challenge", wallet),
link: (proof) => post("/api/wallets/link", proof),
unlink: (walletId) => fetch(`/api/wallets/${walletId}`, { method: "DELETE" }).then(assertOk),
setPrimary: (walletId) => fetch(`/api/wallets/${walletId}/primary`, { method: "POST" }).then(assertOk)
}
```
Pass `walletManager` to `siwePlugin()`. BAUI then adds connect, list, primary, and remove controls to security settings.
The link challenge must contain a single-use nonce. Verify its domain, chain, address, nonce, and signature before attaching the wallet.
The server must delete a wallet's `walletAddress` row and matching SIWE account in one transaction.
## Email modes [#email-modes]
* `"optional"` asks for an email but allows an empty value. This is the default.
* `"required"` requires an email before the wallet opens.
* `"none"` opens the wallet immediately.
The signature proves control of the wallet. It does not verify the supplied email address.
# SSO (/docs/heroui/plugins/sso)
The SSO plugin replaces the standard sign-in view with an email-first flow. It sends the email to Better Auth for provider discovery. If a provider exists, Better Auth redirects the user to it. If no provider exists, the view shows the configured password and plugin sign-in methods.
Magic-link and email-OTP views reuse the submitted email. Users do not need to enter it again.
## Setup [#setup]
### Add SSO to the server [#add-sso-to-the-server]
Install `@better-auth/sso`, add `sso()` to Better Auth, and apply the plugin schema to your database. Configure providers with Better Auth or Better Auth Dash.
```ts title="lib/auth.ts"
import { sso } from "@better-auth/sso"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
plugins: [sso()]
})
```
See the [Better Auth SSO guide](https://www.better-auth.com/docs/plugins/sso) for provider setup and schema commands.
### Add the client plugin [#add-the-client-plugin]
```ts title="lib/auth-client.ts"
import { ssoClient } from "@better-auth/sso/client"
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
plugins: [ssoClient()]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Place `ssoPlugin()` before another plugin that replaces the sign-in view.
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { ssoPlugin } from "@better-auth-ui/heroui/plugins/sso"
{children}
```
## Options [#options]
Set `emailFirst: false` when you want the data hooks without replacing the sign-in view.
```tsx
ssoPlugin({
emailFirst: false,
localization: {
continueWithEmail: "Find my workspace"
}
})
```
## Component [#component]
```tsx
import { EmailFirstSignIn } from "@better-auth-ui/heroui/plugins/sso"
```
# Theme (/docs/heroui/plugins/theme)
The theme plugin adds theme selection to your authentication UI. Users can switch between system, light, and dark themes from the user button dropdown and account settings.
## Setup [#setup]
### Register the UI plugin [#register-the-ui-plugin]
The plugin works with any theme library. Pass the theme library's hook, such as `useTheme` from [next-themes](https://github.com/pacocoursey/next-themes).
The slot components can then read the current theme inside ``. You do not need another wrapper.
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { themePlugin } from "@better-auth-ui/heroui/plugins/theme" // [!code highlight]
import { ThemeProvider, useTheme } from "next-themes"
import type { ReactNode } from "react"
import { authClient } from "@/lib/auth-client"
export function Providers({ children }: { children: ReactNode }) {
return (
{children}
)
}
```
The plugin calls `useTheme()` inside its slot components during each render. The call stays inside `` when both providers share a component.
### Or pass static theme state [#or-pass-static-theme-state]
If the theme source has no hook, pass both `theme` and `setTheme`. The source can use `useState` or a custom controller.
The plugin runs during each parent render. The slot components update when the state owner renders with a new value.
```tsx title="components/providers.tsx"
const [theme, setTheme] = useState("system") // [!code highlight]
{children}
```
The two forms are mutually exclusive: you pass either `useTheme` **or** the `theme`/`setTheme` pair.
## Components [#components]
### `` [#userbutton-]
### `` [#appearance-]
The `` card is automatically rendered in `` when the plugin is registered.
**Usage**
```tsx
import { Appearance } from "@better-auth-ui/heroui/plugins/theme"
```
**Props**
## Options [#options]
## Localization [#localization]
# Two Factor (/docs/heroui/plugins/two-factor)
The two-factor plugin adds a second step to password sign-in. Better Auth withholds the session until that step succeeds, answering the sign-in request with `{ twoFactorRedirect: true, twoFactorMethods }` instead.
It contributes:
* A `` view at `/auth/two-factor` covering authenticator codes, emailed codes, backup codes, and "trust this device"
* A `` card in security settings for enrolling, showing the QR code, and managing backup codes
* Mutation hooks for every two-factor endpoint (`useEnableTwoFactor`, `useDisableTwoFactor`, `useVerifyTotp`, `useSendTwoFactorOtp`, `useVerifyTwoFactorOtp`, `useVerifyBackupCode`, `useGenerateBackupCodes`, `useGetTotpUri`)
The built-in sign-in forms detect `twoFactorRedirect` and open the challenge. They preserve `redirectTo` and use it after successful verification.
Better Auth does not apply two-factor to passwordless sign-in. Magic link, email OTP, passkeys, and OAuth all bypass the challenge: the second factor only guards password (and username) sign-in.
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Add the [2FA](https://www.better-auth.com/docs/plugins/2fa) plugin to your server config. Wire `otpOptions.sendOTP` if you want to offer emailed codes as a second factor:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
twoFactor({ // [!code highlight]
issuer: "My App", // [!code highlight]
otpOptions: { // [!code highlight]
sendOTP: async ({ user, otp }) => { // [!code highlight]
// Email `otp` to `user.email`. // [!code highlight]
} // [!code highlight]
} // [!code highlight]
}) // [!code highlight]
]
})
```
### Migrate your database [#migrate-your-database]
The plugin adds a `twoFactor` table and a `twoFactorEnabled` field on `user`. Generate the schema and run the migration:
```bash
npx @better-auth/cli generate
npx @better-auth/cli migrate
```
### Install the matching client plugin [#install-the-matching-client-plugin]
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { twoFactorClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [twoFactorClient()] // [!code highlight]
})
```
Leave `twoFactorPage` and `onTwoFactorRedirect` unset: the UI handles the redirect itself and keeps `redirectTo` intact, while `twoFactorPage` forces a full page reload.
### Register the UI plugin [#register-the-ui-plugin]
```tsx title="components/providers.tsx"
import { AuthProvider } from "@better-auth-ui/heroui"
import { twoFactorPlugin } from "@better-auth-ui/heroui/plugins" // [!code highlight]
{children}
```
### Allow the new view path [#allow-the-new-view-path]
The plugin contributes a `two-factor` segment to `viewPaths.auth`. Spread `twoFactorPlugin().viewPaths?.auth` into your auth route's allowed-paths set:
TanStack Start
Next.js
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { twoFactorPlugin } from "@better-auth-ui/heroui/plugins" // [!code highlight]
import { createFileRoute, notFound } from "@tanstack/react-router"
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (
!Object.values({
...viewPaths.auth,
...twoFactorPlugin().viewPaths?.auth // [!code highlight]
}).includes(path)
) {
throw notFound()
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return
}
```
```tsx title="app/auth/[path]/page.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@better-auth-ui/heroui"
import { twoFactorPlugin } from "@better-auth-ui/heroui/plugins" // [!code highlight]
import { notFound } from "next/navigation"
export default async function AuthPage({
params
}: {
params: Promise<{ path: string }>
}) {
const { path } = await params
if (
!Object.values({
...viewPaths.auth,
...twoFactorPlugin().viewPaths?.auth // [!code highlight]
}).includes(path)
) {
notFound()
}
return
}
```
## The sign-in flow [#the-sign-in-flow]
```text
email/password or username/password
↓
{ twoFactorRedirect: true, twoFactorMethods: ["totp", "otp"] }
↓
/auth/two-factor?redirectTo=…
↓ authenticator code, emailed code, or backup code
authenticated session
```
The method names ride along in session storage: names only, never a code or token. The two-factor cookie that authorizes the challenge stays HTTP-only, exactly as Better Auth set it.
Building a custom sign-in form? Check for the redirect yourself:
```tsx
import {
isTwoFactorRedirect,
storeTwoFactorMethods
} from "@better-auth-ui/core/plugins/two-factor"
const { mutate: signInEmail } = useSignInEmail(authClient, {
onSuccess: (data) => {
if (isTwoFactorRedirect(data)) {
storeTwoFactorMethods(data.twoFactorMethods)
navigate({ to: "/auth/two-factor" })
return
}
navigate({ to: redirectTo })
}
})
```
## Components [#components]
### `` [#twofactorchallenge-]
Rendered at `/auth/two-factor`. Offers the methods the sign-in response reported, plus backup-code recovery and an optional "trust this device" checkbox. Emailed codes are sent on request rather than automatically, so a user with an authenticator app never triggers a pointless email.
```tsx
import { TwoFactorChallenge } from "@better-auth-ui/heroui/plugins"
```
### `` [#twofactorsettings-]
Added to `` automatically. Users can enroll with an authenticator app or a delivered code. Enrolled users can regenerate backup codes or turn two-factor off.
```tsx
import { TwoFactorSettings } from "@better-auth-ui/heroui/plugins"
```
Backup codes live in component state and are never written to storage or the query cache: once the dialog closes they are gone.
## Delivered-code enrollment [#delivered-code-enrollment]
The enrollment dialog offers authenticator apps by default. Configure OTP delivery on the server before you add the delivered-code option:
```ts
// Server
twoFactor({
otpOptions: { sendOTP }
})
```
```tsx
// UI
twoFactorPlugin({ enrollmentMethods: ["totp", "otp"] })
```
Better Auth activates OTP enrollment immediately. The dialog closes after `authClient.twoFactor.enable({ method: "otp" })` succeeds.
## Passwordless accounts [#passwordless-accounts]
Set `allowPasswordless` on both sides to let passkey-only users manage two-factor without a password:
```ts
// Server
twoFactor({ allowPasswordless: true })
```
```tsx
// UI
twoFactorPlugin({ allowPasswordless: true })
```
The UI still asks for a password when the account has a credential account, matching the server's rule. It reads the linked accounts to decide, so users who do have a password are not offered a shortcut around it.
## Options [#options]
```ts
twoFactorPlugin({
// Override the URL segment. Default: "two-factor"
path: "2fa",
// Match the server's TOTP/OTP digits. Default: 6
codeLength: 6,
// Turn off when the server sets `backupCodeOptions: { enabled: false }`
backupCodes: true,
// Hide the "Trust this device" checkbox
trustDevice: false,
// Offer delivered-code enrollment after the server configures sendOTP
enrollmentMethods: ["totp", "otp"]
})
```
## Localization [#localization]
Read these from `useAuthPlugin(twoFactorPlugin).localization` inside custom slot components.
## Email template [#email-template]
Pair `otpOptions.sendOTP` with the [``](/docs/heroui/components/email/otp-email) component for a styled code email.
# Username (/docs/heroui/plugins/username)
The username plugin adds username-based authentication to your auth UI. Users can sign in with a username instead of an email address, and optionally check username availability during sign-up and profile updates.
It contributes:
* A `` view that accepts both username and email, routing to the appropriate sign-in method
* A `` renderer for the username additional field with real-time availability checking
* `useSignInUsername` and `useIsUsernameAvailable` hooks
* Automatic username field injection into sign-up and user profile forms
## Setup [#setup]
The username plugin requires no additional UI installation: the components are built-in. You only need to configure the Better Auth server plugin and register the client plugin.
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Install the [`better-auth`](https://www.better-auth.com/docs/plugins/username) package and add it to your Better Auth server config:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { username } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
username() // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `usernameClient()` to your auth client so `authClient.signIn.username` and `authClient.username.*` are available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { usernameClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [usernameClient()] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Pass `usernamePlugin()` to ``:
```tsx title="components/providers.tsx"
import { usernamePlugin } from "@better-auth-ui/heroui/plugins/username" // [!code highlight]
import { AuthProvider } from "@better-auth-ui/heroui"
{children}
```
## Components [#components]
### `` [#signin-]
### `` [#signup-]
### `` [#userprofile-]
## Options [#options]
Use `usernamePrefix` when usernames are displayed with a marker such as `@`. The prefix appears inside username fields but is not included in the value sent to Better Auth.
```tsx
usernamePlugin({
usernamePrefix: "@",
localization: {
usernamePlaceholder: "username"
}
})
```
## Localization [#localization]
# Introduction (/docs)
Better Auth UI provides beautiful, ready-to-use authentication components for [Better Auth](https://better-auth.com). Built with shadcn/ui, HeroUI, and Zaidan Solid, these components are fully customizable and easy to integrate.
## Features [#features]
* **Fully Customizable**: Built on shadcn/ui, HeroUI, and Zaidan Solid. Own your code, style it your way.
* **Drop-in Ready**: Pre-built Sign In, Sign Up, Forgot Password, and more. Just add and configure.
* **Better Auth Native**: Built specifically for Better Auth. Social logins, magic links, and more.
## Choose Your Framework [#choose-your-framework]
Select a UI framework to get started with Better Auth UI.
## React Reference [#react-reference]
Already picked a UI framework? Dive into the shared data layer that powers every Better Auth UI component.
Hooks, queries, and mutations for every Better Auth endpoint.
Every auth read, with usage and server-side recipes.
Every auth write, with mutation keys and cache side effects.
## Migration guides [#migration-guides]
Upgrade an existing application with the versioned migration guides. These guides remain available after newer releases replace the latest release notes.
Update consumer imports, server helpers, types, and copied components for Better Auth UI 1.7.
## Community [#community]
Join the Better Auth UI Discord to ask questions, share feedback, and connect with other users and contributors.
[](https://better-auth-ui.com/discord)
# Migrate from 1.6 to 1.7 (/docs/migrations/1-7)
Better Auth UI 1.7 changes several public import paths. Most applications must update consumer code, even when their authentication configuration does not change.
Better Auth UI 1.7 requires Better Auth 1.7 or newer. An older Better Auth version can cause many unrelated TypeScript errors after this migration.
## Consumer changes at a glance [#consumer-changes-at-a-glance]
| Consumer code | Required change |
| ----------------------------------- | ------------------------------------------------------------------------------------------------ |
| Core plugin APIs | Replace `@better-auth-ui/core/plugins` with `@better-auth-ui/core/plugins/` |
| React and Solid plugin hooks | Import from `@better-auth-ui/react/plugins/` or `@better-auth-ui/solid/plugins/` |
| Query and mutation option factories | Import shared factories from core and plugin factories from a core plugin path |
| Auth client types | Import shared types from core and plugin types from a core plugin path |
| Server helpers | Replace the React or Solid server path with a core server path |
| Solid auth client | Import `createAuthClient` from `better-auth/solid` |
| Copied registry components | Refresh them and merge the 1.7 changes with local customizations |
## Update consumer code [#update-consumer-code]
### Find imports that need changes [#find-imports-that-need-changes]
Search your application and any internal packages:
```bash
rg '"@better-auth-ui/(core|react|solid)/plugins"' .
rg '"@better-auth-ui/(react|solid)/server"' .
```
Also search for APIs that moved or changed names:
```bash
rg '\b(AuthClient|createAuthClient|sessionOptions|ensureSession|prefetchSession|fetchSession)\b' .
```
Ignore generated output and dependency folders in the results.
### Replace core plugin aggregate imports [#replace-core-plugin-aggregate-imports]
The `@better-auth-ui/core/plugins` entry point no longer exists. Import each API from its plugin entry point.
```ts title="Before"
import {
type OrganizationLocalization,
organizationPlugin,
organizationQueryKeys
} from "@better-auth-ui/core/plugins"
```
```ts title="After"
import {
type OrganizationLocalization,
organizationPlugin,
organizationQueryKeys
} from "@better-auth-ui/core/plugins/organization"
```
Use the plugin name at the end of the path. For example, use `/plugins/api-key`, `/plugins/passkey`, `/plugins/two-factor`, or `/plugins/organization`.
If one import contains APIs from different plugins, split it into one import for each plugin.
### Move plugin hooks out of the framework root [#move-plugin-hooks-out-of-the-framework-root]
Shared hooks remain in `@better-auth-ui/react` and `@better-auth-ui/solid`. Plugin hooks now use a plugin entry point.
```ts title="Before"
import {
useActiveOrganization,
useInviteMember,
useListOrganizations,
useSession
} from "@better-auth-ui/react"
```
```ts title="After"
import { useSession } from "@better-auth-ui/react"
import {
useActiveOrganization,
useInviteMember,
useListOrganizations
} from "@better-auth-ui/react/plugins/organization"
```
Use the same pattern with `@better-auth-ui/solid/plugins/` in a Solid application.
### Move option factories and auth client types to core [#move-option-factories-and-auth-client-types-to-core]
Version 1.7 makes query and mutation option factories framework-independent. Import shared factories and types from `@better-auth-ui/core`.
```ts title="Before"
import {
type AuthClient,
sessionOptions,
signOutOptions
} from "@better-auth-ui/react"
```
```ts title="After"
import {
type AuthClient,
sessionOptions,
signOutOptions
} from "@better-auth-ui/core"
```
Import plugin factories and client types from the matching core plugin path:
```ts
import {
type OrganizationAuthClient,
inviteMemberOptions,
listOrganizationsOptions
} from "@better-auth-ui/core/plugins/organization"
```
Keep framework hooks in the React or Solid package. For example, `useSession` remains in the framework root, and `useListOrganizations` moves to the framework organization entry point.
### Move server helpers and rename session helpers [#move-server-helpers-and-rename-session-helpers]
The React and Solid `/server` entry points no longer exist. Import shared server APIs from `@better-auth-ui/core/server`.
The session helper names now use a `Server` suffix:
| 1.6 name | 1.7 name |
| ----------------- | ----------------------- |
| `sessionOptions` | `sessionOptionsServer` |
| `ensureSession` | `ensureSessionServer` |
| `prefetchSession` | `prefetchSessionServer` |
| `fetchSession` | `fetchSessionServer` |
```ts title="Before"
import {
ensureSession,
sessionOptions
} from "@better-auth-ui/react/server"
```
```ts title="After"
import {
ensureSessionServer,
sessionOptionsServer
} from "@better-auth-ui/core/server"
```
Plugin server APIs use a plugin-specific core path. Their existing function names remain unchanged.
```ts title="Before"
import {
ensureActiveOrganization,
listOrganizationsOptions
} from "@better-auth-ui/react/server"
```
```ts title="After"
import {
ensureActiveOrganization,
listOrganizationsOptions
} from "@better-auth-ui/core/plugins/organization/server"
```
Version 1.7 provides server entry points for API key, magic link, multi-session, organization, passkey, and username.
### Update Solid auth client imports [#update-solid-auth-client-imports]
If a Solid source file imports `createAuthClient` from Better Auth UI, import it from Better Auth instead.
```ts title="Before"
import { createAuthClient } from "@better-auth-ui/solid"
```
```ts title="After"
import { createAuthClient } from "better-auth/solid"
```
### Refresh copied components [#refresh-copied-components]
If you installed shadcn/ui components, the source files are part of your application. A package update does not update those files.
Inspect the registry changes first:
```bash
bunx --bun shadcn@latest add @better-auth-ui/all --dry-run
bunx --bun shadcn@latest add @better-auth-ui/all --diff
```
Then refresh only the groups that your application uses. Merge the changes with your local customizations.
```bash
bunx --bun shadcn@latest add @better-auth-ui/auth @better-auth-ui/settings
bunx --bun shadcn@latest add @better-auth-ui/organization
```
`@better-auth-ui/organization` is a shadcn registry item. It is not an npm package.
If you use Zaidan, inspect the matching Solid registry entries before you replace local files:
```bash
bunx --bun shadcn@latest add https://better-auth-ui.com/r/solid/auth.json --dry-run
bunx --bun shadcn@latest add https://better-auth-ui.com/r/solid/organization.json --dry-run
```
Remove `--dry-run` after you review the changes. HeroUI components come from `@better-auth-ui/heroui`, so they do not need this copied-component step.
## Align dependencies [#align-dependencies]
After you update the consumer code, keep the authentication packages on compatible versions:
* Use `better-auth` 1.7.0 or newer.
* Use Better Auth UI packages from the same 1.7 release.
* Update each installed Better Auth plugin package to 1.7.0 or newer. This includes `@better-auth/api-key`, `@better-auth/oauth-provider`, and `@better-auth/passkey` when you use them.
* Use TanStack Query 5.101.2 or newer.
For a React application, the main update is:
```bash
bun add @better-auth-ui/core@^1.7.0 @better-auth-ui/react@^1.7.0 \
better-auth@^1.7.0 @tanstack/query-core@^5.101.2 \
@tanstack/react-query@^5.101.2
```
For a Solid application, the main update is:
```bash
bun add @better-auth-ui/core@^1.7.0 @better-auth-ui/solid@^1.7.0 \
better-auth@^1.7.0 @tanstack/query-core@^5.101.2 \
@tanstack/solid-query@^5.101.2 solid-js@^1.9.14
```
Add `@better-auth-ui/heroui@^1.7.0` for HeroUI. Update only the separate Better Auth plugin packages that your application already uses.
## Validate the migration [#validate-the-migration]
Check that the dependency tree contains compatible Better Auth versions:
```bash
bun pm why better-auth
bun pm why @better-auth-ui/core
```
Search for removed paths again. Both commands must return no source imports:
```bash
rg '"@better-auth-ui/(core|react|solid)/plugins"' .
rg '"@better-auth-ui/(react|solid)/server"' .
```
Run your typecheck, linter, tests, and production build. Then test the authentication flows that your application uses, including sign-in, account settings, organizations, passkeys, and API keys.
## Common migration errors [#common-migration-errors]
### Package subpath is not exported [#package-subpath-is-not-exported]
A source file still imports an aggregate 1.6 path. Replace it with the applicable plugin-specific 1.7 path.
### Auth client types are incompatible [#auth-client-types-are-incompatible]
First, verify that `better-auth` and its separate plugin packages use version 1.7 or newer. Then verify that every Better Auth UI package uses version 1.7.
If the versions match, update your copied components. Old registry files can keep 1.6 imports and types in your application.
### A Better Auth client method is missing [#a-better-auth-client-method-is-missing]
Add the matching Better Auth client plugin. For example, organization hooks require `organizationClient()` from `better-auth/client/plugins`.
### The organization installer does not resolve [#the-organization-installer-does-not-resolve]
Run the registry installer:
```bash
bunx --bun shadcn@latest add @better-auth-ui/organization
```
Do not install `@better-auth-ui/organization` with a package manager. It is a registry item, not a package.
## Postpone the migration [#postpone-the-migration]
If you cannot update the full integration, restore the complete pre-upgrade state. Restore `package.json`, `bun.lock`, and copied component files together.
The last 1.6 release is `1.6.45`. Do not combine 1.6 packages with 1.7 copied components.
# Overview (/docs/react)
The `@better-auth-ui/react` package provides hooks for Better Auth UI components. It uses [TanStack Query](https://tanstack.com/query) and provides a hook for each supported endpoint.
Each hook wraps `useQuery` or `useMutation` and a matching core options factory. Use the hook in a component.
Use a factory from `@better-auth-ui/core` when TanStack Query requires a `queryOptions` or `mutationOptions` object.
## Prerequisites [#prerequisites]
Wrap your app with a `QueryClientProvider` above ``.
npm
pnpm
yarn
bun
```bash
npm install @tanstack/react-query
```
```bash
pnpm add @tanstack/react-query
```
```bash
yarn add @tanstack/react-query
```
```bash
bun add @tanstack/react-query
```
## Cache keys [#cache-keys]
All keys start with `"auth"`. Queries for one user use the `["auth", "user", userId, ...]` prefix.
This structure clears user queries after sign-out or an account switch. Mutation keys are stable for `useIsMutating` and global `MutationCache` observers.
Read a key off a core factory for cache seeding or invalidation:
```ts
import { sessionOptions, signInEmailOptions } from "@better-auth-ui/core"
sessionOptions(authClient).queryKey
signInEmailOptions(authClient).mutationKey
```
Or pull from the shared key factories in `@better-auth-ui/core`:
```ts
import { authMutationKeys, authQueryKeys } from "@better-auth-ui/core"
queryClient.invalidateQueries({ queryKey: authQueryKeys.session })
useIsMutating({ mutationKey: authMutationKeys.signIn.all })
```
## Escape hatches [#escape-hatches]
For a Better Auth endpoint without a specific hook, use a generic hook:
* `useAuthQuery(authFn, queryKey, options?)`: read endpoints.
* `useAuthMutation(authFn, mutationKey, options?)`: mutation endpoints.
For loaders and prefetch operations, import `authQueryOptions(authFn, queryKey, params?)` from `@better-auth-ui/core`. Mutation endpoints use their core options factory.
Both wire `throw: true` into `fetchOptions` so results reject with a `BetterFetchError` instead of resolving to `{ error }`.
## Next [#next]
Every auth read, with usage and server-side recipes.
Every auth write, with mutation keys and cache side effects.
Customize the QueryClient and prefetch auth data on the server.
# useAcceptInvitation (/docs/react/mutations/accept-invitation)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useAcceptInvitation } from "@better-auth-ui/react/plugins/organization"
const mutation = useAcceptInvitation(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { acceptInvitationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(acceptInvitationOptions(authClient))
```
## Params [#params]
# useAddPasskey (/docs/react/mutations/add-passkey)
Prompts the WebAuthn ceremony then refetches the passkey list on success.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useAddPasskey } from "@better-auth-ui/react/plugins/passkey"
const { mutate: addPasskey, isPending } = useAddPasskey(authClient)
addPasskey({ name: "MacBook Pro" })
```
`useAddPasskey` requires the Better Auth `passkey` plugin. Import your configured `authClient`. It already carries the plugin methods from `createAuthClient`.
## Options factory [#options-factory]
```tsx
import { addPasskeyOptions } from "@better-auth-ui/core/plugins/passkey"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(addPasskeyOptions(authClient))
```
## Params [#params]
# useApproveDevice (/docs/react/mutations/approve-device)
Requires `deviceAuthorizationClient()` from `better-auth/client/plugins`. Call this mutation only after the current session has verified and claimed the user code.
## Usage [#usage]
```tsx
import { useApproveDevice } from "@better-auth-ui/react/plugins/device-authorization"
const approveDevice = useApproveDevice(authClient)
approveDevice.mutate({
userCode: "ABCD1234"
})
```
## Options factory [#options-factory]
```ts
import { approveDeviceOptions } from "@better-auth-ui/core/plugins/device-authorization"
const options = approveDeviceOptions(authClient)
```
## Params [#params]
# useCancelInvitation (/docs/react/mutations/cancel-invitation)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useCancelInvitation } from "@better-auth-ui/react/plugins/organization"
const mutation = useCancelInvitation(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { cancelInvitationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(cancelInvitationOptions(authClient))
```
## Params [#params]
# useChangeEmailOtp (/docs/react/mutations/change-email-otp)
The address on the session changes, so the session query is refetched on success.
## Usage [#usage]
```tsx
import type { EmailOtpAuthClient } from "@better-auth-ui/core/plugins/email-otp"
import { useAuth } from "@better-auth-ui/react"
import { useChangeEmailOtp } from "@better-auth-ui/react/plugins/email-otp"
const { authClient } = useAuth()
const { mutate: changeEmailOtp } = useChangeEmailOtp(
authClient as EmailOtpAuthClient
)
changeEmailOtp({
newEmail: "new@example.com",
otp: "123456"
})
```
`useChangeEmailOtp` requires the Better Auth `emailOTP` plugin. Cast `authClient` to `EmailOtpAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { changeEmailOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(changeEmailOtpOptions(authClient))
```
## Params [#params]
# useChangeEmail (/docs/react/mutations/change-email)
Refetches the session on success so the new email surfaces everywhere that reads `useSession` / `useUser`.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useChangeEmail } from "@better-auth-ui/react"
const { mutate: changeEmail } = useChangeEmail(authClient)
changeEmail({
newEmail: "new@example.com",
callbackURL: "/account"
})
```
## Options factory [#options-factory]
```tsx
import { changeEmailOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(changeEmailOptions(authClient))
```
## Params [#params]
# useChangePassword (/docs/react/mutations/change-password)
The server uses `revokeOtherSessions` to decide whether it revokes existing sessions. This hook does not update the cache.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useChangePassword } from "@better-auth-ui/react"
const { mutate: changePassword } = useChangePassword(authClient)
changePassword({
currentPassword: "hunter2",
newPassword: "hunter3",
revokeOtherSessions: true
})
```
## Options factory [#options-factory]
```tsx
import { changePasswordOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(changePasswordOptions(authClient))
```
## Params [#params]
# useCheckSlug (/docs/react/mutations/check-slug)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useCheckSlug } from "@better-auth-ui/react/plugins/organization"
const mutation = useCheckSlug(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { checkSlugOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(checkSlugOptions(authClient))
```
## Params [#params]
# useCreateApiKey (/docs/react/mutations/create-api-key)
Requires the Better Auth API key plugin.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useCreateApiKey } from "@better-auth-ui/react/plugins/api-key"
const mutation = useCreateApiKey(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { createApiKeyOptions } from "@better-auth-ui/core/plugins/api-key"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(createApiKeyOptions(authClient))
```
## Params [#params]
# useCreateOrganization (/docs/react/mutations/create-organization)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useCreateOrganization } from "@better-auth-ui/react/plugins/organization"
const mutation = useCreateOrganization(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { createOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(createOrganizationOptions(authClient))
```
## Params [#params]
# useDeleteApiKey (/docs/react/mutations/delete-api-key)
Requires the Better Auth API key plugin.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useDeleteApiKey } from "@better-auth-ui/react/plugins/api-key"
const mutation = useDeleteApiKey(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { deleteApiKeyOptions } from "@better-auth-ui/core/plugins/api-key"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(deleteApiKeyOptions(authClient))
```
## Params [#params]
# useDeleteOAuthConsent (/docs/react/mutations/delete-oauth-consent)
Requires `oauthProviderClient()` from `@better-auth/oauth-provider/client`.
## Usage [#usage]
```tsx
import { useAuth } from "@better-auth-ui/react"
import { useDeleteOAuthConsent } from "@better-auth-ui/react/plugins/oauth-provider"
const { authClient } = useAuth()
const { mutateAsync: deleteConsent } = useDeleteOAuthConsent(authClient)
for (const id of application.consentIds) {
await deleteConsent({ id })
}
```
Better Auth can store more than one consent record per OAuth client. Group the records with `groupOAuthConsents` and delete every ID in the group so the application really has to ask again.
Deleting a consent removes the stored approval, so the application needs the user's approval before it receives new access. It does not revoke access tokens or refresh tokens that were already issued: those stay valid until they expire. Better Auth's consent deletion endpoint does not offer complete token revocation semantics, so do not tell users their access was cut off.
On success the user's consent list is invalidated and refetched, so a removal that fails halfway still shows the server's real state.
## Options factory [#options-factory]
```ts
import { deleteOAuthConsentOptions } from "@better-auth-ui/core/plugins/oauth-provider"
const options = deleteOAuthConsentOptions(authClient)
```
## Params [#params]
# useDeleteOrganization (/docs/react/mutations/delete-organization)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useDeleteOrganization } from "@better-auth-ui/react/plugins/organization"
const mutation = useDeleteOrganization(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { deleteOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(deleteOrganizationOptions(authClient))
```
## Params [#params]
# useDeletePasskey (/docs/react/mutations/delete-passkey)
Refetches the passkey list on success.
## Usage [#usage]
```tsx
import { useDeletePasskey } from "@better-auth-ui/react/plugins/passkey"
import { authClient } from "@/lib/auth-client"
const { mutate: deletePasskey } = useDeletePasskey(authClient)
deletePasskey({ id: passkey.id })
```
`useDeletePasskey` requires the Better Auth `passkey` plugin on your app auth client.
## Options factory [#options-factory]
```tsx
import { deletePasskeyOptions } from "@better-auth-ui/core/plugins/passkey"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(deletePasskeyOptions(authClient))
```
## Params [#params]
# useDeleteUser (/docs/react/mutations/delete-user)
Depending on your Better Auth server config, this either deletes the account immediately or sends a confirmation email.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useDeleteUser } from "@better-auth-ui/react"
const { mutate: deleteUser } = useDeleteUser(authClient)
deleteUser({ password: "hunter2", callbackURL: "/" })
```
After an immediate deletion, use `useSignOut` to clear the cache. Alternatively, call `queryClient.removeQueries({ queryKey: ["auth"] })`.
## Options factory [#options-factory]
```tsx
import { deleteUserOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(deleteUserOptions(authClient))
```
## Params [#params]
# useDenyDevice (/docs/react/mutations/deny-device)
Requires `deviceAuthorizationClient()` from `better-auth/client/plugins`. Call this mutation after the current session has verified and claimed the user code.
## Usage [#usage]
```tsx
import { useDenyDevice } from "@better-auth-ui/react/plugins/device-authorization"
const denyDevice = useDenyDevice(authClient)
denyDevice.mutate({
userCode: "ABCD1234"
})
```
## Options factory [#options-factory]
```ts
import { denyDeviceOptions } from "@better-auth-ui/core/plugins/device-authorization"
const options = denyDeviceOptions(authClient)
```
## Params [#params]
# useDisableTwoFactor (/docs/react/mutations/disable-two-factor)
Refetches the session so `user.twoFactorEnabled` is up to date.
## Usage [#usage]
```tsx
import type { TwoFactorAuthClient } from "@better-auth-ui/core/plugins/two-factor"
import { useAuth } from "@better-auth-ui/react"
import { useDisableTwoFactor } from "@better-auth-ui/react/plugins/two-factor"
const { authClient } = useAuth()
const { mutate: disableTwoFactor } = useDisableTwoFactor(
authClient as TwoFactorAuthClient
)
disableTwoFactor({ password: "current-password" })
```
`useDisableTwoFactor` requires the Better Auth `twoFactor` plugin. Cast `authClient` to `TwoFactorAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { disableTwoFactorOptions } from "@better-auth-ui/core/plugins/two-factor"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(disableTwoFactorOptions(authClient))
```
## Params [#params]
# useEnableTwoFactor (/docs/react/mutations/enable-two-factor)
Resolves with the TOTP URI and the backup codes. Unless the server sets `skipVerificationOnEnable`, two-factor only becomes active once `useVerifyTotp` succeeds.
## Usage [#usage]
```tsx
import type { TwoFactorAuthClient } from "@better-auth-ui/core/plugins/two-factor"
import { useAuth } from "@better-auth-ui/react"
import { useEnableTwoFactor } from "@better-auth-ui/react/plugins/two-factor"
const { authClient } = useAuth()
const { mutate: enableTwoFactor } = useEnableTwoFactor(
authClient as TwoFactorAuthClient
)
enableTwoFactor(
{ password: "current-password" },
{
onSuccess: ({ totpURI, backupCodes }) => {
// Render the QR code and show the backup codes once.
}
}
)
```
`useEnableTwoFactor` requires the Better Auth `twoFactor` plugin. Cast `authClient` to `TwoFactorAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { enableTwoFactorOptions } from "@better-auth-ui/core/plugins/two-factor"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(enableTwoFactorOptions(authClient))
```
## Params [#params]
# useGenerateBackupCodes (/docs/react/mutations/generate-backup-codes)
The new codes are returned once. Keep them in component state and let the user copy them: they are never returned again.
## Usage [#usage]
```tsx
import type { TwoFactorAuthClient } from "@better-auth-ui/core/plugins/two-factor"
import { useAuth } from "@better-auth-ui/react"
import { useGenerateBackupCodes } from "@better-auth-ui/react/plugins/two-factor"
const { authClient } = useAuth()
const { mutate: generateBackupCodes } = useGenerateBackupCodes(
authClient as TwoFactorAuthClient
)
generateBackupCodes({ password: "current-password" })
```
`useGenerateBackupCodes` requires the Better Auth `twoFactor` plugin. Cast `authClient` to `TwoFactorAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { generateBackupCodesOptions } from "@better-auth-ui/core/plugins/two-factor"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(generateBackupCodesOptions(authClient))
```
## Params [#params]
# useGetTotpUri (/docs/react/mutations/get-totp-uri)
This operation is a mutation because the endpoint uses POST. It accepts the password and returns a secret that the application must not cache.
## Usage [#usage]
```tsx
import type { TwoFactorAuthClient } from "@better-auth-ui/core/plugins/two-factor"
import { useAuth } from "@better-auth-ui/react"
import { useGetTotpUri } from "@better-auth-ui/react/plugins/two-factor"
const { authClient } = useAuth()
const { mutate: getTotpUri } = useGetTotpUri(
authClient as TwoFactorAuthClient
)
getTotpUri({ password: "current-password" })
```
`useGetTotpUri` requires the Better Auth `twoFactor` plugin. Cast `authClient` to `TwoFactorAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { getTotpUriOptions } from "@better-auth-ui/core/plugins/two-factor"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(getTotpUriOptions(authClient))
```
## Params [#params]
# Mutations (/docs/react/mutations)
React provides mutation hooks. Core provides the shared options factories. Each factory exposes its canonical key through `.mutationKey`.
```ts
import { authClient } from "@/lib/auth-client"
import { signInEmailOptions } from "@better-auth-ui/core"
import { useSignInEmail } from "@better-auth-ui/react"
```
Use the hook in components. Use the core factory where TanStack Query accepts a `mutationOptions` object.
## Error handling [#error-handling]
Each mutation adds `throw: true` to `fetchOptions`. The promise rejects with `BetterFetchError` instead of resolving with `{ error }`.
You can therefore use the standard `error`, `isError`, `throwOnError`, and `onError` values from `useMutation`.
```tsx
const { mutate, error } = useSignInEmail(authClient, {
onError: (err) => toast.error(err.message)
})
```
## Cache side effects [#cache-side-effects]
Hooks that change authentication state also update the cache. A custom `onSuccess` handler runs after the built-in cache update:
* `useSignInEmail` / `useSignInUsername` / `useSignInPasskey` / `useSignUpEmail`: reset the session query so it retrieves the new session.
* `useSignInSocial` / `useSignInMagicLink`: redirect without a cache update.
* `useSignOut`: remove every `["auth", ...]` query.
* `useUpdateUser`: update the current user's profile fields and refresh the cached session.
* `useSetActiveSession`: update the cached session, scroll to the top, and refresh the session queries.
* `useChangeEmail` / `useChangeEmailOtp`: refresh the session.
* `useSignInEmailOtp` / `useVerifyEmailOtp`: refresh the session because both operations create one.
* `useVerifyTotp` / `useVerifyTwoFactorOtp` / `useVerifyBackupCode`: refresh the session after the second factor creates it.
* `useEnableTwoFactor` / `useDisableTwoFactor`: refresh the session to update `user.twoFactorEnabled`.
* `useAddPasskey` / `useDeletePasskey`: refresh the passkey list.
* `useRevokeSession`: refresh the sessions list.
* `useRevokeMultiSession`: refresh the device sessions list.
* `useUnlinkAccount`: refresh the linked accounts list.
## Tracking mutation state globally [#tracking-mutation-state-globally]
All mutation keys start with `"auth"`. The shared `authMutationKeys` factory in `@better-auth-ui/core` exposes these keys.
Use this factory instead of inline tuples. Call sites and mutation factories will then use the same keys:
```ts
import { authMutationKeys } from "@better-auth-ui/core"
import { useIsMutating } from "@tanstack/react-query"
const authPending = useIsMutating({ mutationKey: authMutationKeys.all })
const signInPending = useIsMutating({ mutationKey: authMutationKeys.signIn.all })
const emailSignInPending = useIsMutating({
mutationKey: authMutationKeys.signIn.email
})
```
Each grouping (`signIn`, `signUp`, `passkey`, `multiSession`) exposes an `all` prefix so you can match a whole feature at once.
Match inside a `MutationCache` observer for global toasts or analytics:
```ts
import { authMutationKeys } from "@better-auth-ui/core"
new MutationCache({
onError: (error, _vars, _ctx, mutation) => {
if (mutation.options.mutationKey?.[0] === authMutationKeys.all[0]) {
toast.error(error.message)
}
}
})
```
## Escape hatch [#escape-hatch]
Use `useAuthMutation` for a mutation endpoint that has no specific hook. Use `useAuthQuery` for a read endpoint.
```tsx
import { authClient } from "@/lib/auth-client"
import { useAuthMutation } from "@better-auth-ui/react"
const { mutate } = useAuthMutation(
authClient.emailOtp.sendVerificationOtp,
["auth", "emailOtp", "sendVerificationOtp"]
)
mutate({ email: "user@example.com", type: "sign-in" })
```
TypeScript infers variables from the `authFn` parameter. Required parameters prevent an empty `mutate()` call, while optional parameters permit it.
The factory adds `throw: true` to `fetchOptions`. Therefore, `onError` and `error` receive a `BetterFetchError`.
For shared mutation registration (`useIsMutating`, a global `MutationCache` observer, manual `useMutation`), import the endpoint's option factory from core and use it directly:
```ts
import { changePasswordOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(changePasswordOptions(authClient))
```
For endpoints that already have a key in `authMutationKeys`, prefer it over an inline tuple so cache observers and `useIsMutating` checks line up.
## Available mutations [#available-mutations]
### Auth [#auth]
### Settings [#settings]
### Organization [#organization]
# useInviteMember (/docs/react/mutations/invite-member)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useInviteMember } from "@better-auth-ui/react/plugins/organization"
const mutation = useInviteMember(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { inviteMemberOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(inviteMemberOptions(authClient))
```
## Params [#params]
# useIsUsernameAvailable (/docs/react/mutations/is-username-available)
This operation is a mutation because a user action starts it. Typical actions include a debounced input change or form submission.
`data` is typed as `{ available: boolean; message: string | null }`.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useIsUsernameAvailable } from "@better-auth-ui/react/plugins/username"
const { mutateAsync: checkUsername, data } = useIsUsernameAvailable(authClient)
const result = await checkUsername({ username: "alice" })
// result.available -> boolean
```
`useIsUsernameAvailable` requires the Better Auth `username` plugin. Import your configured `authClient`. It already carries the plugin methods from `createAuthClient`.
## Options factory [#options-factory]
```tsx
import { isUsernameAvailableOptions } from "@better-auth-ui/core/plugins/username"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(isUsernameAvailableOptions(authClient))
```
## Params [#params]
# useLeaveOrganization (/docs/react/mutations/leave-organization)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useLeaveOrganization } from "@better-auth-ui/react/plugins/organization"
const mutation = useLeaveOrganization(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { leaveOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(leaveOrganizationOptions(authClient))
```
## Params [#params]
# useLinkSocial (/docs/react/mutations/link-social)
Kicks off the OAuth redirect: the linked account appears in `useListAccounts` after the provider redirects back.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useLinkSocial } from "@better-auth-ui/react"
const { mutate: linkSocial } = useLinkSocial(authClient)
linkSocial({
provider: "github",
callbackURL: "/account"
})
```
## Options factory [#options-factory]
```tsx
import { linkSocialOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(linkSocialOptions(authClient))
```
## Params [#params]
# useOAuthConsent (/docs/react/mutations/oauth-consent)
Requires `oauthProviderClient()` from `@better-auth/oauth-provider/client`. Better Auth reads and validates the signed authorization query from the current browser URL before completing the redirect.
## Usage [#usage]
```tsx
import { useOAuthConsent } from "@better-auth-ui/react/plugins/oauth-provider"
const consent = useOAuthConsent(authClient)
consent.mutate({ accept: true })
consent.mutate({ accept: false })
```
Pass only the user's decision for the complete requested scope set. The OAuth Provider plugin handles the redirect.
Do not navigate to `redirect_uri` from the application.
## Options factory [#options-factory]
```ts
import { oauthConsentOptions } from "@better-auth-ui/core/plugins/oauth-provider"
const options = oauthConsentOptions(authClient)
```
## Params [#params]
# useOAuthContinue (/docs/react/mutations/oauth-continue)
Requires `oauthProviderClient()` from `@better-auth/oauth-provider/client`.
Better Auth's [redirect screens](https://better-auth.com/docs/plugins/oauth-provider#redirect-screens) hand control to your app so it can create an account, pick an account, or run its own post-login step. When that step finishes, this mutation tells Better Auth to pick the authorization back up.
## Usage [#usage]
```tsx
import { useOAuthContinue } from "@better-auth-ui/react/plugins/oauth-provider"
const oauthContinue = useOAuthContinue(authClient)
// After the account was created during this flow (`prompt=create`)
oauthContinue.mutate({ created: true })
// After the user picked an account (`prompt=select_account`)
oauthContinue.mutate({ selected: true })
// After your own post-login selection screen
await oauthContinue.mutateAsync({ postLogin: true })
```
Set exactly one flag per call: the one matching the screen that just finished.
The signed authorization query has to stay in the browser URL. `oauthProviderClient()` reads it from there and forwards it, so never rebuild the query string or navigate to `redirect_uri` yourself.
Only call `{ created: true }` when the account was created during the current flow and the sign-up left a usable session. An already signed-in user is not a newly created one, and email verification or a social sign-up needs its own resume step.
## Options factory [#options-factory]
```ts
import { oauthContinueOptions } from "@better-auth-ui/core/plugins/oauth-provider"
const options = oauthContinueOptions(authClient)
```
## Params [#params]
# useRejectInvitation (/docs/react/mutations/reject-invitation)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useRejectInvitation } from "@better-auth-ui/react/plugins/organization"
const mutation = useRejectInvitation(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { rejectInvitationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(rejectInvitationOptions(authClient))
```
## Params [#params]
# useRemoveMember (/docs/react/mutations/remove-member)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useRemoveMember } from "@better-auth-ui/react/plugins/organization"
const mutation = useRemoveMember(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { removeMemberOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(removeMemberOptions(authClient))
```
## Params [#params]
# useRequestEmailChangeOtp (/docs/react/mutations/request-email-change-otp)
Sends a code to the new address. When the server runs with `changeEmail: { verifyCurrentEmail: true }`, pass the `otp` the user received at their current address too.
## Usage [#usage]
```tsx
import type { EmailOtpAuthClient } from "@better-auth-ui/core/plugins/email-otp"
import { useAuth } from "@better-auth-ui/react"
import { useRequestEmailChangeOtp } from "@better-auth-ui/react/plugins/email-otp"
const { authClient } = useAuth()
const { mutate: requestEmailChangeOtp } = useRequestEmailChangeOtp(
authClient as EmailOtpAuthClient
)
requestEmailChangeOtp({ newEmail: "new@example.com" })
```
`useRequestEmailChangeOtp` requires the Better Auth `emailOTP` plugin. Cast `authClient` to `EmailOtpAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { requestEmailChangeOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(requestEmailChangeOtpOptions(authClient))
```
## Params [#params]
# useRequestPasswordResetOtp (/docs/react/mutations/request-password-reset-otp)
Pair with `useResetPasswordOtp`, which takes the code and the new password together: no reset link is involved.
## Usage [#usage]
```tsx
import type { EmailOtpAuthClient } from "@better-auth-ui/core/plugins/email-otp"
import { useAuth } from "@better-auth-ui/react"
import { useRequestPasswordResetOtp } from "@better-auth-ui/react/plugins/email-otp"
const { authClient } = useAuth()
const { mutate: requestPasswordResetOtp } = useRequestPasswordResetOtp(
authClient as EmailOtpAuthClient
)
requestPasswordResetOtp({ email: "alice@example.com" })
```
`useRequestPasswordResetOtp` requires the Better Auth `emailOTP` plugin. Cast `authClient` to `EmailOtpAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { requestPasswordResetOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(requestPasswordResetOtpOptions(authClient))
```
## Params [#params]
# useRequestPasswordReset (/docs/react/mutations/request-password-reset)
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useRequestPasswordReset } from "@better-auth-ui/react"
const { mutate: requestPasswordReset } = useRequestPasswordReset(authClient)
requestPasswordReset({
email: "alice@example.com",
redirectTo: "/auth/reset-password"
})
```
## Options factory [#options-factory]
```tsx
import { requestPasswordResetOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(requestPasswordResetOptions(authClient))
```
## Params [#params]
# useRequestPhoneNumberPasswordReset (/docs/react/mutations/request-phone-number-password-reset)
Configure Better Auth `sendPasswordResetOTP` before exposing this action.
```tsx
import type { PhoneNumberAuthClient } from "@better-auth-ui/core/plugins/phone-number"
import { useAuth } from "@better-auth-ui/react"
import { useRequestPhoneNumberPasswordReset } from "@better-auth-ui/react/plugins/phone-number"
const { authClient } = useAuth()
const { mutate: requestReset } = useRequestPhoneNumberPasswordReset(
authClient as PhoneNumberAuthClient
)
requestReset({ phoneNumber: "+12025550123" })
```
## Options factory [#options-factory]
```tsx
import { requestPhoneNumberPasswordResetOptions } from "@better-auth-ui/core/plugins/phone-number"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(
requestPhoneNumberPasswordResetOptions(authClient)
)
```
## Params [#params]
# useResetPasswordOtp (/docs/react/mutations/reset-password-otp)
There is no token in the URL: the code and the new password are submitted in one call.
## Usage [#usage]
```tsx
import type { EmailOtpAuthClient } from "@better-auth-ui/core/plugins/email-otp"
import { useAuth } from "@better-auth-ui/react"
import { useResetPasswordOtp } from "@better-auth-ui/react/plugins/email-otp"
const { authClient } = useAuth()
const { mutate: resetPasswordOtp } = useResetPasswordOtp(
authClient as EmailOtpAuthClient
)
resetPasswordOtp({
email: "alice@example.com",
otp: "123456",
password: "new-password"
})
```
`useResetPasswordOtp` requires the Better Auth `emailOTP` plugin. Cast `authClient` to `EmailOtpAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { resetPasswordOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(resetPasswordOtpOptions(authClient))
```
## Params [#params]
# useResetPassword (/docs/react/mutations/reset-password)
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useResetPassword } from "@better-auth-ui/react"
const { mutate: resetPassword } = useResetPassword(authClient)
resetPassword({
newPassword: "hunter3",
token
})
```
## Options factory [#options-factory]
```tsx
import { resetPasswordOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(resetPasswordOptions(authClient))
```
## Params [#params]
# useResetPhoneNumberPassword (/docs/react/mutations/reset-phone-number-password)
```tsx
import type { PhoneNumberAuthClient } from "@better-auth-ui/core/plugins/phone-number"
import { useAuth } from "@better-auth-ui/react"
import { useResetPhoneNumberPassword } from "@better-auth-ui/react/plugins/phone-number"
const { authClient } = useAuth()
const { mutate: resetPassword } = useResetPhoneNumberPassword(
authClient as PhoneNumberAuthClient
)
resetPassword({
phoneNumber: "+12025550123",
otp: "123456",
newPassword
})
```
## Options factory [#options-factory]
```tsx
import { resetPhoneNumberPasswordOptions } from "@better-auth-ui/core/plugins/phone-number"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(resetPhoneNumberPasswordOptions(authClient))
```
## Params [#params]
# useRevokeMultiSession (/docs/react/mutations/revoke-multi-session)
Refetches the device sessions list on success.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useRevokeMultiSession } from "@better-auth-ui/react/plugins/multi-session"
const { mutate: revokeMultiSession } = useRevokeMultiSession(authClient)
revokeMultiSession({ sessionToken: deviceSession.session.token })
```
`useRevokeMultiSession` requires the Better Auth `multiSession` plugin. Import your configured `authClient`. It already carries the plugin methods from `createAuthClient`.
## Options factory [#options-factory]
```tsx
import { revokeMultiSessionOptions } from "@better-auth-ui/core/plugins/multi-session"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(revokeMultiSessionOptions(authClient))
```
## Params [#params]
# useRevokeSession (/docs/react/mutations/revoke-session)
Refetches the sessions list on success.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useRevokeSession } from "@better-auth-ui/react"
const { mutate: revokeSession } = useRevokeSession(authClient)
revokeSession({ token: session.token })
```
## Options factory [#options-factory]
```tsx
import { revokeSessionOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(revokeSessionOptions(authClient))
```
## Params [#params]
# useSendPhoneNumberOtp (/docs/react/mutations/send-phone-number-otp)
Requires Better Auth `phoneNumber()` on the server and `phoneNumberClient()` on the client.
```tsx
import {
type PhoneNumberAuthClient
} from "@better-auth-ui/core/plugins/phone-number"
import { useAuth } from "@better-auth-ui/react"
import { useSendPhoneNumberOtp } from "@better-auth-ui/react/plugins/phone-number"
const { authClient } = useAuth()
const { mutate: sendOtp } = useSendPhoneNumberOtp(
authClient as PhoneNumberAuthClient
)
sendOtp({ phoneNumber: "+12025550123" })
```
## Options factory [#options-factory]
```tsx
import { sendPhoneNumberOtpOptions } from "@better-auth-ui/core/plugins/phone-number"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(sendPhoneNumberOtpOptions(authClient))
```
## Params [#params]
# useSendTwoFactorOtp (/docs/react/mutations/send-two-factor-otp)
Authenticated by the two-factor cookie Better Auth set during sign-in, so it only works while a challenge is pending.
## Usage [#usage]
```tsx
import type { TwoFactorAuthClient } from "@better-auth-ui/core/plugins/two-factor"
import { useAuth } from "@better-auth-ui/react"
import { useSendTwoFactorOtp } from "@better-auth-ui/react/plugins/two-factor"
const { authClient } = useAuth()
const { mutate: sendTwoFactorOtp } = useSendTwoFactorOtp(
authClient as TwoFactorAuthClient
)
sendTwoFactorOtp()
```
`useSendTwoFactorOtp` requires the Better Auth `twoFactor` plugin. Cast `authClient` to `TwoFactorAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { sendTwoFactorOtpOptions } from "@better-auth-ui/core/plugins/two-factor"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(sendTwoFactorOtpOptions(authClient))
```
## Params [#params]
# useSendVerificationEmail (/docs/react/mutations/send-verification-email)
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSendVerificationEmail } from "@better-auth-ui/react"
const { mutate: sendVerificationEmail } = useSendVerificationEmail(authClient)
sendVerificationEmail({
email: "alice@example.com",
callbackURL: "/dashboard"
})
```
## Options factory [#options-factory]
```tsx
import { sendVerificationEmailOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(sendVerificationEmailOptions(authClient))
```
## Params [#params]
# useSendVerificationOtp (/docs/react/mutations/send-verification-otp)
One endpoint backs every email-OTP flow. `type` picks which one: `"sign-in"`, `"email-verification"`, `"forget-password"`, or `"change-email"`.
## Usage [#usage]
```tsx
import type { EmailOtpAuthClient } from "@better-auth-ui/core/plugins/email-otp"
import { useAuth } from "@better-auth-ui/react"
import { useSendVerificationOtp } from "@better-auth-ui/react/plugins/email-otp"
const { authClient } = useAuth()
const { mutate: sendVerificationOtp } = useSendVerificationOtp(
authClient as EmailOtpAuthClient
)
sendVerificationOtp({
email: "alice@example.com",
type: "sign-in"
})
```
`useSendVerificationOtp` requires the Better Auth `emailOTP` plugin. Cast `authClient` to `EmailOtpAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { sendVerificationOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(sendVerificationOtpOptions(authClient))
```
## Params [#params]
# useSetActiveOrganization (/docs/react/mutations/set-active-organization)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSetActiveOrganization } from "@better-auth-ui/react/plugins/organization"
const mutation = useSetActiveOrganization(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { setActiveOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(setActiveOrganizationOptions(authClient))
```
## Params [#params]
# useSetActiveSession (/docs/react/mutations/set-active-session)
On success the hook:
1. Optimistically swaps the cached session to the matching entry from `useListDeviceSessions`.
2. Scrolls the window to the top.
3. Refetches both the session and device-session queries.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSetActiveSession } from "@better-auth-ui/react/plugins/multi-session"
const { mutate: setActive } = useSetActiveSession(authClient)
setActive({ sessionToken: deviceSession.session.token })
```
`useSetActiveSession` requires the Better Auth `multiSession` plugin. Import your configured `authClient`. It already carries the plugin methods from `createAuthClient`.
## Options factory [#options-factory]
```tsx
import { setActiveSessionOptions } from "@better-auth-ui/core/plugins/multi-session"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(setActiveSessionOptions(authClient))
```
Note: The raw factory skips the optimistic session update. Use `useSetActiveSession` for the complete behavior.
Use the raw factory only when you need a stable options object, such as for matching a `mutationKey`.
## Params [#params]
# useSignInEmailOtp (/docs/react/mutations/sign-in-email-otp)
Refetches the session on success. Send the code with `useSendVerificationOtp({ type: "sign-in" })` first.
## Usage [#usage]
```tsx
import type { EmailOtpAuthClient } from "@better-auth-ui/core/plugins/email-otp"
import { useAuth } from "@better-auth-ui/react"
import { useSignInEmailOtp } from "@better-auth-ui/react/plugins/email-otp"
const { authClient } = useAuth()
const { mutate: signInEmailOtp } = useSignInEmailOtp(
authClient as EmailOtpAuthClient
)
signInEmailOtp({
email: "alice@example.com",
otp: "123456"
})
```
`useSignInEmailOtp` requires the Better Auth `emailOTP` plugin. Cast `authClient` to `EmailOtpAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { signInEmailOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(signInEmailOtpOptions(authClient))
```
## Params [#params]
# useSignInEmail (/docs/react/mutations/sign-in-email)
The email sign-in mutation calls `authClient.signIn.email` with the supplied credentials.
With `AuthProvider`, a successful sign-in invalidates the session query and waits for active session queries to refetch.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSignInEmail } from "@better-auth-ui/react"
const { mutate: signInEmail, isPending } = useSignInEmail(authClient)
signInEmail({
email: "alice@example.com",
password: "hunter2",
rememberMe: true
})
```
## Options factory [#options-factory]
```tsx
import { signInEmailOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(signInEmailOptions(authClient))
```
`signInEmailOptions` supplies the mutation key, request function, and session invalidation metadata.
Without `AuthProvider` or `setupMutationInvalidation`, handle session invalidation in the mutation's `onSuccess` callback. This requirement applies to both the hook and the options factory.
## Params [#params]
# useSignInMagicLink (/docs/react/mutations/sign-in-magic-link)
The session is established when the user clicks the emailed link, so no session-cache side effects run here.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSignInMagicLink } from "@better-auth-ui/react/plugins/magic-link"
const { mutate: signInMagicLink } = useSignInMagicLink(authClient)
signInMagicLink({
email: "alice@example.com",
callbackURL: "/dashboard"
})
```
`useSignInMagicLink` requires the Better Auth `magicLink` plugin. Import your configured `authClient`. It already carries the plugin methods from `createAuthClient`.
## Options factory [#options-factory]
```tsx
import { signInMagicLinkOptions } from "@better-auth-ui/core/plugins/magic-link"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(signInMagicLinkOptions(authClient))
```
## Params [#params]
# useSignInPasskey (/docs/react/mutations/sign-in-passkey)
Resets the session query on success so the new session is refetched.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSignInPasskey } from "@better-auth-ui/react/plugins/passkey"
const { mutate: signInPasskey } = useSignInPasskey(authClient)
signInPasskey()
```
`useSignInPasskey` requires the Better Auth `passkey` plugin. Import your configured `authClient`. It already carries the plugin methods from `createAuthClient`.
## Options factory [#options-factory]
```tsx
import { signInPasskeyOptions } from "@better-auth-ui/core/plugins/passkey"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(signInPasskeyOptions(authClient))
```
## Params [#params]
# useSignInPhoneNumber (/docs/react/mutations/sign-in-phone-number)
```tsx
import {
type PhoneNumberAuthClient
} from "@better-auth-ui/core/plugins/phone-number"
import { useAuth } from "@better-auth-ui/react"
import { useSignInPhoneNumber } from "@better-auth-ui/react/plugins/phone-number"
const { authClient } = useAuth()
const { mutate: signInPhoneNumber } = useSignInPhoneNumber(
authClient as PhoneNumberAuthClient
)
signInPhoneNumber({
phoneNumber: "+12025550123",
password,
rememberMe: true
})
```
With Better Auth `requireVerification`, an unverified credential returns `PHONE_NUMBER_NOT_VERIFIED` and starts phone verification. This password-based method follows configured 2FA.
## Options factory [#options-factory]
```tsx
import { signInPhoneNumberOptions } from "@better-auth-ui/core/plugins/phone-number"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(signInPhoneNumberOptions(authClient))
```
## Params [#params]
# useSignInSocial (/docs/react/mutations/sign-in-social)
Initiates the OAuth redirect flow: the session lands once the provider redirects back, so no session-cache side effects run here.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSignInSocial } from "@better-auth-ui/react"
const { mutate: signInSocial } = useSignInSocial(authClient)
signInSocial({
provider: "github",
callbackURL: "/dashboard"
})
```
## Options factory [#options-factory]
```tsx
import { signInSocialOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(signInSocialOptions(authClient))
```
## Params [#params]
# useSignInSso (/docs/react/mutations/sign-in-sso)
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSignInSso } from "@better-auth-ui/react/plugins/sso"
const { mutate: signInSso } = useSignInSso(authClient)
signInSso({
email: "alice@example.com",
callbackURL: "/dashboard"
})
```
You can discover a provider with `email`, `domain`, `organizationSlug`, or `providerId`. Better Auth redirects successful requests to the provider.
## Options factory [#options-factory]
```tsx
import { signInSsoOptions } from "@better-auth-ui/core/plugins/sso"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(signInSsoOptions(authClient))
```
## Params [#params]
# useSignInUsername (/docs/react/mutations/sign-in-username)
Resets the session query on success so the new session is refetched.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSignInUsername } from "@better-auth-ui/react/plugins/username"
const { mutate: signInUsername } = useSignInUsername(authClient)
signInUsername({
username: "alice",
password: "hunter2"
})
```
`useSignInUsername` requires the Better Auth `username` plugin. Import your configured `authClient`. It already carries the plugin methods from `createAuthClient`.
## Options factory [#options-factory]
```tsx
import { signInUsernameOptions } from "@better-auth-ui/core/plugins/username"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(signInUsernameOptions(authClient))
```
## Params [#params]
# useSignOut (/docs/react/mutations/sign-out)
Removes every `["auth", ...]` query on success, clearing the session, linked accounts, sessions, device sessions, and passkeys in one go.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSignOut } from "@better-auth-ui/react"
const { mutate: signOut } = useSignOut(authClient, {
onSuccess: () => navigate("/")
})
signOut()
```
## Options factory [#options-factory]
```tsx
import { signOutOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(signOutOptions(authClient))
```
Note: The raw factory skips cache cleanup. Use `useSignOut` for the complete behavior.
Use the raw factory only when you need a stable options object, such as for matching a `mutationKey`.
## Params [#params]
# useSignUpEmail (/docs/react/mutations/sign-up-email)
Resets the session query on success so the new session is refetched.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSignUpEmail } from "@better-auth-ui/react"
const { mutate: signUpEmail } = useSignUpEmail(authClient)
signUpEmail({
email: "alice@example.com",
password: "hunter2",
name: "Alice"
})
```
## Options factory [#options-factory]
```tsx
import { signUpEmailOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(signUpEmailOptions(authClient))
```
## Params [#params]
# useUnlinkAccount (/docs/react/mutations/unlink-account)
Refetches the linked accounts list on success.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useUnlinkAccount } from "@better-auth-ui/react"
const { mutate: unlinkAccount } = useUnlinkAccount(authClient)
unlinkAccount({ providerId: "github", accountId: "acc_123" })
```
## Options factory [#options-factory]
```tsx
import { unlinkAccountOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(unlinkAccountOptions(authClient))
```
## Params [#params]
# useUpdateMemberRole (/docs/react/mutations/update-member-role)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useUpdateMemberRole } from "@better-auth-ui/react/plugins/organization"
const mutation = useUpdateMemberRole(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { updateMemberRoleOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(updateMemberRoleOptions(authClient))
```
## Params [#params]
# useUpdateOrganization (/docs/react/mutations/update-organization)
Organization mutations require `organizationClient()` on the Better Auth client.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useUpdateOrganization } from "@better-auth-ui/react/plugins/organization"
const mutation = useUpdateOrganization(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { updateOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/react-query"
const mutation = useMutation(updateOrganizationOptions(authClient))
```
## Params [#params]
# useUpdateUser (/docs/react/mutations/update-user)
On success, the shared mutation invalidator awaits session invalidation so cached user data refreshes.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useUpdateUser } from "@better-auth-ui/react"
const { mutate: updateUser, isPending } = useUpdateUser(authClient)
updateUser({ name: "Alice", image: "https://.../avatar.png" })
```
## Options factory [#options-factory]
```tsx
import { updateUserOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(updateUserOptions(authClient))
```
Note: prefer `useUpdateUser` for React components. Use the core factory when you need a stable framework-neutral options object, for example to match on `mutationKey` inside a global `MutationCache` observer.
## Params [#params]
# useVerifyBackupCode (/docs/react/mutations/verify-backup-code)
Each code works once: the server consumes it on success. Refetches the session.
## Usage [#usage]
```tsx
import type { TwoFactorAuthClient } from "@better-auth-ui/core/plugins/two-factor"
import { useAuth } from "@better-auth-ui/react"
import { useVerifyBackupCode } from "@better-auth-ui/react/plugins/two-factor"
const { authClient } = useAuth()
const { mutate: verifyBackupCode } = useVerifyBackupCode(
authClient as TwoFactorAuthClient
)
verifyBackupCode({ code: "a1b2-c3d4" })
```
`useVerifyBackupCode` requires the Better Auth `twoFactor` plugin. Cast `authClient` to `TwoFactorAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { verifyBackupCodeOptions } from "@better-auth-ui/core/plugins/two-factor"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(verifyBackupCodeOptions(authClient))
```
## Params [#params]
# useVerifyDeviceCode (/docs/react/mutations/verify-device-code)
Requires `deviceAuthorizationClient()` from `better-auth/client/plugins`. Verification is modeled as a mutation because it claims the code for the signed-in session, even though Better Auth exposes the underlying endpoint through `authClient.device`.
## Usage [#usage]
```tsx
import { useVerifyDeviceCode } from "@better-auth-ui/react/plugins/device-authorization"
const verifyDeviceCode = useVerifyDeviceCode(authClient)
verifyDeviceCode.mutate({
query: {
user_code: "ABCD1234"
}
})
```
The returned `status` indicates whether the request still needs approval or has already been approved or denied.
## Options factory [#options-factory]
```ts
import { verifyDeviceCodeOptions } from "@better-auth-ui/core/plugins/device-authorization"
const options = verifyDeviceCodeOptions(authClient)
```
## Params [#params]
# useVerifyEmailOtp (/docs/react/mutations/verify-email-otp)
Verification signs the user in, so the session query is refetched on success.
## Usage [#usage]
```tsx
import type { EmailOtpAuthClient } from "@better-auth-ui/core/plugins/email-otp"
import { useAuth } from "@better-auth-ui/react"
import { useVerifyEmailOtp } from "@better-auth-ui/react/plugins/email-otp"
const { authClient } = useAuth()
const { mutate: verifyEmailOtp } = useVerifyEmailOtp(
authClient as EmailOtpAuthClient
)
verifyEmailOtp({
email: "alice@example.com",
otp: "123456"
})
```
`useVerifyEmailOtp` requires the Better Auth `emailOTP` plugin. Cast `authClient` to `EmailOtpAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { verifyEmailOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(verifyEmailOtpOptions(authClient))
```
## Params [#params]
# useVerifyPhoneNumber (/docs/react/mutations/verify-phone-number)
```tsx
import {
type PhoneNumberAuthClient
} from "@better-auth-ui/core/plugins/phone-number"
import { useAuth } from "@better-auth-ui/react"
import { useVerifyPhoneNumber } from "@better-auth-ui/react/plugins/phone-number"
const { authClient } = useAuth()
const { mutate: verifyPhoneNumber } = useVerifyPhoneNumber(
authClient as PhoneNumberAuthClient
)
verifyPhoneNumber({
phoneNumber: "+12025550123",
code: "123456"
})
```
Pass `updatePhoneNumber: true` while authenticated to replace the current user's phone number. The mutation refreshes session data after success.
## Options factory [#options-factory]
```tsx
import { verifyPhoneNumberOptions } from "@better-auth-ui/core/plugins/phone-number"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(verifyPhoneNumberOptions(authClient))
```
## Params [#params]
# useVerifyTotp (/docs/react/mutations/verify-totp)
Used both to finish a pending sign-in challenge and to confirm enrollment. Verification is what creates the session, so it refetches the session query.
## Usage [#usage]
```tsx
import type { TwoFactorAuthClient } from "@better-auth-ui/core/plugins/two-factor"
import { useAuth } from "@better-auth-ui/react"
import { useVerifyTotp } from "@better-auth-ui/react/plugins/two-factor"
const { authClient } = useAuth()
const { mutate: verifyTotp } = useVerifyTotp(
authClient as TwoFactorAuthClient
)
verifyTotp({ code: "123456", trustDevice: true })
```
`useVerifyTotp` requires the Better Auth `twoFactor` plugin. Cast `authClient` to `TwoFactorAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { verifyTotpOptions } from "@better-auth-ui/core/plugins/two-factor"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(verifyTotpOptions(authClient))
```
## Params [#params]
# useVerifyTwoFactorOtp (/docs/react/mutations/verify-two-factor-otp)
Send the code with `useSendTwoFactorOtp` first. Refetches the session on success.
## Usage [#usage]
```tsx
import type { TwoFactorAuthClient } from "@better-auth-ui/core/plugins/two-factor"
import { useAuth } from "@better-auth-ui/react"
import { useVerifyTwoFactorOtp } from "@better-auth-ui/react/plugins/two-factor"
const { authClient } = useAuth()
const { mutate: verifyTwoFactorOtp } = useVerifyTwoFactorOtp(
authClient as TwoFactorAuthClient
)
verifyTwoFactorOtp({ code: "123456", trustDevice: true })
```
`useVerifyTwoFactorOtp` requires the Better Auth `twoFactor` plugin. Cast `authClient` to `TwoFactorAuthClient` so TypeScript picks up the plugin-extended method signature.
## Options factory [#options-factory]
```tsx
import { verifyTwoFactorOtpOptions } from "@better-auth-ui/core/plugins/two-factor"
import { useMutation } from "@tanstack/react-query"
const { mutate } = useMutation(verifyTwoFactorOtpOptions(authClient))
```
## Params [#params]
# useAccountInfo (/docs/react/queries/account-info)
Keyed per-user. Waits for the active session and `query.accountId` before firing.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useAccountInfo } from "@better-auth-ui/react"
const { data: accountInfo } = useAccountInfo(authClient, {
query: { accountId: "acc_123" }
})
// With React Query options in the same object
const { data: accountInfo } = useAccountInfo(authClient, {
query: { accountId: "acc_123" },
staleTime: 60_000
})
```
## Options factory [#options-factory]
```tsx
import { accountInfoOptions } from "@better-auth-ui/core"
import { useQuery } from "@tanstack/react-query"
const { data: accountInfo } = useQuery(
accountInfoOptions(authClient, userId, {
query: { accountId: "acc_123" }
})
)
```
## Invalidation [#invalidation]
```ts
import { accountInfoOptions } from "@better-auth-ui/core"
queryClient.invalidateQueries({
queryKey: accountInfoOptions(authClient, userId, {
query: { accountId: "acc_123" }
}).queryKey
})
```
## Params [#params]
# useActiveOrganization (/docs/react/queries/active-organization)
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useActiveOrganization } from "@better-auth-ui/react/plugins/organization"
const result = useActiveOrganization(authClient)
```
## Client options factory and router-loader helpers [#client-options-factory-and-router-loader-helpers]
```tsx
import {
activeOrganizationOptions,
ensureActiveOrganization,
fetchActiveOrganization,
prefetchActiveOrganization
} from "@better-auth-ui/core/plugins/organization"
const options = activeOrganizationOptions(authClient, userId)
await ensureActiveOrganization(queryClient, authClient, userId)
await prefetchActiveOrganization(queryClient, authClient, userId)
const organization = await fetchActiveOrganization(queryClient, authClient, userId)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* An explicit `query.organizationId` or `query.organizationSlug` takes precedence over the plugin slug.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For server loaders, import from `@better-auth-ui/core/plugins/organization/server`. The server helper uses your Better Auth server instance and request params so the prefetched key hydrates into `useActiveOrganization`.
```ts
import { ensureActiveOrganization } from "@better-auth-ui/core/plugins/organization/server"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
await ensureActiveOrganization(queryClient, auth, userId, {
headers: getRequestHeaders(),
query: { organizationSlug: "acme" }
})
```
## Params [#params]
# useAuthenticate (/docs/react/queries/authenticate)
`useAuthenticate` calls [`useSession`](/docs/react/queries/session) and, once the query settles, redirects unauthenticated users to the configured sign-in path. The current URL is preserved as a `redirectTo` query parameter so the user lands back where they started after signing in.
Use this as the primary guard inside protected route components.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useAuthenticate } from "@better-auth-ui/react"
export function AccountPage() {
const { data: session, isPending } = useAuthenticate(authClient)
if (isPending) return
if (!session) return null // navigating to sign-in
return
}
```
The redirect uses `basePaths.auth` + `viewPaths.auth.signIn` from `AuthProvider`: override those to target a custom sign-in view.
Accepts the same arguments as `useSession`: see its [Params](/docs/react/queries/session#params).
## First-render caveat [#first-render-caveat]
The redirect runs inside `useEffect`, so it is browser-only. During the first render, `session` is `undefined` and navigation has not started.
The protected interface can appear briefly before the browser mounts the component and redirects to sign-in.
You have two ways to handle this:
### Gate on `isPending` (simplest) [#gate-on-ispending-simplest]
Render a skeleton or spinner while the session query is pending, and `null` once it resolves to unauthenticated (the redirect is already in flight). This is what the [Usage](#usage) example does and is enough for most apps:
```tsx
const { data: session, isPending } = useAuthenticate(authClient)
if (isPending) return
if (!session) return null // navigating to sign-in
return
```
No flash of protected content, no server work required: just a brief loading state on first mount. Works identically for SSR, client-rendered, and statically prerendered routes.
### Pair with a server-side guard (no loading state) [#pair-with-a-server-side-guard-no-loading-state]
To render the protected interface immediately without a skeleton, prefetch the session on the server. Then hydrate it into the query cache:
* **Next.js**: prefetch in a server component and wrap the subtree in `HydrationBoundary`. See the [Next.js integration guide](/docs/heroui/integrations/nextjs#reactive-protection--prerendered-routes-useauthenticate).
* **TanStack Start**: check the session in `beforeLoad` on the route. See the [TanStack Start integration guide](/docs/heroui/integrations/tanstack-start#reactive-protection--prerendered-routes-useauthenticate).
The server-side check protects the first render. `useAuthenticate` redirects after token expiration, remote sign-out, or server-side session revocation.
# useFullOrganization (/docs/react/queries/full-organization)
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useFullOrganization } from "@better-auth-ui/react/plugins/organization"
const result = useFullOrganization(authClient)
```
## Client options factory and router-loader helpers [#client-options-factory-and-router-loader-helpers]
```tsx
import {
ensureFullOrganization,
fetchFullOrganization,
fullOrganizationOptions,
prefetchFullOrganization
} from "@better-auth-ui/core/plugins/organization"
const options = fullOrganizationOptions(authClient, userId)
await ensureFullOrganization(queryClient, authClient, userId)
await prefetchFullOrganization(queryClient, authClient, userId)
const organization = await fetchFullOrganization(queryClient, authClient, userId)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For server loaders, import from `@better-auth-ui/core/plugins/organization/server`. The server helper calls your Better Auth server instance directly and accepts the same request params forwarded to `auth.api.getFullOrganization`.
```ts
import { ensureFullOrganization } from "@better-auth-ui/core/plugins/organization/server"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
await ensureFullOrganization(queryClient, auth, userId, {
headers: getRequestHeaders(),
query: { organizationId: "org_123" }
})
```
## Params [#params]
# useGetApiKey (/docs/react/queries/get-api-key)
Requires the Better Auth API key plugin. This query calls the browser
`authClient.apiKey.get` endpoint and waits for the current session.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useGetApiKey } from "@better-auth-ui/react/plugins/api-key"
const { data: apiKey } = useGetApiKey(authClient, {
query: { id: apiKeyId, configId: "service" }
})
```
Omit `configId` when you use the default API key configuration.
## Options factory and loader helpers [#options-factory-and-loader-helpers]
```ts
import {
ensureGetApiKey,
fetchGetApiKey,
getApiKey,
getApiKeyOptions,
prefetchGetApiKey
} from "@better-auth-ui/core/plugins/api-key"
const options = { query: { id: apiKeyId, configId: "service" } }
getApiKeyOptions(authClient, userId, options)
await ensureGetApiKey(queryClient, authClient, userId, options)
await prefetchGetApiKey(queryClient, authClient, userId, options)
const apiKey = await fetchGetApiKey(
queryClient,
authClient,
userId,
options
)
const cachedApiKey = getApiKey(queryClient, authClient, userId, options)
```
These helpers use `authClient`, so use them in client-side loaders. Do not call
them from a trusted server loader.
## Params [#params]
# useHasPermission (/docs/react/queries/has-permission)
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useHasPermission } from "@better-auth-ui/react/plugins/organization"
const result = useHasPermission(authClient, {
permissions: { organization: ["update"] }
})
```
## Client options factory and router-loader helpers [#client-options-factory-and-router-loader-helpers]
```tsx
import {
ensureHasPermission,
fetchHasPermission,
hasPermissionOptions,
prefetchHasPermission
} from "@better-auth-ui/core/plugins/organization"
const params = {
organizationId: "org_123",
permissions: { organization: ["update"] }
}
const options = hasPermissionOptions(authClient, userId, params)
await ensureHasPermission(queryClient, authClient, userId, params)
await prefetchHasPermission(queryClient, authClient, userId, params)
const permission = await fetchHasPermission(
queryClient,
authClient,
userId,
params
)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For server loaders, import from `@better-auth-ui/core/plugins/organization/server`. Server helpers use the Better Auth server endpoint shape: pass the Better Auth server instance, signed-in `userId`, and request params with the permission payload under `body`.
```ts
import { ensureHasPermission } from "@better-auth-ui/core/plugins/organization/server"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
await ensureHasPermission(queryClient, auth, userId, {
headers: getRequestHeaders(),
body: {
organizationId: "org_123",
permissions: { organization: ["update"] }
}
})
```
## Params [#params]
# Queries (/docs/react/queries)
Every query exposes a hook and an options factory. The factory's `.queryKey` is the canonical key.
```ts
import { useSession } from "@better-auth-ui/react"
import { sessionOptions } from "@better-auth-ui/core"
```
Use the hook in components. Use the factory where TanStack Query accepts a `queryOptions` object.
Read `.queryKey` from the factory when you add data to the cache or invalidate it.
## Invalidation [#invalidation]
All keys start with `["auth", ...]`. Queries for one user use the `["auth", "user", userId, ...]` prefix.
Invalidate a prefix to control the related cache entries. Use `authQueryKeys` from `@better-auth-ui/core` to create consistent keys:
```ts
import { authQueryKeys } from "@better-auth-ui/core"
queryClient.invalidateQueries({ queryKey: authQueryKeys.all })
queryClient.invalidateQueries({ queryKey: authQueryKeys.user(userId) })
queryClient.invalidateQueries({ queryKey: authQueryKeys.session })
```
## Per-user queries [#per-user-queries]
Settings queries use a separate cache key for each user. They wait for the session before they start.
React Query uses `skipToken` until `userId` resolves. This design removes stale data after sign-out and isolates data after an account switch.
## Escape hatch [#escape-hatch]
Use `useAuthQuery` for a read endpoint that has no specific hook. Use `useAuthMutation` for a mutation endpoint.
```tsx
import { authClient } from "@/lib/auth-client"
import { useAuthQuery } from "@better-auth-ui/react"
const { data } = useAuthQuery(
authClient.magicLink.list,
["auth", "magicLink", "list"],
{ query: { limit: 20 } }
)
```
`options.query` is appended to the key, so the cached key above is `["auth", "magicLink", "list", { limit: 20 }]` (or `[..., null]` when omitted). Pass `fetchOptions` through the same options object.
For loaders, `prefetchQuery`, or `useQueries`, import the core factory directly:
```ts
import { authQueryOptions } from "@better-auth-ui/core"
authQueryOptions(authClient.magicLink.list, ["auth", "magicLink", "list"], {
query: { limit: 20 }
})
```
## Available queries [#available-queries]
### Auth [#auth]
The current authenticated session.
The current authenticated user (sugar over `useSession`).
Session query that redirects unauthenticated users to sign-in.
Public metadata for the application requesting OAuth authorization.
### Settings [#settings]
The current user's linked social accounts.
Provider-specific info for a linked account.
Active sessions (devices) for the current user.
Device sessions for the multi-session account switcher.
Passkeys registered for the current user.
API keys for the current user when the API key plugin is installed.
One API key selected by ID.
### Organization [#organization]
# useListAccounts (/docs/react/queries/list-accounts)
Keyed per-user. Waits for the active session before firing.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useListAccounts } from "@better-auth-ui/react"
const { data: accounts } = useListAccounts(authClient)
// With React Query options
const { data: accounts } = useListAccounts(authClient, { refetchOnMount: false })
```
## Options factory [#options-factory]
```tsx
import { listAccountsOptions } from "@better-auth-ui/core"
import { useQuery } from "@tanstack/react-query"
const { data: accounts } = useQuery(listAccountsOptions(authClient, userId))
```
## Invalidation [#invalidation]
```ts
import { listAccountsOptions } from "@better-auth-ui/core"
queryClient.invalidateQueries({
queryKey: listAccountsOptions(authClient, userId).queryKey
})
```
## Params [#params]
# useListApiKeys (/docs/react/queries/list-api-keys)
Requires the Better Auth API key plugin.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useListApiKeys } from "@better-auth-ui/react/plugins/api-key"
const { data: apiKeys } = useListApiKeys(authClient)
```
## Client options factory and router-loader helpers [#client-options-factory-and-router-loader-helpers]
```tsx
import {
ensureListApiKeys,
fetchListApiKeys,
listApiKeysOptions,
prefetchListApiKeys
} from "@better-auth-ui/core/plugins/api-key"
const options = listApiKeysOptions(authClient, userId)
await ensureListApiKeys(queryClient, authClient, userId)
await prefetchListApiKeys(queryClient, authClient, userId)
const apiKeys = await fetchListApiKeys(queryClient, authClient, userId)
```
## Server-side prefetching [#server-side-prefetching]
For server loaders, import from `@better-auth-ui/core/plugins/api-key/server`. These helpers call your Better Auth server instance directly, so they take `auth`, the signed-in `userId`, and request params instead of an `authClient`.
```ts
import { ensureListApiKeys } from "@better-auth-ui/core/plugins/api-key/server"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
await ensureListApiKeys(queryClient, auth, userId, {
headers: getRequestHeaders()
})
```
## Params [#params]
# useListDeviceSessions (/docs/react/queries/list-device-sessions)
Powers the multi-session account switcher: each entry represents a session currently signed in on this device.
Keyed per-user. Waits for the active session before firing.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useListDeviceSessions } from "@better-auth-ui/react/plugins/multi-session"
const { data: deviceSessions } = useListDeviceSessions(authClient)
// With React Query options
const { data: deviceSessions } = useListDeviceSessions(authClient, {
refetchOnMount: false
})
```
`useListDeviceSessions` requires the Better Auth `multiSession` plugin. Import your configured `authClient`. It already carries the plugin methods from `createAuthClient`.
## Options factory [#options-factory]
```tsx
import { listDeviceSessionsOptions } from "@better-auth-ui/core/plugins/multi-session"
import { useQuery } from "@tanstack/react-query"
const { data: deviceSessions } = useQuery(
listDeviceSessionsOptions(authClient, userId)
)
```
## Invalidation [#invalidation]
```ts
import { listDeviceSessionsOptions } from "@better-auth-ui/core/plugins/multi-session"
queryClient.invalidateQueries({
queryKey: listDeviceSessionsOptions(authClient, userId).queryKey
})
```
## Params [#params]
# useListOrganizationInvitations (/docs/react/queries/list-invitations)
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useListOrganizationInvitations } from "@better-auth-ui/react/plugins/organization"
const result = useListOrganizationInvitations(authClient)
```
## Client options factory and router-loader helpers [#client-options-factory-and-router-loader-helpers]
```tsx
import {
ensureListOrganizationInvitations,
fetchListOrganizationInvitations,
listOrganizationInvitationsOptions,
prefetchListOrganizationInvitations
} from "@better-auth-ui/core/plugins/organization"
const options = listOrganizationInvitationsOptions(authClient, userId)
await ensureListOrganizationInvitations(queryClient, authClient, userId)
await prefetchListOrganizationInvitations(queryClient, authClient, userId)
const invitations = await fetchListOrganizationInvitations(queryClient, authClient, userId)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For server loaders, import from `@better-auth-ui/core/plugins/organization/server`. Pass the Better Auth server instance, signed-in `userId`, and the request params for organization invitations.
```ts
import { ensureListOrganizationInvitations } from "@better-auth-ui/core/plugins/organization/server"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
await ensureListOrganizationInvitations(queryClient, auth, userId, {
headers: getRequestHeaders(),
query: { organizationId: "org_123" }
})
```
## Params [#params]
# useListOrganizationMembers (/docs/react/queries/list-members)
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useListOrganizationMembers } from "@better-auth-ui/react/plugins/organization"
const result = useListOrganizationMembers(authClient)
```
## Client options factory and router-loader helpers [#client-options-factory-and-router-loader-helpers]
```tsx
import {
ensureListOrganizationMembers,
fetchListOrganizationMembers,
listOrganizationMembersOptions,
prefetchListOrganizationMembers
} from "@better-auth-ui/core/plugins/organization"
const options = listOrganizationMembersOptions(authClient, userId)
await ensureListOrganizationMembers(queryClient, authClient, userId)
await prefetchListOrganizationMembers(queryClient, authClient, userId)
const members = await fetchListOrganizationMembers(queryClient, authClient, userId)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For server loaders, import from `@better-auth-ui/core/plugins/organization/server`. Pass the Better Auth server instance, signed-in `userId`, and the request params for the organization member list.
```ts
import { ensureListOrganizationMembers } from "@better-auth-ui/core/plugins/organization/server"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
await ensureListOrganizationMembers(queryClient, auth, userId, {
headers: getRequestHeaders(),
query: { organizationId: "org_123" }
})
```
## Params [#params]
# useListOAuthConsents (/docs/react/queries/list-oauth-consents)
Requires `oauthProviderClient()` from `@better-auth/oauth-provider/client`.
Returns the raw consent records Better Auth stores for the signed-in user. One OAuth client can have several records, so pair this with `groupOAuthConsents` from `@better-auth-ui/core/plugins/oauth-provider` before rendering.
## Usage [#usage]
```tsx
import { groupOAuthConsents } from "@better-auth-ui/core/plugins/oauth-provider"
import { useAuth } from "@better-auth-ui/react"
import { useListOAuthConsents } from "@better-auth-ui/react/plugins/oauth-provider"
const { authClient } = useAuth()
const { data: consents } = useListOAuthConsents(authClient)
const applications = groupOAuthConsents(consents)
```
The query key is scoped to the signed-in user, so one account's authorized applications never surface in another account's view.
## Client options factory and router-loader helpers [#client-options-factory-and-router-loader-helpers]
```tsx
import {
ensureListOAuthConsents,
fetchListOAuthConsents,
listOAuthConsentsOptions,
prefetchListOAuthConsents
} from "@better-auth-ui/core/plugins/oauth-provider"
const options = listOAuthConsentsOptions(authClient, userId)
await ensureListOAuthConsents(queryClient, authClient, userId)
await prefetchListOAuthConsents(queryClient, authClient, userId)
const consents = await fetchListOAuthConsents(queryClient, authClient, userId)
```
## Invalidation [#invalidation]
```ts
import { oauthProviderQueryKeys } from "@better-auth-ui/core/plugins/oauth-provider"
queryClient.invalidateQueries({
queryKey: oauthProviderQueryKeys.consents(userId)
})
```
[`useDeleteOAuthConsent`](/docs/react/mutations/delete-oauth-consent) already awaits this invalidation, so a partially failed removal still ends up reflecting the server's state.
## Params [#params]
# useListOrganizations (/docs/react/queries/list-organizations)
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useListOrganizations } from "@better-auth-ui/react/plugins/organization"
const result = useListOrganizations(authClient)
```
## Client options factory and router-loader helpers [#client-options-factory-and-router-loader-helpers]
```tsx
import {
ensureListOrganizations,
fetchListOrganizations,
listOrganizationsOptions,
prefetchListOrganizations
} from "@better-auth-ui/core/plugins/organization"
const options = listOrganizationsOptions(authClient, userId)
await ensureListOrganizations(queryClient, authClient, userId)
await prefetchListOrganizations(queryClient, authClient, userId)
const organizations = await fetchListOrganizations(queryClient, authClient, userId)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For server loaders, import from `@better-auth-ui/core/plugins/organization/server`. The server helper uses your Better Auth server instance and the signed-in `userId` so the cache key stays partitioned per user.
```ts
import { ensureListOrganizations } from "@better-auth-ui/core/plugins/organization/server"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
await ensureListOrganizations(queryClient, auth, userId, {
headers: getRequestHeaders()
})
```
## Params [#params]
# useListPasskeys (/docs/react/queries/list-passkeys)
Keyed per-user. Waits for the active session before firing.
The Better Auth client method is `authClient.passkey.listUserPasskeys`, while the corresponding server endpoint is `auth.api.listPasskeys`. The names diverge in Better Auth itself: this is not a typo. We expose a single unified `listPasskeys` API on top (hooks, options, helpers all drop the redundant `User` infix).
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useListPasskeys } from "@better-auth-ui/react/plugins/passkey"
const { data: passkeys } = useListPasskeys(authClient)
// With React Query options
const { data: passkeys } = useListPasskeys(authClient, {
refetchOnMount: false
})
```
`useListPasskeys` requires the Better Auth `passkey` plugin. Import your configured `authClient`. It already carries the plugin methods from `createAuthClient`.
## Options factory [#options-factory]
```tsx
import { listPasskeysOptions } from "@better-auth-ui/core/plugins/passkey"
import { useQuery } from "@tanstack/react-query"
const { data: passkeys } = useQuery(
listPasskeysOptions(authClient, userId)
)
```
## Invalidation [#invalidation]
```ts
import { listPasskeysOptions } from "@better-auth-ui/core/plugins/passkey"
queryClient.invalidateQueries({
queryKey: listPasskeysOptions(authClient, userId).queryKey
})
```
## Params [#params]
# useListSessions (/docs/react/queries/list-sessions)
Keyed per-user. Waits for the active session before firing.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useListSessions } from "@better-auth-ui/react"
const { data: sessions } = useListSessions(authClient)
// With React Query options
const { data: sessions } = useListSessions(authClient, { refetchOnMount: false })
```
## Options factory [#options-factory]
```tsx
import { listSessionsOptions } from "@better-auth-ui/core"
import { useQuery } from "@tanstack/react-query"
const { data: sessions } = useQuery(listSessionsOptions(authClient, userId))
```
## Invalidation [#invalidation]
```ts
import { listSessionsOptions } from "@better-auth-ui/core"
queryClient.invalidateQueries({
queryKey: listSessionsOptions(authClient, userId).queryKey
})
```
## Params [#params]
# useListUserInvitations (/docs/react/queries/list-user-invitations)
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useListUserInvitations } from "@better-auth-ui/react/plugins/organization"
const result = useListUserInvitations(authClient)
```
## Client options factory and router-loader helpers [#client-options-factory-and-router-loader-helpers]
```tsx
import {
ensureListUserInvitations,
fetchListUserInvitations,
listUserInvitationsOptions,
prefetchListUserInvitations
} from "@better-auth-ui/core/plugins/organization"
const options = listUserInvitationsOptions(authClient, userId)
await ensureListUserInvitations(queryClient, authClient, userId)
await prefetchListUserInvitations(queryClient, authClient, userId)
const invitations = await fetchListUserInvitations(queryClient, authClient, userId)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For server loaders, import from `@better-auth-ui/core/plugins/organization/server`. Pass the Better Auth server instance and signed-in `userId`. Request headers are forwarded to Better Auth for session-aware calls.
```ts
import { ensureListUserInvitations } from "@better-auth-ui/core/plugins/organization/server"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
await ensureListUserInvitations(queryClient, auth, userId, {
headers: getRequestHeaders()
})
```
## Params [#params]
# usePublicOAuthClient (/docs/react/queries/public-oauth-client)
Requires `oauthProviderClient()` from `@better-auth/oauth-provider/client`. Read the client ID from the signed authorization query and leave the complete query string in the browser URL.
## Usage [#usage]
```tsx
import { parseOAuthAuthorizationRequest } from "@better-auth-ui/core/plugins/oauth-provider"
import { usePublicOAuthClient } from "@better-auth-ui/react/plugins/oauth-provider"
const { clientId } = parseOAuthAuthorizationRequest(window.location.search)
const client = usePublicOAuthClient(authClient, clientId)
```
The query remains disabled until `clientId` is available.
## Options factory and loader helpers [#options-factory-and-loader-helpers]
```ts
import {
ensurePublicOAuthClient,
fetchPublicOAuthClient,
prefetchPublicOAuthClient,
publicOAuthClientOptions
} from "@better-auth-ui/core/plugins/oauth-provider"
const options = publicOAuthClientOptions(authClient, clientId)
await ensurePublicOAuthClient(queryClient, authClient, clientId)
await prefetchPublicOAuthClient(queryClient, authClient, clientId)
const client = await fetchPublicOAuthClient(queryClient, authClient, clientId)
```
## Params [#params]
# useSession (/docs/react/queries/session)
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useSession } from "@better-auth-ui/react"
// Most common
const { data: session } = useSession(authClient)
// React Query options
const { data: session } = useSession(authClient, { staleTime: 30_000 })
// Better Auth params
const { data: session } = useSession(authClient, {
query: { disableCookieCache: true }
})
// Both in one object
const { data: session } = useSession(authClient, {
query: { disableCookieCache: true },
staleTime: 30_000
})
```
## Options factory [#options-factory]
```tsx
import { sessionOptions } from "@better-auth-ui/core"
import { useQuery } from "@tanstack/react-query"
const { data: session } = useQuery(sessionOptions(authClient))
```
## Server-side prefetching [#server-side-prefetching]
For client and router loaders that already have an `authClient`, use the helpers from `@better-auth-ui/core`.
```ts
import { ensureSession, fetchSession, prefetchSession } from "@better-auth-ui/core"
await ensureSession(queryClient, authClient)
await prefetchSession(queryClient, authClient)
const session = await fetchSession(queryClient, authClient)
```
For server loaders, use `@better-auth-ui/core/server`. These helpers call your Better Auth server instance directly. Pass the request headers required by `auth.api.getSession`.
```ts
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
const session = await ensureSessionServer(queryClient, auth, {
headers: getRequestHeaders()
})
```
Both entrypoints share the same session query key, so server-prefetched data hydrates into client-side `useSession`.
## Invalidation [#invalidation]
```ts
import { sessionOptions } from "@better-auth-ui/core"
queryClient.invalidateQueries({
queryKey: sessionOptions(authClient).queryKey
})
```
The shared key factory provides the same query key:
```ts
import { authQueryKeys } from "@better-auth-ui/core"
queryClient.invalidateQueries({ queryKey: authQueryKeys.session })
```
## Client params [#client-params]
## Server params [#server-params]
# useUser (/docs/react/queries/user)
Thin wrapper over [`useSession`](/docs/react/queries/session) that returns `session.user` as `data`. Shares the session cache entry, so no extra request is made.
## Usage [#usage]
```tsx
import { authClient } from "@/lib/auth-client"
import { useUser } from "@better-auth-ui/react"
const { data: user } = useUser(authClient)
```
Accepts the same arguments as `useSession`: see its [Params](/docs/react/queries/session#params).
# SSR (/docs/react/ssr)
Every Better Auth UI hook uses [TanStack Query](https://tanstack.com/query). A shared `QueryClient` lets the application prefetch sessions, protect routes, and hydrate the browser cache.
The examples use [TanStack Start](https://tanstack.com/start) and match [`examples/start-heroui-example`](https://github.com/better-auth-ui/better-auth-ui/tree/main/examples/start-heroui-example). Other React applications can use the same `QueryClient` pattern with different router configuration.
## Install the SSR integration [#install-the-ssr-integration]
npm
pnpm
yarn
bun
```bash
npm install @tanstack/react-query @tanstack/react-router-ssr-query
```
```bash
pnpm add @tanstack/react-query @tanstack/react-router-ssr-query
```
```bash
yarn add @tanstack/react-query @tanstack/react-router-ssr-query
```
```bash
bun add @tanstack/react-query @tanstack/react-router-ssr-query
```
`@tanstack/react-router-ssr-query` dehydrates the `QueryClient` on the server. It sends that state with the HTML and rehydrates it in the browser.
The package also wraps the application in `QueryClientProvider`. You do not need another provider.
## Customize the QueryClient [#customize-the-queryclient]
Create the `QueryClient` inside the router factory. Each SSR request then receives a separate cache.
Apply `defaultOptions` in this factory. The example uses a `staleTime` of 5 seconds to reuse recent results during navigation.
```tsx title="src/router.tsx"
import { QueryClient } from "@tanstack/react-query"
import { createRouter } from "@tanstack/react-router"
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query"
import { routeTree } from "./routeTree.gen"
export const getRouter = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5000
}
}
})
const router = createRouter({
routeTree,
scrollRestoration: true,
defaultPreloadStaleTime: 0,
context: { queryClient }
})
setupRouterSsrQueryIntegration({
router,
queryClient
})
return router
}
```
The options have these effects:
* `defaultPreloadStaleTime: 0` tells TanStack Router to start loaders during each preload. Query `staleTime` can still reuse cached data.
* `context: { queryClient }` exposes the client to every loader/`beforeLoad` hook via `context.queryClient`.
* Call `setupRouterSsrQueryIntegration` **after** `createRouter`. It uses router events to dehydrate and hydrate data during navigation.
Configure `defaultOptions` as you do in other TanStack Query applications. For example, disable refetch on window focus with this option:
```ts
new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
refetchOnWindowFocus: false
}
}
})
```
## Type the root route context [#type-the-root-route-context]
Use `createRootRouteWithContext` so child routes can read `context.queryClient` with full typing.
```tsx title="src/routes/__root.tsx"
import type { QueryClient } from "@tanstack/react-query"
import { createRootRouteWithContext } from "@tanstack/react-router"
export const Route = createRootRouteWithContext<{
queryClient: QueryClient
}>()({
// ...head, shellComponent, etc.
})
```
The tree of routes now has `{ queryClient }` available in every loader, `beforeLoad`, and component via `Route.useRouteContext()`.
## Prefetch the session in `beforeLoad` [#prefetch-the-session-in-beforeload]
Each `@better-auth-ui/react` query provides matching `ensure*`, `prefetch*`, and `fetch*` helpers. These helpers accept `authClient`, `QueryClient`, and the query parameters.
| Helper | When to use |
| ----------------- | ------------------------------------------------------------------------ |
| `ensureSession` | Read the session, resolving from cache if fresh. Most common in loaders. |
| `prefetchSession` | Kick off a background fetch without awaiting. Good for soft preloads. |
| `fetchSession` | Always bypass the cache and fetch fresh data. |
Use `ensureSession` in `beforeLoad` to protect a route. If it returns no session, redirect the user to sign-in.
Preserve the current URL in the redirect. The application can return the user to this URL after sign-in.
```tsx title="src/routes/settings/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Settings } from "@better-auth-ui/heroui"
import { ensureSession } from "@better-auth-ui/core"
import { createFileRoute, notFound, redirect } from "@tanstack/react-router"
import { authClient } from "@/lib/auth-client"
export const Route = createFileRoute("/settings/$path")({
async beforeLoad({ params: { path }, context: { queryClient }, location }) {
if (!Object.values(viewPaths.settings).includes(path)) {
throw notFound()
}
const session = await ensureSession(queryClient, authClient)
if (!session) {
throw redirect({
to: "/auth/$path",
params: { path: "sign-in" },
search: { redirectTo: location.href }
})
}
return { user: session.user }
},
component: SettingsPage
})
function SettingsPage() {
const { path } = Route.useParams()
const { user } = Route.useRouteContext()
return (
)
}
```
The server adds the session to the query cache. As a result, `Settings` and its `useSession` calls read it during the first render.
TanStack Router adds the value from `beforeLoad` to the route context. Read this value in a component with `Route.useRouteContext()`.
This pattern avoids another query for the same user data.
## Prefetch without blocking [#prefetch-without-blocking]
Use `prefetchSession` to fill the cache without blocking navigation. This pattern can prepare a public route that later shows an authenticated widget:
```ts
import { prefetchSession } from "@better-auth-ui/core"
export const Route = createFileRoute("/")({
loader: ({ context: { queryClient } }) => {
void prefetchSession(queryClient, authClient)
}
})
```
The same pattern works for each settings query. The related prefetch helpers and options factories are available from `@better-auth-ui/core`.
## Server-only helpers [#server-only-helpers]
To avoid an HTTP request, import the session helpers from `@better-auth-ui/core/server`. Call them from a server function or another server runtime.
These helpers accept the Better Auth server instance instead of `authClient`:
```ts title="src/lib/session.ts"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import type { QueryClient } from "@tanstack/react-query"
import { createServerFn } from "@tanstack/react-start"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
const getSession = createServerFn().handler(() =>
auth.api.getSession({ headers: getRequestHeaders() })
)
export const ensureServerSession = (queryClient: QueryClient) =>
ensureSessionServer(queryClient, auth, { headers: getRequestHeaders() })
```
`@better-auth-ui/core/server` exports `sessionOptionsServer`, `ensureSessionServer`, `prefetchSessionServer`, and `fetchSessionServer`. They accept the Better Auth server instance and request parameters.
Available `@better-auth-ui/core/server` helper families include:
* Session: `sessionOptionsServer`, `ensureSessionServer`, `prefetchSessionServer`, `fetchSessionServer`
* Settings: `listAccountsOptions`, `ensureListAccounts`, `prefetchListAccounts`, `fetchListAccounts`, `accountInfoOptions`, `ensureAccountInfo`, `listSessionsOptions`, and `ensureListSessions` with matching prefetch/fetch helpers
Plugin server query helper families live under their plugin server entrypoints:
* API-key (`@better-auth-ui/core/plugins/api-key/server`): `listApiKeysOptions`, `ensureListApiKeys`, `prefetchListApiKeys`, `fetchListApiKeys`
* Multi-session (`@better-auth-ui/core/plugins/multi-session/server`): `listDeviceSessionsOptions`, `ensureListDeviceSessions`, `prefetchListDeviceSessions`, `fetchListDeviceSessions`
* Passkey (`@better-auth-ui/core/plugins/passkey/server`): `listPasskeysOptions`, `ensureListPasskeys`, `prefetchListPasskeys`, `fetchListPasskeys`
* Organization (`@better-auth-ui/core/plugins/organization/server`): `activeOrganizationOptions`, `ensureActiveOrganization`, `prefetchActiveOrganization`, `fetchActiveOrganization`, `fullOrganizationOptions`, `ensureFullOrganization`, `listOrganizationsOptions`, `ensureListOrganizations`, `listOrganizationMembersOptions`, `ensureListOrganizationMembers`, `listOrganizationInvitationsOptions`, `ensureListOrganizationInvitations`, `listUserInvitationsOptions`, `ensureListUserInvitations`, `hasPermissionOptions`, and `ensureHasPermission` with matching prefetch/fetch helpers
Server and client helpers use the same cache keys. Therefore, browser hooks such as `useSession` can read data that the server prefetched.
# (/docs/shadcn/components/auth/auth-redirect)
`` powers the `/auth/redirect` view. It checks the current
session, then continues to the `redirectTo` query parameter.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/auth-redirect
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/auth-redirect
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/auth-redirect
```
```bash
bun x shadcn@latest add @better-auth-ui/auth-redirect
```
Route it through the unified auth component:
```tsx
import { Auth } from "@/components/auth/auth"
export default function AuthPage({ path }: { path: string }) {
return
}
```
Open the view with an encoded, same-origin destination:
```text
/auth/redirect?redirectTo=%2Fsettings%2Faccount
```
Authenticated users continue immediately. Signed-out users go to sign in and
return to the redirect view after authentication. The final redirect uses a
full-page request, so the destination can be an API callback.
Only root-relative paths and same-origin HTTP(S) URLs are accepted. Unsafe,
cross-origin, malformed, or self-referencing targets fall back to `/`.
## Use with account deletion emails [#use-with-account-deletion-emails]
Better Auth requires the user who follows a deletion link to have a matching
session. Wrap the generated deletion URL with the redirect view before sending
the email:
```ts
import { betterAuth } from "better-auth"
export const auth = betterAuth({
user: {
deleteUser: {
enabled: true,
sendDeleteAccountVerification: async ({ user, url }) => {
const appURL = new URL(process.env.BETTER_AUTH_URL!)
const deleteURL = new URL(url)
const redirectURL = new URL("/auth/redirect", appURL)
redirectURL.searchParams.set(
"redirectTo",
`${deleteURL.pathname}${deleteURL.search}${deleteURL.hash}`,
)
await sendDeleteAccountEmail({
to: user.email,
url: redirectURL.toString(),
})
},
},
},
})
```
The Better Auth callback and the redirect view must share an origin.
## Props [#props]
# (/docs/shadcn/components/auth/auth)
## Usage [#usage]
```tsx file=/src/demos/shadcn/auth/auth.tsx
import { Auth } from "@/components/auth/auth"
export function AuthDemo() {
return
}
```
### Built-in views [#built-in-views]
| `view` | Default path |
| ---------------- | ----------------------- |
| `callback` | `/auth/callback` |
| `error` | `/auth/error` |
| `redirect` | `/auth/redirect` |
| `signIn` | `/auth/sign-in` |
| `signUp` | `/auth/sign-up` |
| `signOut` | `/auth/sign-out` |
| `forgotPassword` | `/auth/forgot-password` |
| `resetPassword` | `/auth/reset-password` |
| `resetLinkSent` | `/auth/reset-link-sent` |
| `verifyEmail` | `/auth/verify-email` |
Registered plugins can contribute more views through their own `viewPaths.auth`, such as `magicLink` and `magicLinkSent`.
### Callback results [#callback-results]
Set Better Auth `onAPIError.errorURL` and each social sign-in `errorCallbackURL` to `/auth/error`. The view reads Better Auth's `error` query parameter and gives the user a suitable recovery action.
Use `/auth/callback?result=email_verified` as an email verification callback. The result view also supports `account_linked`, `password_reset`, `signup_complete`, and `cancelled`. Add `flow=email-verification`, `account-linking`, `password-reset`, or `oauth` when `result=success` needs more context. A local `redirectTo` path adds a Continue action.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/auth
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/auth
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/auth
```
```bash
bun x shadcn@latest add @better-auth-ui/auth
```
## Props [#props]
# (/docs/shadcn/components/auth/forgot-password)
## Usage [#usage]
```tsx file=/src/demos/shadcn/auth/forgot-password.tsx
import { ForgotPassword } from "@/components/auth/forgot-password"
export function ForgotPasswordDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/forgot-password
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/forgot-password
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/forgot-password
```
```bash
bun x shadcn@latest add @better-auth-ui/forgot-password
```
## Props [#props]
# (/docs/shadcn/components/auth/reset-link-sent)
`` renders this view at `/auth/reset-link-sent` after `` successfully requests a reset link. It reads the submitted email from session storage, shows an email-provider shortcut when one is available, and links back to sign-in.
Hover or focus the email-provider button to show a QR code for the same provider URL. Users can scan it when their email account is available on another device.
## Installation [#installation]
The view is included with the forgot-password registry entry.
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/forgot-password
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/forgot-password
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/forgot-password
```
```bash
bun x shadcn@latest add @better-auth-ui/forgot-password
```
## Usage [#usage]
```tsx
import { ResetLinkSent } from "@/components/auth/reset-link-sent"
```
## Props [#props]
# (/docs/shadcn/components/auth/reset-password)
## Usage [#usage]
```tsx file=/src/demos/shadcn/auth/reset-password.tsx
import { ResetPassword } from "@/components/auth/reset-password"
export function ResetPasswordDemo() {
return
}
```
New-password and confirmation fields each provide a localized show/hide control while preserving their current values.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/reset-password
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/reset-password
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/reset-password
```
```bash
bun x shadcn@latest add @better-auth-ui/reset-password
```
## Props [#props]
# (/docs/shadcn/components/auth/sign-in)
## Usage [#usage]
```tsx file=/src/demos/shadcn/auth/sign-in.tsx
import { SignIn } from "@/components/auth/sign-in"
export function SignInDemo() {
return
}
```
The password starts masked. Its localized show/hide button changes only the field presentation, so the submitted password value stays unchanged.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/sign-in
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/sign-in
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/sign-in
```
```bash
bun x shadcn@latest add @better-auth-ui/sign-in
```
## Props [#props]
# (/docs/shadcn/components/auth/sign-out)
## Usage [#usage]
```tsx file=/src/demos/shadcn/auth/sign-out.tsx
import { SignOut } from "@/components/auth/sign-out"
export function SignOutDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/sign-out
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/sign-out
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/sign-out
```
```bash
bun x shadcn@latest add @better-auth-ui/sign-out
```
## Props [#props]
# (/docs/shadcn/components/auth/sign-up)
## Usage [#usage]
```tsx file=/src/demos/shadcn/auth/sign-up.tsx
import { SignUp } from "@/components/auth/sign-up"
export function SignUpDemo() {
return
}
```
Password and confirmation fields each provide a localized show/hide control while preserving their current values.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/sign-up
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/sign-up
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/sign-up
```
```bash
bun x shadcn@latest add @better-auth-ui/sign-up
```
## Props [#props]
# (/docs/shadcn/components/auth/verify-email)
Hover or focus the email-provider button to show a QR code for the same provider URL. Users can scan it when their email account is available on another device.
## Usage [#usage]
```tsx file=/src/demos/shadcn/auth/verify-email.tsx
import { VerifyEmail } from "@/components/auth/verify-email"
export function VerifyEmailDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/verify-email
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/verify-email
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/verify-email
```
```bash
bun x shadcn@latest add @better-auth-ui/verify-email
```
## Props [#props]
# (/docs/shadcn/components/auth-provider)
## Usage [#usage]
```tsx file=/../../examples/start-shadcn-example/src/components/providers.tsx
import { Link, useNavigate, useParams } from "@tanstack/react-router"
import { ThemeProvider, useTheme } from "next-themes"
import type { ReactNode } from "react"
import { apiKeyPlugin } from "@/lib/auth/api-key-plugin"
import { deleteUserPlugin } from "@/lib/auth/delete-user-plugin"
import { emailOtpPlugin } from "@/lib/auth/email-otp-plugin"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin"
import { multiSessionPlugin } from "@/lib/auth/multi-session-plugin"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
import { passkeyPlugin } from "@/lib/auth/passkey-plugin"
import { themePlugin } from "@/lib/auth/theme-plugin"
import { twoFactorPlugin } from "@/lib/auth/two-factor-plugin"
import { usernamePlugin } from "@/lib/auth/username-plugin"
import { authClient } from "@/lib/auth-client"
import { AuthProvider } from "./auth/auth-provider"
import { Toaster } from "./ui/sonner"
export function Providers({ children }: { children: ReactNode }) {
const navigate = useNavigate()
const { slug } = useParams({ strict: false })
return (
}
>
{children}
)
}
```
## Localization [#localization]
Install the locale package:
```bash
bun add @better-auth-ui/locales
```
Import one locale and pass it to `AuthProvider`:
```tsx title="components/providers.tsx"
import { deDE } from "@better-auth-ui/locales/de-DE"
{children}
```
Locale bundles include the core messages and all built-in plugin messages.
Use `localization` for product-specific text. These values take priority over the selected locale:
```tsx
{children}
```
### Match the browser language [#match-the-browser-language]
In a client-only application, import the supported locales and match `navigator.languages` against that list:
```tsx
import { matchAuthLocale } from "@better-auth-ui/locales"
import { deDE } from "@better-auth-ui/locales/de-DE"
import { enUS } from "@better-auth-ui/locales/en-US"
const locale = matchAuthLocale({
requested: navigator.languages,
supported: [enUS, deDE],
fallback: enUS
})
```
For server rendering, resolve the same locale from a user preference or the `Accept-Language` header. Pass that locale during the first render to prevent a hydration mismatch.
Changing the `locale` prop updates mounted auth components. Email components do not read `AuthProvider`; pass their localization on the server.
## Custom and Generic OAuth providers [#custom-and-generic-oauth-providers]
Built-in providers use their Better Auth ID as a string. For a custom or Generic OAuth provider, pass its ID, visible label, and optional icon.
```tsx title="components/providers.tsx"
import { Building2 } from "lucide-react"
}
]}
>
{children}
```
The same metadata appears on sign-in, sign-up, and linked-account views. BAUI sends only `id` to Better Auth.
Better Auth 1.7 registers Generic OAuth providers as normal social providers. Configure the same ID on the server:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { genericOAuth } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
genericOAuth({
config: [
{
providerId: "company-oauth",
clientId: process.env.COMPANY_OAUTH_CLIENT_ID!,
clientSecret: process.env.COMPANY_OAUTH_CLIENT_SECRET!,
discoveryUrl:
"https://id.example.com/.well-known/openid-configuration"
}
]
})
]
})
```
Register `/api/auth/callback/company-oauth` with the provider. See the [Better Auth Generic OAuth guide](https://www.better-auth.com/docs/plugins/generic-oauth) for endpoint and profile options.
## Popup social sign-in [#popup-social-sign-in]
Set `socialSignInMode="popup"` to keep the current page open during social sign-in. Redirect mode remains the default.
Better Auth marks this API as experimental. Configure the server and client plugins before you enable it:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { bearer, oauthPopup } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [bearer(), oauthPopup()]
})
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { oauthPopupClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [oauthPopupClient()]
})
```
Then select popup mode on the provider:
```tsx
{children}
```
Popup mode uses the same provider buttons and redirect target. It returns control to the current page, refreshes the session, and then runs the configured navigation.
## Props [#props]
# (/docs/shadcn/components/email/change-email-confirmation-email)
## Usage [#usage]
```tsx file=/src/demos/shadcn/email/change-email-confirmation-email.tsx#L13-
import { ChangeEmailConfirmationEmail } from "@better-auth-ui/react/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Better Auth setup [#better-auth-setup]
Render this template from `user.changeEmail.sendChangeEmailConfirmation` and send it to `user.email`. Better Auth supplies the approval URL and requested address.
```tsx
sendChangeEmailConfirmation: async ({ user, newEmail, url }) => {
const html = await render(
)
await sendEmail({
to: user.email,
subject: "Approve your email change",
html
})
}
```
Use `` separately if you also send a notification after the address has changed.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/change-email-confirmation-email
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/change-email-confirmation-email
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/change-email-confirmation-email
```
```bash
bun x shadcn@latest add @better-auth-ui/change-email-confirmation-email
```
## Props [#props]
## Features [#features]
* Shows the current and requested email addresses
* Includes an approval button and fallback URL
* Explains that ignoring the message leaves the address unchanged
* Supports expiration details, theming, branding, and localization
# (/docs/shadcn/components/email/delete-account-verification-email)
## Usage [#usage]
```tsx file=/src/demos/shadcn/email/delete-account-verification-email.tsx#L13-
import { DeleteAccountVerificationEmail } from "@better-auth-ui/react/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Better Auth setup [#better-auth-setup]
Render this template from `user.deleteUser.sendDeleteAccountVerification`. Better Auth supplies the verification URL and user whose account is being deleted.
```tsx
sendDeleteAccountVerification: async ({ user, url }) => {
const html = await render(
)
await sendEmail({
to: user.email,
subject: "Confirm account deletion",
html
})
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/delete-account-verification-email
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/delete-account-verification-email
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/delete-account-verification-email
```
```bash
bun x shadcn@latest add @better-auth-ui/delete-account-verification-email
```
## Props [#props]
## Features [#features]
* Clearly states that account deletion is permanent
* Includes a verification button and fallback URL
* Explains that ignoring the message keeps the account active
* Supports expiration details, theming, branding, and localization
# (/docs/shadcn/components/email/email-changed-email)
## Usage [#usage]
```tsx file=/src/demos/shadcn/email/email-changed-email.tsx#L13-
import { EmailChangedEmail } from "@better-auth-ui/react/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/email-changed-email
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/email-changed-email
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/email-changed-email
```
```bash
bun x shadcn@latest add @better-auth-ui/email-changed-email
```
## Props [#props]
## Features [#features]
* Email change notification
* Shows previous and new email addresses
* Revert action button if unauthorized
* Support contact information
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/shadcn/components/email/email-verification-email)
## Usage [#usage]
```tsx file=/src/demos/shadcn/email/email-verification-email.tsx#L13-
import { EmailVerificationEmail } from "@better-auth-ui/react/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/email-verification-email
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/email-verification-email
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/email-verification-email
```
```bash
bun x shadcn@latest add @better-auth-ui/email-verification-email
```
## Props [#props]
## Features [#features]
* Verification button and fallback URL
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/shadcn/components/email/magic-link-email)
## Usage [#usage]
```tsx file=/src/demos/shadcn/email/magic-link-email.tsx#L13-
import { MagicLinkEmail } from "@better-auth-ui/react/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/magic-link-email
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/magic-link-email
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/magic-link-email
```
```bash
bun x shadcn@latest add @better-auth-ui/magic-link-email
```
## Props [#props]
## Features [#features]
* Sign-in button with magic link
* Fallback URL for manual copy/paste
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/shadcn/components/email/new-device-email)
## Usage [#usage]
```tsx file=/src/demos/shadcn/email/new-device-email.tsx#L13-
import { NewDeviceEmail } from "@better-auth-ui/react/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/new-device-email
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/new-device-email
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/new-device-email
```
```bash
bun x shadcn@latest add @better-auth-ui/new-device-email
```
## Props [#props]
## Features [#features]
* Device information display (browser, OS, location, IP)
* Timestamp of the sign-in
* Security action button if unauthorized
* Support contact information
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/shadcn/components/email/organization-invitation-email)
## Usage [#usage]
```tsx file=/src/demos/shadcn/email/organization-invitation-email.tsx#L13-
import { OrganizationInvitationEmail } from "@better-auth-ui/react/email"
import { render } from "@react-email/render"
const html = await render(
// biome-ignore lint/a11y/useValidAriaRole: `role` is a prop on the email component, not an ARIA role.
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/organization-invitation-email
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/organization-invitation-email
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/organization-invitation-email
```
```bash
bun x shadcn@latest add @better-auth-ui/organization-invitation-email
```
## Server setup [#server-setup]
Wire the email into the Better Auth `organization` plugin via `sendInvitationEmail`. Point `url` at the direct invitation view registered by `organizationPlugin()`.
```tsx title="auth.ts"
import { OrganizationInvitationEmail } from "@better-auth-ui/react/email"
import { render } from "@react-email/render"
import { betterAuth } from "better-auth"
import { organization } from "better-auth/plugins"
const baseUrl = process.env.BETTER_AUTH_URL!
export const auth = betterAuth({
plugins: [
organization({
async sendInvitationEmail(data) {
const html = await render(
)
await sendEmail({
to: data.email,
subject: `You're invited to ${data.organization.name}`,
html
})
}
})
]
})
```
Add `organizationPlugin().viewPaths.auth.acceptInvitation` to your auth
route allow-list. Use `{baseUrl}/settings/organizations` only when you want
the invitation email to open the full pending-invitations list instead.
## Props [#props]
## Features [#features]
* Inviter name and email display
* Organization name and optional organization logo
* Role being offered (for example member, admin, owner)
* Accept invitation button linking to the direct invitation view
* Fallback URL for manual copy/paste
* Optional expiration time
* Security notice for unexpected invitations
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/shadcn/components/email/otp-email)
## Usage [#usage]
```tsx file=/src/demos/shadcn/email/otp-email.tsx#L13-
import { OtpEmail } from "@better-auth-ui/react/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/otp-email
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/otp-email
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/otp-email
```
```bash
bun x shadcn@latest add @better-auth-ui/otp-email
```
## Props [#props]
## Features [#features]
* Large, prominently displayed verification code
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/shadcn/components/email/password-changed-email)
## Usage [#usage]
```tsx file=/src/demos/shadcn/email/password-changed-email.tsx#L13-
import { PasswordChangedEmail } from "@better-auth-ui/react/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/password-changed-email
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/password-changed-email
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/password-changed-email
```
```bash
bun x shadcn@latest add @better-auth-ui/password-changed-email
```
## Props [#props]
## Features [#features]
* Password change notification
* Timestamp of the change
* Security action button if unauthorized
* Support contact information
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/shadcn/components/email/reset-password-email)
## Usage [#usage]
```tsx file=/src/demos/shadcn/email/reset-password-email.tsx#L13-
import { ResetPasswordEmail } from "@better-auth-ui/react/email"
import { render } from "@react-email/render"
const html = await render(
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/reset-password-email
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/reset-password-email
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/reset-password-email
```
```bash
bun x shadcn@latest add @better-auth-ui/reset-password-email
```
## Props [#props]
## Features [#features]
* Password reset button and fallback URL
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/shadcn/components/settings/account/account-settings)
## Usage [#usage]
```tsx file=/src/demos/shadcn/settings/account/account-settings.tsx
import { AccountSettings } from "@/components/auth/settings/account/account-settings"
export function AccountSettingsDemo() {
return (
)
}
```
Current, new, and confirmation password fields provide independent localized show/hide controls.
When a user without a credential account requests a set-password email, the email-provider button appears after the request succeeds. Hover or focus it to show a QR code for opening the same provider URL on another device.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/change-password
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/change-password
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/change-password
```
```bash
bun x shadcn@latest add @better-auth-ui/change-password
```
## Props [#props]
# (/docs/shadcn/components/settings/security/linked-accounts)
## Usage [#usage]
```tsx file=/src/demos/shadcn/settings/security/linked-accounts.tsx
import { LinkedAccounts } from "@/components/auth/settings/security/linked-accounts"
export function LinkedAccountsDemo() {
return (
)
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/security-settings
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/security-settings
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/security-settings
```
```bash
bun x shadcn@latest add @better-auth-ui/security-settings
```
## Props [#props]
## Fresh-session checks [#fresh-session-checks]
Sensitive session operations can return `SESSION_NOT_FRESH`. The active sessions card shows an inline password prompt for this response. After the user signs in again, it retries the session query. If password sign-in is disabled, the prompt links to the configured sign-in route.
# (/docs/shadcn/components/settings/settings)
## Usage [#usage]
```tsx file=/src/demos/shadcn/settings/settings.tsx
import { Settings } from "@/components/auth/settings/settings"
export function SettingsDemo() {
return (
)
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/settings
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/settings
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/settings
```
```bash
bun x shadcn@latest add @better-auth-ui/settings
```
## Props [#props]
# (/docs/shadcn/components/user/user-avatar)
## Usage [#usage]
```tsx file=/src/demos/shadcn/user/user-avatar.tsx
import { UserAvatar } from "@/components/auth/user/user-avatar"
export function UserAvatarDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/user-avatar
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/user-avatar
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/user-avatar
```
```bash
bun x shadcn@latest add @better-auth-ui/user-avatar
```
## Props [#props]
# (/docs/shadcn/components/user/user-button)
## Usage [#usage]
```tsx file=/src/demos/shadcn/user/user-button.tsx
import { UserButton } from "@/components/auth/user/user-button"
export function UserButtonDemo() {
return
}
```
## Icon [#icon]
```tsx file=/src/demos/shadcn/user/user-button-icon.tsx
import { UserButton } from "@/components/auth/user/user-button"
export function UserButtonIconDemo() {
return
}
```
## Custom links [#custom-links]
Use the `links` prop to add entries above the built-in items. Each entry is either a `{ label, href, icon?, variant?, visibility? }` descriptor or a fully rendered React element. `visibility` defaults to `"always"` and accepts `"authenticated" | "unauthenticated" | "always"`. Pass `hideSettings` to remove the built-in Settings link.
```tsx file=/src/demos/shadcn/user/user-button-links.tsx
import { LayoutDashboard, Users } from "lucide-react"
import { UserButton } from "@/components/auth/user/user-button"
export function UserButtonLinksDemo() {
return (
,
visibility: "authenticated"
},
{
label: "Team",
href: "/team",
icon:
}
]}
/>
)
}
```
For interactive items shared across the app (for example a theme toggle or account switcher), prefer a plugin's `userMenuItems` slot over `links`.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/user-button
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/user-button
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/user-button
```
```bash
bun x shadcn@latest add @better-auth-ui/user-button
```
## Props [#props]
# (/docs/shadcn/components/user/user-view)
## Usage [#usage]
```tsx file=/src/demos/shadcn/user/user-view.tsx
import { UserView } from "@/components/auth/user/user-view"
export function UserViewDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/user-view
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/user-view
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/user-view
```
```bash
bun x shadcn@latest add @better-auth-ui/user-view
```
## Props [#props]
# Additional Fields (/docs/shadcn/concepts/additional-fields)
`additionalFields` is an `AuthProvider` config option that declares extra user fields to render on the sign-up form and the user profile. Each field describes its data type, label, and optional UI rendering. Better Auth UI then handles rendering, parsing, and submitting the value through `signUp.email` (sign-up) and `updateUser` (profile).
Define the same fields in your Better Auth server config under `user.additionalFields`. The UI's `additionalFields` only controls rendering and form submission: the server still owns persistence and validation.
## Usage [#usage]
Pass an array of field configurations to ``:
```tsx
import { AuthProvider } from "@/components/auth/auth-provider"
{children}
```
Fields default to rendering on the user profile only. Set `signUp: true` to also render the field on the sign-up form.
Additional sign-up fields without `required: true` include an ` (optional)` suffix in their label. Override the complete suffix with `localization.auth.optional`, or set it to an empty string to remove the indicator.
## Field types [#field-types]
The `type` controls the data type of the field. The default `inputType` is inferred from `type`, but you can override it for a different look.
| `type` | Default `inputType` | Submitted as |
| ----------- | ------------------- | ------------ |
| `"string"` | `"input"` | `string` |
| `"number"` | `"number"` | `number` |
| `"boolean"` | `"switch"` | `boolean` |
| `"date"` | `"date"` | `Date` |
## Input types [#input-types]
Override the visual rendering with `inputType`:
| `inputType` | Renders |
| ------------ | ----------------------------------------------- |
| `"input"` | Single-line text input |
| `"textarea"` | Multi-line text input |
| `"number"` | Number field with increment / decrement buttons |
| `"slider"` | Slider with live value output |
| `"switch"` | Toggle switch |
| `"checkbox"` | Checkbox |
| `"select"` | Select dropdown |
| `"combobox"` | Searchable combo box |
| `"date"` | Date picker |
| `"datetime"` | Date picker with time field |
| `"hidden"` | Hidden input (submitted but not rendered) |
## Examples [#examples]
### Numeric formatting [#numeric-formatting]
`number` fields accept `Intl.NumberFormatOptions` via `formatOptions`:
```ts
{
name: "hourlyRate",
type: "number",
label: "Hourly rate",
formatOptions: { style: "currency", currency: "USD" }
}
{
name: "commissionRate",
type: "number",
label: "Commission rate",
formatOptions: { style: "percent", maximumFractionDigits: 2 }
}
```
Use `min`, `max`, and `step` to bound the value:
```ts
{
name: "yearsExperience",
type: "number",
label: "Years of experience",
min: 0,
max: 50,
step: 1
}
```
### Slider [#slider]
`inputType: "slider"` honors `min`, `max`, `step`, and `formatOptions`:
```ts
{
name: "budget",
type: "number",
label: "Budget",
inputType: "slider",
min: 0,
max: 5000,
step: 50,
defaultValue: 1000,
formatOptions: { style: "currency", currency: "USD" }
}
```
### Select / Combobox [#select--combobox]
Both accept an `options` array of `{ label, value }` objects:
```ts
{
name: "country",
type: "string",
label: "Country",
inputType: "select",
options: [
{ label: "United States", value: "us" },
{ label: "Canada", value: "ca" },
{ label: "United Kingdom", value: "gb" }
]
}
```
### Prefix / suffix [#prefix--suffix]
String inputs render inside an `InputGroup` when `prefix` or `suffix` is set:
```ts
{
name: "website",
type: "string",
label: "Website",
prefix: "https://",
suffix: ".com"
}
```
### Copy button [#copy-button]
Set `copyable: true` to add a copy button to the input. The button copies the current value, including an edited value.
Use this option with `readOnly: true` for fields such as `id`:
```ts
{
name: "id",
type: "string",
label: "User ID",
readOnly: true,
copyable: true
}
```
### Hidden value [#hidden-value]
`inputType: "hidden"` submits a value without rendering anything visible. Combine with `defaultValue` to attach a server-side preset:
```ts
{
name: "referralSource",
type: "string",
label: "Referral source",
inputType: "hidden",
defaultValue: "demo-app",
signUp: true
}
```
### Custom validation [#custom-validation]
Provide a `validate` callback to check a value before submission. If validation fails, throw an `Error`.
The interface shows the error message in a toast:
```ts
{
name: "nickname",
type: "string",
label: "Nickname",
signUp: true,
required: true,
validate: (value) => {
if (typeof value === "string" && !/^[a-zA-Z0-9_]+$/.test(value)) {
throw new Error(
"Nickname must only contain letters, numbers, and underscores"
)
}
}
}
```
## Where fields render [#where-fields-render]
| Flag | Default | Effect |
| ---------------- | ------- | -------------------------------------------- |
| `signUp: true` | `false` | Render on the sign-up form |
| `profile: false` | `true` | Hide on the user profile |
| `readOnly: true` | `false` | Render but exclude the value from submission |
## Type reference [#type-reference]
# Password Strength (/docs/shadcn/concepts/passwords)
Every form that sets a *new* password renders a four-segment strength meter under the field: sign-up, reset password, change password, and the OTP and phone-number reset variants. The score is computed in the browser as the user types.
The meter is a hint, not a security control. It never blocks submission and it never reaches your server. Your Better Auth password rules stay the only thing that decides what is acceptable.
## Turning it off [#turning-it-off]
The meter is on by default. Switch it off through the `emailAndPassword` config:
```tsx title="components/providers.tsx"
{children}
```
## How the score works [#how-the-score-works]
`evaluatePasswordStrength` scores length first, then character variety, then marks the password down for patterns that read as strong but are not:
* Length at or above `minPasswordLength`, then again at `+4`, then again at 16 characters.
* Three or more of lowercase, uppercase, digits, and symbols. Using all four scores again.
* A password built from one or two distinct characters loses two points.
* A run of four or more characters from the alphabet, the digits, or the top keyboard row loses one point. `abcd`, `4321`, and `qwer` all count, in either direction.
Anything shorter than `minPasswordLength` is capped at **Weak**, so the meter never disagrees with the rule the form itself enforces.
You can call the same function directly if you need the score somewhere else:
```ts
import { evaluatePasswordStrength } from "@better-auth-ui/core"
const { score, level } = evaluatePasswordStrength(password, { minLength: 8 })
// score: 0 | 1 | 2 | 3 | 4
// level: "empty" | "weak" | "fair" | "good" | "strong"
```
## Breached passwords [#breached-passwords]
Better Auth's [`haveIBeenPwned`](https://www.better-auth.com/docs/plugins/have-i-been-pwned) plugin rejects passwords that appear in a known breach corpus. Add it on the server:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { haveIBeenPwned } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
haveIBeenPwned() // [!code highlight]
]
})
```
No UI plugin is needed. The rejection arrives as a `PASSWORD_COMPROMISED` error, and Better Auth UI renders it against the password field rather than as a toast, because it is something the user can fix right there. `` skips the code for the same reason.
Reword it through localization:
```tsx
{children}
```
To detect the same rejection in your own code, use the exported guard:
```ts
import { isPasswordCompromisedError } from "@better-auth-ui/core"
```
# Quick Start (/docs/shadcn)
## Prerequisites [#prerequisites]
Install these prerequisites in your project:
* [Better Auth](https://www.better-auth.com/docs/installation)
* [shadcn/ui](https://ui.shadcn.com/docs/installation)
* [Sonner](https://ui.shadcn.com/docs/components/sonner)
## Installation [#installation]
### Install the auth components [#install-the-auth-components]
Install the authentication page components with the shadcn CLI.
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/auth
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/auth
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/auth
```
```bash
bun x shadcn@latest add @better-auth-ui/auth
```
This command installs `` and its built-in authentication views.
### Install settings and user button (optional) [#install-settings-and-user-button-optional]
If you need the settings page and user button, install them separately.
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/settings @better-auth-ui/user-button
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/settings @better-auth-ui/user-button
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/settings @better-auth-ui/user-button
```
```bash
bun x shadcn@latest add @better-auth-ui/settings @better-auth-ui/user-button
```
### Install everything (optional) [#install-everything-optional]
To install all components, plugins, and email templates, run this command:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/all
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/all
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/all
```
```bash
bun x shadcn@latest add @better-auth-ui/all
```
## Next steps [#next-steps]
Follow a framework-specific guide to integrate Better Auth UI into your project.
Integrate Better Auth UI with TanStack Start
Integrate Better Auth UI with Next.js
## React Reference [#react-reference]
Each shadcn/ui component uses the shared `@better-auth-ui/react` data layer. Read the React reference to access or change authentication state directly.
Hooks, queries, and mutations for every Better Auth endpoint.
# Next.js (/docs/shadcn/integrations/nextjs)
## Prerequisites [#prerequisites]
Complete the [Quick Start](/docs/shadcn) guide first.
## Integration [#integration]
### Create the QueryClient [#create-the-queryclient]
Create a shared `QueryClient` factory with the standard Next.js SSR pattern. Create one client per server request and one browser singleton.
```ts title="lib/query-client.ts" file=/../../examples/next-shadcn-example/src/lib/query-client.ts
import { environmentManager, QueryClient } from "@tanstack/react-query"
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 5000
}
}
})
}
let browserQueryClient: QueryClient | undefined
export function getQueryClient() {
if (environmentManager.isServer()) {
// Server: always make a new query client
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
```
On the server, each call returns a new `QueryClient`. This design keeps the request cache separate for each user.
In the browser, the singleton preserves the React Query cache across navigation. It also gives `HydrationBoundary` a stable hydration target.
### Configure AuthProvider [#configure-authprovider]
Configure `AuthProvider` with Next.js navigation. Then wrap it in `QueryClientProvider` so it uses the shared client.
```tsx title="components/providers.tsx" file=/../../examples/next-shadcn-example/src/components/providers.tsx
"use client"
import { QueryClientProvider } from "@tanstack/react-query"
import Link from "next/link"
import { useParams, useRouter } from "next/navigation"
import { useTheme } from "next-themes"
import type { ReactNode } from "react"
import { apiKeyPlugin } from "@/lib/auth/api-key-plugin"
import { deleteUserPlugin } from "@/lib/auth/delete-user-plugin"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin"
import { multiSessionPlugin } from "@/lib/auth/multi-session-plugin"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
import { passkeyPlugin } from "@/lib/auth/passkey-plugin"
import { themePlugin } from "@/lib/auth/theme-plugin"
import { usernamePlugin } from "@/lib/auth/username-plugin"
import { authClient } from "@/lib/auth-client"
import { getQueryClient } from "@/lib/query-client"
import { AuthProvider } from "./auth/auth-provider"
import { Toaster } from "./ui/sonner"
const normalizeParam = (param: string | string[] | undefined) =>
(Array.isArray(param) ? param[0] : param)?.replace(/^@/, "") ?? null
export function Providers({ children }: { children: ReactNode }) {
const router = useRouter()
const params = useParams()
const queryClient = getQueryClient()
const slug = normalizeParam(params.slug)
return (
replace ? router.replace(to) : router.push(to)
}
plugins={[
usernamePlugin({
usernamePrefix: "@",
localization: { usernamePlaceholder: "username" }
}),
magicLinkPlugin(),
passkeyPlugin(),
apiKeyPlugin({ organization: true }),
themePlugin({ useTheme }),
multiSessionPlugin(),
deleteUserPlugin(),
organizationPlugin({
slugPrefix: "@",
slug
})
]}
Link={Link}
>
{children}
)
}
```
The `navigate` and `Link` props connect Better Auth UI to Next.js navigation. The `navigate` prop accepts `{ to, replace }` options.
### Update the Root Layout [#update-the-root-layout]
Wrap your application with the `Providers` component in your root layout.
```tsx title="app/layout.tsx" file=/../../examples/next-shadcn-example/src/app/layout.tsx
import type { Metadata } from "next"
import { Geist } from "next/font/google"
import type { ReactNode } from "react"
import "@/styles/app.css"
import { ThemeProvider } from "next-themes"
import { Header } from "@/components/header"
import { Providers } from "@/components/providers"
import { cn } from "@/lib/utils"
const geist = Geist({ subsets: ["latin"], variable: "--font-sans" })
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app"
}
export default function RootLayout({
children
}: Readonly<{
children: ReactNode
}>) {
return (
{children}
)
}
```
### Create the Auth Page [#create-the-auth-page]
Install the auth components. Then create a dynamic auth page that selects the authentication view from the path.
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/auth
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/auth
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/auth
```
```bash
bun x shadcn@latest add @better-auth-ui/auth
```
```tsx title="app/auth/[path]/page.tsx" file=/../../examples/next-shadcn-example/src/app/auth/[path]/page.tsx
import { viewPaths } from "@better-auth-ui/core"
import { magicLinkPlugin } from "@better-auth-ui/core/plugins/magic-link"
import { notFound } from "next/navigation"
import { Auth } from "@/components/auth/auth"
const validAuthPaths = new Set([
...Object.values(viewPaths.auth),
...Object.values(magicLinkPlugin().viewPaths.auth ?? {})
])
export default async function AuthPage({
params
}: {
params: Promise<{
path: string
}>
}) {
const { path } = await params
if (!validAuthPaths.has(path)) {
notFound()
}
return (
)
}
```
The `viewPaths.auth` object contains the built-in auth paths: `redirect`, `sign-in`, `sign-up`, `sign-out`, `forgot-password`, `reset-password`, `reset-link-sent`, and `verify-email`.
### Create the Settings page [#create-the-settings-page]
If you installed the `settings` component, create a dynamic settings route for the URL segment.
The async server component validates the path and session. It redirects unauthenticated users before it sends HTML.
The component also adds the session query to `HydrationBoundary`. As a result, child hooks skip their loading state during hydration.
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/settings
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/settings
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/settings
```
```bash
bun x shadcn@latest add @better-auth-ui/settings
```
```tsx title="app/settings/[path]/page.tsx" file=/../../examples/next-shadcn-example/src/app/settings/[path]/page.tsx
import { viewPaths } from "@better-auth-ui/core"
import { organizationPlugin } from "@better-auth-ui/core/plugins/organization"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"
import { headers } from "next/headers"
import { notFound, redirect } from "next/navigation"
import { Settings } from "@/components/auth/settings/settings"
import { auth } from "@/lib/auth"
import { getQueryClient } from "@/lib/query-client"
const validSettingsPaths = new Set([
...Object.values(viewPaths.settings),
...Object.values(organizationPlugin().viewPaths.settings ?? {})
])
export default async function SettingsPage({
params
}: {
params: Promise<{
path: string
}>
}) {
const { path } = await params
if (!validSettingsPaths.has(path)) {
notFound()
}
const requestHeaders = await headers()
const queryClient = getQueryClient()
const session = await ensureSessionServer(queryClient, auth, {
headers: requestHeaders
})
if (!session) {
redirect(
`/auth/sign-in?redirectTo=${encodeURIComponent(`/settings/${path}`)}`
)
}
return (
)
}
```
The `viewPaths.settings` object contains valid settings path segments: `account` and `security`.
## Protecting Routes [#protecting-routes]
Better Auth UI provides separate protection patterns for server-rendered and prerendered routes.
### Server-rendered routes (async server component) [#server-rendered-routes-async-server-component]
For an SSR route, read the session in an async server component. This redirects unauthenticated users before the server streams HTML.
Call `ensureSessionServer` from `@better-auth-ui/core/server` with a separate `QueryClient` for each request. This helper calls `auth.api` directly.
Wrap the rendered children in `HydrationBoundary`. Then downstream `useSession` calls read `authQueryKeys.session` from the hydrated cache.
```tsx title="app/dashboard/page.tsx"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"
import { headers } from "next/headers"
import Link from "next/link"
import { redirect } from "next/navigation"
import { auth } from "@/lib/auth"
import { getQueryClient } from "@/lib/query-client"
export default async function Dashboard() {
const queryClient = getQueryClient()
const session = await ensureSessionServer(queryClient, auth, {
headers: await headers()
})
if (!session) {
redirect("/auth/sign-in?redirectTo=/dashboard")
}
return (
Hello, {session.user.email}
Sign Out
)
}
```
`ensureSession` adds the session to the query cache during SSR. `HydrationBoundary` sends this state to the browser.
As a result, child components that call `useSession` render without a loading state.
If a client component outside the protected route reads the session, prefetch the session in its server parent. Then wrap that subtree in `HydrationBoundary`.
This pattern commonly applies to a header or sidebar that contains `UserButton`. Without the prefetch, the component starts a new browser request.
See the [example header](https://github.com/better-auth-ui/better-auth-ui/blob/main/examples/next-shadcn-example/src/components/header.tsx) for the complete pattern.
### Reactive protection and prerendered routes (`useAuthenticate`) [#reactive-protection-and-prerendered-routes-useauthenticate]
Server-side session checks only occur when the route loads. They do not detect session changes while the page remains mounted.
These changes include token expiration, sign-out in another tab, and server-side session revocation. Prerendered and client-rendered routes also have no server check.
The `useAuthenticate` hook covers both cases. It subscribes to `useSession` and redirects the user when the session ends.
The hook preserves the current URL in the `redirectTo` query parameter.
Use it in two situations:
1. **Alongside an async server component** for server-rendered routes, as a second layer that keeps the UI in sync after the initial load.
2. **On its own** for prerendered or client-rendered routes that have no server-side session access.
```tsx title="app/dashboard/page.tsx"
"use client"
import { authClient } from "@/lib/auth-client"
import { useAuthenticate } from "@better-auth-ui/react"
import Link from "next/link"
import { Spinner } from "@/components/ui/spinner"
export default function Dashboard() {
const { data: session } = useAuthenticate(authClient)
if (!session) {
return (
)
}
return (
Hello, {session.user.email}
Sign Out
)
}
```
The async server component protects the initial render and hydrates the session. Then `useAuthenticate` reacts to later session changes.
## Example Project [#example-project]
For a complete working example, see [next-shadcn-example](https://github.com/better-auth-ui/better-auth-ui/tree/main/examples/next-shadcn-example) in the repository.
## Next Steps [#next-steps]
Read about the shared React hooks and query primitives that power each Better Auth UI component.
Hooks, queries, and mutations for every Better Auth endpoint.
Every auth read, with usage and server-side recipes.
Every auth write, with mutation keys and cache side effects.
# TanStack Start (/docs/shadcn/integrations/tanstack-start)
## Prerequisites [#prerequisites]
Complete the [Quick Start](/docs/shadcn) guide first.
## Integration [#integration]
### Configure AuthProvider [#configure-authprovider]
Configure `AuthProvider` with TanStack Router's navigation.
```tsx title="components/providers.tsx" file=/../../examples/start-shadcn-example/src/components/providers.tsx
import { Link, useNavigate, useParams } from "@tanstack/react-router"
import { ThemeProvider, useTheme } from "next-themes"
import type { ReactNode } from "react"
import { apiKeyPlugin } from "@/lib/auth/api-key-plugin"
import { deleteUserPlugin } from "@/lib/auth/delete-user-plugin"
import { emailOtpPlugin } from "@/lib/auth/email-otp-plugin"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin"
import { multiSessionPlugin } from "@/lib/auth/multi-session-plugin"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
import { passkeyPlugin } from "@/lib/auth/passkey-plugin"
import { themePlugin } from "@/lib/auth/theme-plugin"
import { twoFactorPlugin } from "@/lib/auth/two-factor-plugin"
import { usernamePlugin } from "@/lib/auth/username-plugin"
import { authClient } from "@/lib/auth-client"
import { AuthProvider } from "./auth/auth-provider"
import { Toaster } from "./ui/sonner"
export function Providers({ children }: { children: ReactNode }) {
const navigate = useNavigate()
const { slug } = useParams({ strict: false })
return (
}
>
{children}
)
}
```
The `navigate` and `Link` props connect Better Auth UI to TanStack Router. Pass the `navigate` function directly because it accepts `{ to, replace }`.
`Link` requires a small wrapper. Better Auth UI passes the destination through `href`, but TanStack Router uses `to`.
Map `href` to `to`:
```tsx
Link={({ href, ...props }) => }
```
Do not pass TanStack Router's `Link` directly as `Link={Link}`. It does not receive the required `to` value.
Without this value, the anchor resolves against the current route. The hover URL is incorrect, although the link still works.
### Update the Root Route [#update-the-root-route]
Wrap your application with the `Providers` component in your root route.
```tsx title="routes/__root.tsx" file=/../../examples/start-shadcn-example/src/routes/__root.tsx
import { TanStackDevtools } from "@tanstack/react-devtools"
import type { QueryClient } from "@tanstack/react-query"
import {
createRootRouteWithContext,
HeadContent,
Scripts
} from "@tanstack/react-router"
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"
import type { ReactNode } from "react"
import { Header } from "@/components/header"
import { Providers } from "@/components/providers"
import appCss from "@/styles/app.css?url"
export const Route = createRootRouteWithContext<{
queryClient: QueryClient
}>()({
head: () => ({
meta: [
{
charSet: "utf-8"
},
{
name: "viewport",
content: "width=device-width, initial-scale=1"
},
{
title: "Start shadcn/ui Example"
}
],
links: [
{
rel: "stylesheet",
href: appCss
}
]
}),
shellComponent: RootDocument
})
function RootDocument({ children }: { children: ReactNode }) {
return (
{children}
}
]}
/>
)
}
```
### Create the Auth Page [#create-the-auth-page]
Install the auth components. Then create a dynamic auth page that selects the authentication view from the path.
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/auth
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/auth
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/auth
```
```bash
bun x shadcn@latest add @better-auth-ui/auth
```
```tsx title="routes/auth/$path.tsx" file=/../../examples/start-shadcn-example/src/routes/auth/$path.tsx
import { viewPaths } from "@better-auth-ui/core"
import { createFileRoute, redirect } from "@tanstack/react-router"
import { Auth } from "@/components/auth/auth"
import { emailOtpPlugin } from "@/lib/auth/email-otp-plugin"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
import { twoFactorPlugin } from "@/lib/auth/two-factor-plugin"
const validAuthPathSegments = new Set([
...Object.values(viewPaths.auth),
magicLinkPlugin().viewPaths.auth.magicLink,
organizationPlugin().viewPaths.auth.acceptInvitation,
emailOtpPlugin().viewPaths.auth.emailOtp,
twoFactorPlugin().viewPaths.auth.twoFactor
])
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (!validAuthPathSegments.has(path)) {
throw redirect({ to: "/" })
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return (
)
}
```
The `viewPaths.auth` object contains the built-in auth paths: `redirect`, `sign-in`, `sign-up`, `sign-out`, `forgot-password`, `reset-password`, `reset-link-sent`, and `verify-email`.
### Create the Settings page [#create-the-settings-page]
If you installed the `settings` component, create a dynamic settings route for the URL segment.
Validate the segment against `viewPaths.settings`. Return a 404 response for an unknown path.
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/settings
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/settings
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/settings
```
```bash
bun x shadcn@latest add @better-auth-ui/settings
```
```tsx title="routes/settings/$path.tsx" file=/../../examples/start-shadcn-example/src/routes/settings/$path.tsx
import { ensureSession, viewPaths } from "@better-auth-ui/core"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { createFileRoute, notFound, redirect } from "@tanstack/react-router"
import { createIsomorphicFn } from "@tanstack/react-start"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { Settings } from "@/components/auth/settings/settings"
import { auth } from "@/lib/auth"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
import { authClient } from "@/lib/auth-client"
const validSettingsPaths = [
...Object.values(viewPaths.settings),
...Object.values(organizationPlugin().viewPaths.settings)
]
export const Route = createFileRoute("/settings/$path")({
async beforeLoad({ params: { path }, context: { queryClient }, location }) {
if (!validSettingsPaths.includes(path)) {
throw notFound()
}
const ensureSessionIso = createIsomorphicFn()
.server(() =>
ensureSessionServer(queryClient, auth, { headers: getRequestHeaders() })
)
.client(() => ensureSession(queryClient, authClient))
const session = await ensureSessionIso()
if (!session) {
throw redirect({
to: "/auth/$path",
params: { path: "sign-in" },
search: { redirectTo: location.href }
})
}
return { session }
},
component: SettingsPage
})
function SettingsPage() {
const { path } = Route.useParams()
return (
)
}
```
The `viewPaths.settings` object contains valid settings path segments: `account` and `security`.
## Protecting Routes [#protecting-routes]
Better Auth UI provides separate protection patterns for server-rendered and prerendered routes.
### Server-rendered routes (`beforeLoad`) [#server-rendered-routes-beforeload]
For an SSR route, read the session in `beforeLoad`. This redirects unauthenticated users before a component renders.
Use `createIsomorphicFn` to call `ensureSessionServer` on the server and `ensureSession` in the browser. The server helper calls `auth.api` directly.
Both helpers use `authQueryKeys.session` in the same TanStack Query cache. Child `useSession` calls can reuse the hydrated session.
```tsx title="routes/dashboard.tsx"
import { ensureSession } from "@better-auth-ui/core"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { createFileRoute, Link, redirect } from "@tanstack/react-router"
import { createIsomorphicFn } from "@tanstack/react-start"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
import { authClient } from "@/lib/auth-client"
export const Route = createFileRoute("/dashboard")({
async beforeLoad({ context: { queryClient }, location }) {
const ensureSessionIso = createIsomorphicFn()
.server(() =>
ensureSessionServer(queryClient, auth, { headers: getRequestHeaders() })
)
.client(() => ensureSession(queryClient, authClient))
const session = await ensureSessionIso()
if (!session) {
throw redirect({
to: "/auth/$path",
params: { path: "sign-in" },
search: { redirectTo: location.href }
})
}
return { session }
},
component: Dashboard
})
function Dashboard() {
const { session } = Route.useRouteContext()
return (
Hello, {session.user.email}
Sign Out
)
}
```
Child routes and components can read the returned `{ session }` through `Route.useRouteContext()`.
`ensureSessionServer` also adds the session to the query cache during SSR. Downstream `useSession` calls then render without a loading state.
### Reactive protection and prerendered routes (`useAuthenticate`) [#reactive-protection-and-prerendered-routes-useauthenticate]
`beforeLoad` only runs when the route loads. It does not detect session changes while the page remains mounted.
These changes include token expiration, sign-out in another tab, and server-side session revocation. Prerendered and client-rendered routes also have no server check.
The `useAuthenticate` hook covers both cases. It subscribes to `useSession` and redirects the user when the session ends.
The hook preserves the current URL in the `redirectTo` query parameter.
Use it in two situations:
1. **Alongside `beforeLoad`** for server-rendered routes, as a second layer that keeps the UI in sync after the initial load.
2. **On its own** for prerendered or client-rendered routes that have no server-side session access.
```tsx title="routes/dashboard.tsx"
import { authClient } from "@/lib/auth-client"
import { useAuthenticate } from "@better-auth-ui/react"
import { createFileRoute, Link } from "@tanstack/react-router"
import { Spinner } from "@/components/ui/spinner"
export const Route = createFileRoute("/dashboard")({
component: Dashboard
})
function Dashboard() {
const { data: session } = useAuthenticate(authClient)
if (!session) {
return (
)
}
return (
Hello, {session.user.email}
Sign Out
)
}
```
`beforeLoad` protects the initial render and hydrates the session. Then `useAuthenticate` reacts to later session changes.
## Example Project [#example-project]
For a complete working example, see [start-shadcn-example](https://github.com/better-auth-ui/better-auth-ui/tree/main/examples/start-shadcn-example) in the repository.
## Next Steps [#next-steps]
Read about the shared React hooks and query primitives that power each Better Auth UI component.
Hooks, queries, and mutations for every Better Auth endpoint.
Every auth read, with usage and server-side recipes.
Every auth write, with mutation keys and cache side effects.
# Admin (/docs/shadcn/plugins/admin)
The Admin plugin adds a static `/admin/users` page and a user-detail drawer.
It also adds a "Stop impersonating" action to ``.
## Setup [#setup]
### Enable the Better Auth admin plugin [#enable-the-better-auth-admin-plugin]
Add `admin()` to the server configuration:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { admin } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
admin() // [!code highlight]
]
})
```
Update your database schema after enabling the plugin. Better Auth adds admin
fields to users and an `impersonatedBy` field to sessions.
### Add the matching client plugin [#add-the-matching-client-plugin]
Add `adminClient()` so the UI can read the impersonation marker and restore the
administrator's session:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { adminClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [adminClient()] // [!code highlight]
})
```
### Install the UI integration [#install-the-ui-integration]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/admin
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/admin
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/admin
```
```bash
bun x shadcn@latest add @better-auth-ui/admin
```
This installs:
* `src/lib/auth/auth-plugin.ts`
* `src/lib/auth/admin-plugin.ts`
* `src/components/auth/admin/admin.tsx`
* `src/components/auth/admin/admin-users.tsx`
* `src/components/auth/admin/stop-impersonating.tsx`
### Register the UI plugin [#register-the-ui-plugin]
```tsx title="components/providers.tsx"
import { AuthProvider } from "@/components/auth/auth-provider"
import { adminPlugin } from "@/lib/auth/admin-plugin" // [!code highlight]
{children}
```
## Add the users route [#add-the-users-route]
Create one static route for the users page. The drawer keeps user IDs out of
the route contract.
```tsx title="app/admin/users/page.tsx"
import { Admin } from "@/components/auth/admin/admin"
export default function AdminUsersPage() {
return
}
```
TanStack Start can use the same component in `routes/admin/users.tsx`:
```tsx
import { createFileRoute } from "@tanstack/react-router"
import { Admin } from "@/components/auth/admin/admin"
export const Route = createFileRoute("/admin/users")({
component: () =>
})
```
Use `` when a parent route passes the final static path
segment. Applications can control the drawer without changing this route:
```tsx
import { AdminUsers } from "@/components/auth/admin/admin-users"
```
This API lets an application connect search parameters later. Search
parameters are not required by the library.
## Permissions [#permissions]
The users page calls the Better Auth permission API before it requests the
user list. Each server endpoint remains the final security boundary.
Do not authorize this page from a role string alone. Better Auth also supports
custom roles, custom permissions, and `adminUserIds`.
The table searches one field per request. Select `Email` or `Name` before you
enter a search value. The public Admin API does not provide one combined
search across names, email addresses, and user IDs.
## User inspector [#user-inspector]
Select a row to open the user inspector. The inspector includes local Overview
and Sessions tabs. Registered plugins can add more tabs without adding routes.
The Dash integration adds an Activity tab when both UI plugins are registered.
Dash applies its own organization owner or admin access rules to this tab.
The users page can create users. The inspector can update names and roles, set
passwords, ban or unban users, impersonate users, remove users, and revoke one
or all sessions. Dangerous actions require confirmation. The UI disables
self-destructive actions and still relies on the server.
Passwords stay in local form state. The forms clear each password after the
request or after the user closes the form.
Session IP addresses are hidden by default. Set `showIpAddress: true` only when
your privacy policy permits this data.
## Custom roles and options [#custom-roles-and-options]
```ts
adminPlugin({
allowMultipleRoles: false,
defaultRole: "member",
impersonationRedirectTo: "/",
pageSize: 25,
roles: ["member", "support", "admin"],
showIpAddress: false
})
```
Configure the same roles and permissions in Better Auth `admin()` and
`adminClient()`. The UI list only controls which role choices it shows.
Set `allowMultipleRoles` to `false` to make the create and edit forms accept
one role. This option does not change `adminRoles`, which controls administrator
access.
## Scope [#scope]
This integration uses the public Better Auth Admin client. It does not add
account disconnection, organization membership management, global
organization administration, analytics, or a Sentinel dashboard.
## User button behavior [#user-button-behavior]
`adminPlugin()` contributes `` through the
`userMenuItems` slot. `` places it above sign out.
The action renders only when `session.session.impersonatedBy` is present.
Selecting it calls `authClient.admin.stopImpersonating()` and refreshes the
cached session before the pending state completes.
```tsx
import { StopImpersonating } from "@/components/auth/admin/stop-impersonating"
```
## Options [#options]
```ts
adminPlugin({
localization: {
stopImpersonating: "Return to admin"
}
})
```
## Localization [#localization]
## Mutation API [#mutation-api]
```tsx
import { useStopImpersonating } from "@better-auth-ui/react/plugins/admin"
const stopImpersonating = useStopImpersonating(authClient)
```
Use the hook when you need the same behavior outside the user button. It
restores the admin session and awaits invalidation of the shared session query.
# Agent Auth (/docs/shadcn/plugins/agent-auth)
The Agent Auth registry item adds the application-owned UI that the protocol does not render. It shows the requesting agent, host, mode, capabilities, and required approval strength. Users can allow selected capabilities or deny the request. A security settings card lists agents and revokes individual active grants.
## Setup [#setup]
### Configure Agent Auth on the server [#configure-agent-auth-on-the-server]
Point `deviceAuthorizationPage` at the BAUI route. Define each capability with a clear description and the approval strength it needs.
```ts title="lib/auth.ts"
import { agentAuth } from "@better-auth/agent-auth"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
plugins: [
agentAuth({
deviceAuthorizationPage: "/auth/agent-approval",
capabilities: [
{
name: "invoices:read",
description: "Read invoices and payment status",
approvalStrength: "session"
},
{
name: "invoices:pay",
description: "Pay an invoice with a saved method",
approvalStrength: "webauthn"
}
]
})
]
})
```
Apply the Agent Auth schema after enabling the plugin. See the [Better Auth Agent Auth guide](https://www.better-auth.com/docs/plugins/agent-auth).
### Install the registry item [#install-the-registry-item]
```bash
npx shadcn@latest add @better-auth-ui/agent-auth
```
### Add the client plugin and adapter [#add-the-client-plugin-and-adapter]
```ts title="lib/auth-client.ts"
import { agentAuthClient } from "@better-auth/agent-auth/client"
import { createAgentAuthClientAdapter } from "@better-auth-ui/core/plugins/agent-auth"
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
plugins: [agentAuthClient()]
})
export const agentAuthAdapter = createAgentAuthClientAdapter(authClient)
```
The native adapter combines the agent record, pending grants, and capability catalog into one presentation model. It also provides grant listing and revocation.
### Register the UI plugin [#register-the-ui-plugin]
```tsx title="components/providers.tsx"
import { agentAuthPlugin } from "@/lib/auth/agent-auth-plugin"
{children}
```
### Allow the approval path [#allow-the-approval-path]
Your route that renders `` must accept `agent-approval`. Keep this path equal to `deviceAuthorizationPage`. The view preserves the full approval URL when it sends a signed-out user to sign in.
```tsx title="routes/auth/$path.tsx"
const validAuthPathSegments = new Set([
...Object.values(viewPaths.auth),
agentAuthPlugin({ adapter: agentAuthAdapter }).viewPaths.auth.agentApproval
])
```
## Passkey approvals [#passkey-approvals]
Capabilities with `approvalStrength: "webauthn"` return a WebAuthn challenge. Connect your passkey library through `authenticateWithPasskey`. BAUI retries the same approval with the signed response.
```ts title="lib/auth-client.ts"
export const agentAuthAdapter = createAgentAuthClientAdapter(authClient, {
authenticateWithPasskey: async (options) => {
return runPasskeyAuthentication(options)
}
})
```
The user must have a passkey before approving a capability that requires WebAuthn. Show your normal passkey enrollment flow when the server reports that no passkey is enrolled.
## Approval and grant views [#approval-and-grant-views]
`` reads `agent_id`, `approval_id`, and `code` from the current URL. It lets the user approve a subset of pending capabilities.
`` appears in security settings by default. Set `grants: false` to hide it. You can also render either component directly from the installed registry files.
## Custom adapters [#custom-adapters]
Implement `AgentAuthAdapter` when your application resolves autonomous-agent approval details through a server route or needs a custom policy layer. The UI does not depend on Better Auth response shapes after the adapter boundary.
## Options [#options]
## Localization [#localization]
# Anonymous (/docs/shadcn/plugins/anonymous)
The anonymous UI plugin contributes one "Continue as guest" button to the copied authentication forms. A successful sign-in refreshes the session and follows the `redirectTo` configured on ``.
## Setup [#setup]
### Configure the Better Auth server plugin [#configure-the-better-auth-server-plugin]
Add Better Auth's [Anonymous](https://www.better-auth.com/docs/plugins/anonymous) plugin:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { anonymous } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
anonymous() // [!code highlight]
]
})
```
Run the Better Auth migration command so the user table includes `isAnonymous`:
```bash
bunx @better-auth/cli migrate
```
### Configure the Better Auth client plugin [#configure-the-better-auth-client-plugin]
Add `anonymousClient()` to the browser client:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { anonymousClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [
anonymousClient() // [!code highlight]
]
})
```
### Install the UI plugin [#install-the-ui-plugin]
Install the button and local plugin factory:
```bash
bunx --bun shadcn@latest add @better-auth-ui/anonymous
```
The registry item adds:
* `src/lib/auth/anonymous-plugin.ts`
* `src/components/auth/anonymous/anonymous-button.tsx`
### Register the plugin [#register-the-plugin]
Pass `anonymousPlugin()` to the copied ``:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@/components/auth/auth-provider"
import { anonymousPlugin } from "@/lib/auth/anonymous-plugin" // [!code highlight]
{children}
```
## Change the label [#change-the-label]
```tsx
anonymousPlugin({
localization: {
continueAsGuest: "Explore as a guest"
}
})
```
This integration only adds guest entry. If your application later lets guests create a permanent account, configure Better Auth's server-side `onLinkAccount` callback to move application data to the new user.
# API Key (/docs/shadcn/plugins/api-key)
The API key plugin adds programmatic API key management to your authentication UI. Users can create, copy, and revoke API keys from a security card in account settings. New keys can use a configurable expiration interval, and each listed key shows when it expires.
It contributes:
* An `` card to the security settings tab for user-owned keys
* An `` card to `` for organization-owned keys (opt-in via `apiKeyPlugin({ organization: true })`. Requires the [organization plugin](/docs/shadcn/plugins/organization) and a matching server-side API key config)
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Install the [`@better-auth/api-key`](https://www.better-auth.com/docs/plugins/api-key) package and add it to your Better Auth server config:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
apiKey() // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `apiKeyClient()` to your auth client so `authClient.apiKey.*` methods are available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { apiKeyClient } from "@better-auth/api-key/client" // [!code highlight]
export const authClient = createAuthClient({
plugins: [apiKeyClient()] // [!code highlight]
})
```
### Install the UI plugin [#install-the-ui-plugin]
Run the shadcn CLI to install the API key components and the `apiKeyPlugin()` factory into your project:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/api-key
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/api-key
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/api-key
```
```bash
bun x shadcn@latest add @better-auth-ui/api-key
```
This drops the following into your codebase:
* `src/lib/auth/auth-plugin.ts`: local `AuthPlugin` typing widener
* `src/lib/auth/api-key-plugin.ts`: `apiKeyPlugin()` factory
* `src/components/auth/api-key/api-keys.tsx`: the API keys security card
* `src/components/auth/api-key/api-key.tsx`: individual API key row with delete control
* `src/components/auth/api-key/api-keys-empty.tsx`: empty state shown when no keys exist
* `src/components/auth/api-key/api-key-skeleton.tsx`: skeleton shown while keys are loading
* `src/components/auth/api-key/create-api-key-dialog.tsx`: dialog for creating a new key
* `src/components/auth/api-key/new-api-key-dialog.tsx`: dialog showing the newly created key with copy button
* `src/components/auth/api-key/delete-api-key-dialog.tsx`: confirmation dialog for revoking a key
* `src/components/auth/api-key/organization-api-keys.tsx`: owner-gated wrapper that renders `` scoped to the active organization
### Register the UI plugin [#register-the-ui-plugin]
Pass `apiKeyPlugin()` to ``:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@/components/auth/auth-provider"
import { apiKeyPlugin } from "@/lib/auth/api-key-plugin" // [!code highlight]
import { authClient } from "@/lib/auth-client"
{children}
```
## Configure expiration [#configure-expiration]
The create dialog offers 30 days, 90 days, and Never by default. It initially selects 30 days. Configure the choices through `apiKeyPlugin()`:
```tsx title="components/providers.tsx"
apiKeyPlugin({
keyExpiration: {
intervals: [7, 30, 90],
defaultInterval: 30,
allowNever: true
}
})
```
`intervals` and `defaultInterval` use days. Better Auth receives the selected lifetime as seconds.
Keep the UI choices within the limits in your Better Auth server configuration:
```ts title="lib/auth.ts"
apiKey({
keyExpiration: {
minExpiresIn: 7,
maxExpiresIn: 90,
defaultExpiresIn: null
}
})
```
When `allowNever` is enabled, selecting Never sends no custom interval. Better Auth will still apply `defaultExpiresIn` if the server defines one, so set `allowNever: false` in the UI when your server always requires expiration.
To remove the expiration field and rely entirely on the server default:
```tsx title="components/providers.tsx"
apiKeyPlugin({ keyExpiration: false })
```
## Components [#components]
### `` [#apikeys-]
The security settings page shows `` when the layout renders plugin `securityCards`. Add `apiKeyPlugin()` to `plugins` to provide this card.
**Usage**
```tsx
import { ApiKeys } from "@/components/auth/api-key/api-keys"
```
**Props**
### `` [#organizationapikeys-]
A thin wrapper around `` that resolves the active organization via `useActiveOrganization` and forwards its id. Rendered inside `` only when the plugin is registered with `{ organization: true }`.
To enable, opt in on the UI plugin and add a matching API key configuration to your Better Auth server config. The plugin uses a fixed `configId` of `"organization"`, so the server entry **must** be `{ configId: "organization", references: "organization" }`:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@/components/auth/auth-provider"
import { apiKeyPlugin } from "@/lib/auth/api-key-plugin"
import { authClient } from "@/lib/auth-client"
{children}
```
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
import { organization } from "better-auth/plugins"
export const auth = betterAuth({
// ...
plugins: [
organization(),
apiKey([
{ configId: "default", references: "user" },
{ configId: "organization", references: "organization" } // [!code highlight]
])
]
})
```
See the [Better Auth docs](https://www.better-auth.com/docs/plugins/api-key/advanced#organization-owned-api-keys) for role-based permissions on organization-owned keys.
**Usage**
```tsx
import { OrganizationApiKeys } from "@/components/auth/api-key/organization-api-keys"
```
**Props**
## Options [#options]
## Localization [#localization]
## Lifecycle controls [#lifecycle-controls]
`` lets users create, rename, and delete keys. The create form exposes the name, configuration, expiration, and organization. The list shows status, remaining requests, request usage, and the last request time as read-only values.
The built-in dialog does not show metadata because metadata belongs to the application. For metadata, build a custom form with `useCreateApiKey`. Map named fields or application state to the metadata object. Do not show a raw JSON editor. If metadata affects trusted behavior, validate it in a server route.
Better Auth reserves enablement, permissions, quotas, refill rules, and rate limits for server-side creation and updates. Configure those values in trusted server code instead of exposing them in the account UI.
```tsx
apiKeyPlugin({
configurations: [
{ id: "default", label: "Personal", organization: false },
{ id: "organization", label: "Organization", organization: true }
],
pageSize: 20
})
```
The server must define every listed `configId`. Use `useUpdateApiKey` for a custom rename surface.
# Billing (/docs/shadcn/plugins/billing)
The billing plugin adds a complete billing tab to personal or organization settings. Every component reads a provider-neutral `BillingAdapter`. BAUI includes adapters for Stripe, Polar, Autumn, Creem, Dodo Payments, and Commet.
The UI includes:
* Pricing plans with monthly, yearly, and one-time prices
* Current subscription status
* Checkout and plan changes
* Billing portal access
* Cancellation and restoration
* Seat management
* Metered usage
* Personal and organization billing
## Setup [#setup]
### Configure a Better Auth billing plugin [#configure-a-better-auth-billing-plugin]
Configure one of Better Auth's supported billing plugins on the server and client. Apply its database schema before you open the billing page.
See the provider guides for [Stripe](https://www.better-auth.com/docs/beta/plugins/stripe), [Polar](https://www.better-auth.com/docs/plugins/polar), [Autumn](https://www.better-auth.com/docs/plugins/autumn), [Creem](https://www.better-auth.com/docs/plugins/creem), [Dodo Payments](https://docs.dodopayments.com/developer-resources/better-auth-adaptor), and [Commet](https://www.better-auth.com/docs/plugins/commet).
### Install the registry item [#install-the-registry-item]
```bash
bunx shadcn@latest add @better-auth-ui/billing
```
This installs the billing views, the local plugin factory, and the required shadcn/ui components.
### Create an adapter [#create-an-adapter]
Define the plans once in BAUI's generic format. Amounts use the currency's smallest unit. For example, `2000` USD means `$20.00`.
```ts title="lib/billing.ts"
import {
type BillingPlan,
createStripeBillingAdapter
} from "@better-auth-ui/core/plugins/billing"
import { authClient } from "@/lib/auth-client"
const plans = [
{
id: "pro",
name: "Pro",
description: "For teams shipping production applications.",
prices: [
{ id: "pro-month", amount: 2000, currency: "USD", interval: "month" },
{ id: "pro-year", amount: 19200, currency: "USD", interval: "year" }
],
features: ["Unlimited projects", "Priority support"],
highlighted: true,
seatBased: true
}
] satisfies BillingPlan[]
export const billingAdapter = createStripeBillingAdapter(authClient, {
plans,
successUrl: "/settings/billing?checkout=success",
cancelUrl: "/settings/billing?checkout=canceled",
returnUrl: "/settings/billing"
})
```
### Register the plugin [#register-the-plugin]
This snippet shows only the billing additions. The [TanStack Start](/docs/shadcn/integrations/tanstack-start) and [Next.js](/docs/shadcn/integrations/nextjs) guides cover the full provider, including where `authClient` and `navigate` come from.
```tsx title="components/providers.tsx"
import { AuthProvider } from "@/components/auth/auth-provider"
import { billingPlugin } from "@/lib/auth/billing-plugin"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
import { billingAdapter } from "@/lib/billing"
{children}
```
Organization requests always include the organization ID and slug. Your custom adapter must authorize the requested organization. Do not infer it from active organization state.
## Polar adapter [#polar-adapter]
Map each BAUI plan ID to a Polar product ID or checkout slug. The adapter uses the mapping in both directions, so the UI can match a Polar subscription to its BAUI plan.
```ts title="lib/billing.ts"
import { createPolarBillingAdapter } from "@better-auth-ui/core/plugins/billing"
export const billingAdapter = createPolarBillingAdapter(authClient, {
plans,
products: {
pro: { type: "product", value: "123e4567-e89b-12d3-a456-426614174000" }
},
successUrl: "/settings/billing?checkout=success",
cancelUrl: "/settings/billing?checkout=canceled",
returnUrl: "/settings/billing"
})
```
Polar handles cancellation, restoration, and seat changes in its customer portal. Its adapter marks those direct actions as unsupported, so BAUI shows a manage-billing action instead. Stripe performs the actions through Better Auth's subscription endpoints.
## Other bundled adapters [#other-bundled-adapters]
Each adapter reflects the official client API. It does not claim support for actions that the provider handles in its portal.
| Adapter | Checkout and state | Direct actions | Scope |
| ------------- | ---------------------------------------- | ---------------------------------------- | ------------------------------ |
| Stripe | Better Auth subscription API | Cancel, restore, seats | User and explicit organization |
| Polar | Checkout, subscriptions, usage | Portal fallback | User and explicit organization |
| Autumn | Attach, customer subscriptions, balances | Cancel, restore, optional license seats | User |
| Creem | Checkout and active subscription | Cancel | User |
| Dodo Payments | Checkout session and subscription list | Portal fallback | User |
| Commet | Portal and current subscription | Cancel, optional feature usage and seats | User |
```ts title="lib/billing.ts"
import {
createAutumnBillingAdapter,
createCommetBillingAdapter,
createCreemBillingAdapter,
createDodoPaymentsBillingAdapter
} from "@better-auth-ui/core/plugins/billing"
import { createAutumnClient } from "autumn-js/react"
const urls = {
successUrl: "/settings/billing?checkout=success",
cancelUrl: "/settings/billing?checkout=canceled",
returnUrl: "/settings/billing"
}
const autumnClient = createAutumnClient({
pathPrefix: "/api/auth/autumn",
includeCredentials: true
})
export const autumnAdapter = createAutumnBillingAdapter(autumnClient, {
plans,
seatLicensePlans: { pro: "team-seat" },
...urls
})
export const creemAdapter = createCreemBillingAdapter(authClient, {
plans,
products: { pro: "prod_creem_pro" },
...urls
})
export const dodoAdapter = createDodoPaymentsBillingAdapter(authClient, {
plans,
products: { pro: { type: "slug", value: "pro" } },
...urls
})
export const commetAdapter = createCommetBillingAdapter(authClient, {
plans,
planIds: { pro: "commet-plan-id" },
usage: true,
seatFeatureCode: "members",
...urls
})
```
Enable the matching provider sub-plugins. Dodo needs checkout and portal. Commet needs portal and subscriptions, plus features or seats when you enable those adapter options.
Autumn, Creem, Dodo Payments, and Commet resolve the signed-in customer. Their browser APIs do not accept an explicit organization ID. Their adapters declare `scopes.organization: false`. `billingPlugin` rejects an organization billing configuration instead of reading active organization state.
## Custom adapters [#custom-adapters]
Implement `BillingAdapter` when you use another provider. Keep provider SDK objects inside the adapter and return only BAUI's generic plan, subscription, usage, and action types.
```ts
import type { BillingAdapter } from "@better-auth-ui/core/plugins/billing"
export const billingAdapter: BillingAdapter = {
id: "custom",
scopes: { user: true, organization: true },
supports: { cancel: true, restore: true, seats: true },
listPlans: async (scope, signal) => billingApi.listPlans(scope, signal),
getState: async (scope, signal) => billingApi.getState(scope, signal),
checkout: async (scope, input) => billingApi.checkout(scope, input),
openPortal: async (scope) => billingApi.openPortal(scope),
cancel: async (scope, subscriptionId) =>
billingApi.cancel(scope, subscriptionId),
restore: async (scope, subscriptionId) =>
billingApi.restore(scope, subscriptionId),
updateSeats: async (scope, subscriptionId, seats) =>
billingApi.updateSeats(scope, subscriptionId, seats)
}
```
The browser can change organization IDs and slugs. Validate membership and billing permissions on the server for every adapter operation.
## Headless hooks [#headless-hooks]
React and Solid export the same provider-neutral hooks from `@better-auth-ui/react/plugins/billing` and `@better-auth-ui/solid/plugins/billing`:
* `useBillingPlans`
* `useBillingState`
* `useBillingCheckout`
* `useBillingPortal`
* `useCancelBillingSubscription`
* `useRestoreBillingSubscription`
* `useUpdateBillingSeats`
# Captcha (/docs/shadcn/plugins/captcha)
The captcha plugin adds a widget to the sign-in, sign-up, and forgot-password forms. It sends the resolved token in the `x-captcha-response` header.
The plugin supports Cloudflare Turnstile, hCaptcha, CaptchaFox, and reCAPTCHA. Provide a `render` component that connects the provider callbacks to the plugin.
It contributes:
* A captcha widget rendered above the submit button on sign-in, sign-up, and forgot-password forms
* Automatic header management. The plugin clears the token after an error or expiration, or when the component unmounts.
* Automatic widget refresh after an unsuccessful submission. Captcha tokens are single-use, so each retry requires a new token.
## Social sign-in [#social-sign-in]
Provider buttons forward the current CAPTCHA token to `/sign-in/social`. Add this endpoint to the server CAPTCHA configuration to protect social sign-in. Failed requests clear the token and reset the widget before another attempt.
Use `socialSignInMode="redirect"` for CAPTCHA-protected social sign-in. Better Auth 1.7's experimental popup API does not accept `fetchOptions` or forward CAPTCHA headers. Popup failures reset the widget, but the popup flow cannot send the token.
## Setup [#setup]
The authentication forms already contain the captcha widget slot. Configure the Better Auth server plugin and register the client plugin with your widget.
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Add the [`captcha`](https://www.better-auth.com/docs/plugins/captcha) plugin to your Better Auth server config and pick a provider:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { captcha } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
captcha({ // [!code highlight]
provider: "cloudflare-turnstile", // or "hcaptcha", "captchafox", "google-recaptcha" // [!code highlight]
secretKey: process.env.TURNSTILE_SECRET_KEY as string // [!code highlight]
}) // [!code highlight]
]
})
```
By default, the Better Auth captcha plugin protects `/sign-up/email`, `/sign-in/email`, and `/request-password-reset`. The UI plugin shows the widget on these views.
Better Auth 1.7 and later matches complete authentication paths. Use an exact endpoint or an explicit wildcard such as `/sign-in/*`.
Do not use a partial prefix such as `/sign-in`. To protect username and social sign-in, add their endpoints explicitly:
```ts
captcha({
provider: "cloudflare-turnstile",
secretKey: process.env.TURNSTILE_SECRET_KEY as string,
endpoints: [ // [!code highlight]
"/sign-up/email", // [!code highlight]
"/sign-in/email", // [!code highlight]
"/sign-in/username", // [!code highlight]
"/sign-in/social", // [!code highlight]
"/request-password-reset" // [!code highlight]
] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Pass `captchaPlugin({ render })` to ``. `render` is a component that receives `setToken`, `clearToken`, and `setReset` and is responsible for mounting your provider's React widget.
```tsx title="components/providers.tsx"
import { captchaPlugin } from "@better-auth-ui/react/plugins/captcha" // [!code highlight]
import { AuthProvider } from "@/components/auth/auth-provider"
import { TurnstileWidget } from "@/components/turnstile-widget" // [!code highlight]
{children}
```
The `render` component is mounted as a real React component, so hooks like `useTheme` work inside it. See the [Providers](#providers) section below for ready-to-use widgets.
## Providers [#providers]
### Cloudflare Turnstile [#cloudflare-turnstile]
npm
pnpm
yarn
bun
```bash
npm install @marsidev/react-turnstile
```
```bash
pnpm add @marsidev/react-turnstile
```
```bash
yarn add @marsidev/react-turnstile
```
```bash
bun add @marsidev/react-turnstile
```
```tsx title="components/turnstile-widget.tsx"
import type { CaptchaRenderProps } from "@better-auth-ui/react/plugins/captcha"
import { type TurnstileInstance, Turnstile } from "@marsidev/react-turnstile"
import { useEffect, useRef } from "react"
export function TurnstileWidget({
setToken,
clearToken,
setReset
}: CaptchaRenderProps) {
const ref = useRef(null)
useEffect(() => {
setReset(() => ref.current?.reset())
return () => setReset(null)
}, [setReset])
return (
)
}
```
```tsx title="components/providers.tsx"
import { captchaPlugin } from "@better-auth-ui/react/plugins/captcha"
import { AuthProvider } from "@/components/auth/auth-provider"
import { TurnstileWidget } from "@/components/turnstile-widget"
{children}
```
### hCaptcha [#hcaptcha]
npm
pnpm
yarn
bun
```bash
npm install @hcaptcha/react-hcaptcha
```
```bash
pnpm add @hcaptcha/react-hcaptcha
```
```bash
yarn add @hcaptcha/react-hcaptcha
```
```bash
bun add @hcaptcha/react-hcaptcha
```
```tsx title="components/hcaptcha-widget.tsx"
import type { CaptchaRenderProps } from "@better-auth-ui/react/plugins/captcha"
import HCaptcha from "@hcaptcha/react-hcaptcha"
import { useTheme } from "next-themes"
import { useEffect, useRef } from "react"
export function HCaptchaWidget({
setToken,
clearToken,
setReset
}: CaptchaRenderProps) {
const { resolvedTheme } = useTheme()
const ref = useRef(null)
useEffect(() => {
setReset(() => ref.current?.resetCaptcha())
return () => setReset(null)
}, [setReset])
return (
)
}
```
```tsx title="components/providers.tsx"
import { captchaPlugin } from "@better-auth-ui/react/plugins/captcha"
import { AuthProvider } from "@/components/auth/auth-provider"
import { HCaptchaWidget } from "@/components/hcaptcha-widget"
{children}
```
### CaptchaFox [#captchafox]
npm
pnpm
yarn
bun
```bash
npm install @captchafox/react
```
```bash
pnpm add @captchafox/react
```
```bash
yarn add @captchafox/react
```
```bash
bun add @captchafox/react
```
```tsx title="components/captchafox-widget.tsx"
import type { CaptchaRenderProps } from "@better-auth-ui/react/plugins/captcha"
import { CaptchaFox, type CaptchaFoxInstance } from "@captchafox/react"
import { useTheme } from "next-themes"
import { useEffect, useRef } from "react"
export function CaptchaFoxWidget({
setToken,
clearToken,
setReset
}: CaptchaRenderProps) {
const { resolvedTheme } = useTheme()
const ref = useRef(null)
useEffect(() => {
setReset(() => ref.current?.reset())
return () => setReset(null)
}, [setReset])
return (
)
}
```
```tsx title="components/providers.tsx"
import { captchaPlugin } from "@better-auth-ui/react/plugins/captcha"
import { AuthProvider } from "@/components/auth/auth-provider"
import { CaptchaFoxWidget } from "@/components/captchafox-widget"
{children}
```
## Options [#options]
## Render props [#render-props]
The `render` component receives:
* Connect the provider's success callback to `setToken`. It adds the `x-captcha-response` header to the next Better Auth request.
* Connect the error and expiration callbacks to `clearToken`. It removes the header before the application sends a stale token.
* Connect the widget's `reset()` function to `setReset`. Better Auth consumes the token through `/siteverify` before the authentication handler completes.
A rejected request still consumes the token. Each protected form calls the registered `reset()` function from `onError` and clears the old token.
The plugin also clears the header when the component unmounts. The application does not need additional cleanup.
# Dash (/docs/shadcn/plugins/dash)
The Dash integration adds Activity tabs to personal settings, organization settings, and the Admin user inspector. It reads audit logs through the public `dashClient()` API from `@better-auth/infra`.
Organization owners and admins see organization-wide activity. Other members see only their own activity in that organization. Every organization query uses the organization ID from the current route.
## Setup [#setup]
### Configure Dash on the server [#configure-dash-on-the-server]
Install `@better-auth/infra`, then add `dash()` to Better Auth. Dash records supported authentication and organization events automatically.
```ts title="lib/auth.ts"
import { dash } from "@better-auth/infra"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
plugins: [
dash({
apiUrl: process.env.BETTER_AUTH_API_URL,
kvUrl: process.env.BETTER_AUTH_KV_URL,
apiKey: process.env.BETTER_AUTH_API_KEY
})
]
})
```
See the [Dash plugin guide](https://better-auth.com/docs/infrastructure/plugins/dash) for infrastructure setup and available events.
### Add the client plugin [#add-the-client-plugin]
```ts title="lib/auth-client.ts"
import { dashClient } from "@better-auth/infra/client"
import { createAuthClient } from "better-auth/client"
import { organizationClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [organizationClient(), dashClient()]
})
```
### Install the registry item [#install-the-registry-item]
```bash
bunx shadcn@latest add @better-auth-ui/dash
```
This installs the activity views, local plugin factory, and required shadcn/ui components.
### Register the UI plugin [#register-the-ui-plugin]
```tsx title="components/providers.tsx"
import { AuthProvider } from "@/components/auth/auth-provider"
import { dashPlugin } from "@/lib/auth/dash-plugin"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
{children}
```
Both personal and organization activity are enabled by default. Set `organization: false` if the app does not use Better Auth organizations.
## Static routes [#static-routes]
The plugin adds one static path named `activity`. Its default segment is `activity`, which produces routes such as `/settings/activity` and the matching organization activity path.
It does not use query parameters, nested plugin routes, or a catch-all route. Add the segment to both static path lists when the application validates or generates settings and organization routes.
```ts title="route-paths.ts"
import { viewPaths } from "@better-auth-ui/core"
import { dashPlugin } from "@/lib/auth/dash-plugin"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
const activityPath = dashPlugin({ path: "activity" }).viewPaths.settings.activity
export const validSettingsPaths = [
...Object.values(viewPaths.settings),
...Object.values(organizationPlugin().viewPaths.settings ?? {}),
activityPath
]
export const validOrganizationPaths = [
...Object.values(organizationPlugin().viewPaths.organization ?? {}),
activityPath
]
```
Use the same Dash options in the provider and route configuration when you customize the segment.
## Admin user activity [#admin-user-activity]
If the Admin integration is present, Dash adds an Activity tab to its user inspector. The tab calls `getAllAuditLogs({ userId })` for the selected user.
Dash authorizes this endpoint for organization owners and admins. A Better Auth application-admin role does not grant Dash access by itself.
Set `admin: false` on `dashPlugin()` to remove this inspector tab.
## Access and privacy [#access-and-privacy]
* Personal settings call `getAuditLogs` for the signed-in user.
* Organization settings check the member role for the explicit organization ID.
* Owners and admins call `getAllAuditLogs` for that organization.
* Other members call `getAuditLogs` with the organization filter.
* IP addresses are hidden by default. Set `showIpAddress: true` only when your privacy policy permits it.
The empty state says that no retained activity matches the view. It does not claim that an event never occurred.
## Headless hooks [#headless-hooks]
```tsx
import {
useDashAllAuditLogs,
useDashAuditLogs,
useDashUserAuditLogs
} from "@better-auth-ui/react/plugins/dash"
```
The core package also exports query option factories and `ensure`, `prefetch`, and `fetch` helpers from `@better-auth-ui/core/plugins/dash`.
## Copied files [#copied-files]
* `src/lib/auth/dash-plugin.ts`
* `src/components/auth/dash/activity.tsx`
## Options [#options]
## Localization [#localization]
# Delete User (/docs/shadcn/plugins/delete-user)
The delete-user plugin renders the UI for Better Auth's built-in [account deletion](https://www.better-auth.com/docs/concepts/users-accounts#delete-user) feature. Users can permanently delete their account with a confirmation dialog via the `` card, wrapped by `` in security settings.
## Setup [#setup]
### Enable account deletion in Better Auth [#enable-account-deletion-in-better-auth]
[Better Auth core includes account deletion](https://www.better-auth.com/docs/concepts/users-accounts#delete-user). You do not need another plugin.
Enable the feature by setting `user.deleteUser.enabled` to `true`:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
export const auth = betterAuth({
// ...
user: {
deleteUser: { // [!code highlight]
enabled: true // [!code highlight]
} // [!code highlight]
}
})
```
After you enable the feature, the client provides `authClient.deleteUser()`. You do not need a client plugin.
For an OAuth user without a password, provide `sendDeleteAccountVerification`. This callback lets the user confirm deletion by email.
### Install the UI plugin [#install-the-ui-plugin]
Run the shadcn CLI to install the danger zone card and the `deleteUserPlugin()` factory into your project:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/delete-user
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/delete-user
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/delete-user
```
```bash
bun x shadcn@latest add @better-auth-ui/delete-user
```
This drops the following into your codebase:
* `src/lib/auth/auth-plugin.ts`: local `AuthPlugin` typing widener
* `src/lib/auth/delete-user-plugin.ts`: `deleteUserPlugin()` factory
* `src/components/auth/delete-user/danger-zone.tsx`: the danger zone security card
* `src/components/auth/delete-user/delete-account.tsx`: the delete account card with confirmation dialog
### Register the plugin [#register-the-plugin]
Pass `deleteUserPlugin()` to ``:
```tsx title="components/providers.tsx"
import { deleteUserPlugin } from "@/lib/auth/delete-user-plugin" // [!code highlight]
import { AuthProvider } from "@/components/auth/auth-provider"
{children}
```
## Components [#components]
### `` [#dangerzone-]
The `` card is automatically rendered in `` when the plugin is registered. It renders a danger zone heading and the `` card below it.
**Usage**
```tsx
import { DangerZone } from "@/components/auth/delete-user/danger-zone"
```
**Props**
### `` [#deleteaccount-]
The delete account card with confirmation dialog. Used inside `` by default. Import it directly if you need a custom layout.
When credential confirmation is required, the password starts masked and includes a localized show/hide control.
**Usage**
```tsx
import { DeleteAccount } from "@/components/auth/delete-user/delete-account"
```
**Props**
## Options [#options]
## Localization [#localization]
# Device Authorization (/docs/shadcn/plugins/device-authorization)
The Device Authorization plugin adds the browser half of Better Auth's device authorization flow. A user enters the short code shown by a CLI, TV, or another limited-input device, signs in if needed, then approves or denies access.
It contributes:
* A `` view at `/auth/device`
* Code verification with the `user_code` query parameter prefilled when present
* Sign-in redirection that preserves the pending code
* Approve and deny confirmation states
* React Query hooks for verifying, approving, and denying device requests
The requesting device remains responsible for calling Better Auth's `/device/code` endpoint and polling `/device/token`.
## Setup [#setup]
### Configure the Better Auth server [#configure-the-better-auth-server]
Add the [Device Authorization](https://www.better-auth.com/docs/plugins/device-authorization) plugin to your Better Auth server. Set `verificationUri` to the public route that renders the UI view:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { deviceAuthorization } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
deviceAuthorization({ // [!code highlight]
verificationUri: "/auth/device" // [!code highlight]
}) // [!code highlight]
]
})
```
Generate or migrate your Better Auth schema after enabling the plugin:
```bash
bunx auth@latest migrate
```
The plugin adds the `deviceCode` model used to track pending requests.
### Configure the browser client [#configure-the-browser-client]
Add `deviceAuthorizationClient()` to your browser auth client:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { deviceAuthorizationClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [deviceAuthorizationClient()] // [!code highlight]
})
```
### Install the device authorization view [#install-the-device-authorization-view]
```bash
bunx --bun shadcn@latest add @better-auth-ui/device-authorization
```
This installs:
* `src/lib/auth/device-authorization-plugin.ts`
* `src/components/auth/device-authorization/device-authorization.tsx`
* The required shadcn UI primitives
### Register the UI plugin [#register-the-ui-plugin]
Pass `deviceAuthorizationPlugin()` to ``:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@/components/auth/auth-provider"
import { deviceAuthorizationPlugin } from "@/lib/auth/device-authorization-plugin" // [!code highlight]
{children}
```
### Allow the device route [#allow-the-device-route]
Include the plugin path in the route that renders ``. Keep it aligned with the server's `verificationUri`:
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { createFileRoute, notFound } from "@tanstack/react-router"
import { Auth } from "@/components/auth/auth"
import { deviceAuthorizationPlugin } from "@/lib/auth/device-authorization-plugin"
const validAuthPathSegments = new Set([
...Object.values(viewPaths.auth),
deviceAuthorizationPlugin().viewPaths.auth.deviceAuthorization
])
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (!validAuthPathSegments.has(path)) {
throw notFound()
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return
}
```
## Component [#component]
Enter any eight-character code in the preview to exercise the approval flow.
The component is rendered automatically at `/auth/device` when the plugin is registered. You can also render it directly:
```tsx
import { DeviceAuthorization } from "@/components/auth/device-authorization/device-authorization"
```
## Options [#options]
```ts
deviceAuthorizationPlugin({
// Override the URL segment. Default: "device"
path: "activate",
// Match Better Auth's server-side userCodeLength. Default: 8
userCodeLength: 8,
localization: {
approveDevice: "Allow this device?"
}
})
```
Keep `userCodeLength` equal to the value passed to Better Auth's server plugin. A mismatched length prevents valid codes from being submitted.
## Localization [#localization]
## React APIs [#react-apis]
The view uses these React Query hooks:
* [`useVerifyDeviceCode`](/docs/react/mutations/verify-device-code) verifies and claims the submitted code for the current session.
* [`useApproveDevice`](/docs/react/mutations/approve-device) grants the requesting device access.
* [`useDenyDevice`](/docs/react/mutations/deny-device) rejects the request.
## Sessions and revocation [#sessions-and-revocation]
An approved device token creates an ordinary Better Auth session. There is no separate device registry in the Device Authorization plugin. Use [``](/docs/shadcn/components/settings/security/active-sessions) to list sessions and revoke access for an approved device.
# Email OTP (/docs/shadcn/plugins/email-otp)
The email-OTP plugin swaps emailed links for short codes the user types back into the app. Every flow is opt-in, so you can keep the link-based views you like and replace only the ones you do not.
It contributes:
* An `` sign-in view at `/auth/email-otp` plus a "Continue with Email Code" button
* Code-based replacements for the verify-email, forgot-password, reset-password, and change-email surfaces
* Mutation hooks for every email-OTP endpoint (`useSendVerificationOtp`, `useSignInEmailOtp`, `useVerifyEmailOtp`, `useRequestPasswordResetOtp`, `useResetPasswordOtp`, `useRequestEmailChangeOtp`, `useChangeEmailOtp`)
When `emailAndPassword.enabled === false`, `` takes over `/auth/sign-in` as the primary passwordless surface.
Email-OTP sign-in **replaces** the password, it does not add a step after it. If you want "password, then an emailed code", that is the [two-factor plugin](/docs/shadcn/plugins/two-factor) with `otpOptions`. Better Auth does not apply 2FA to passwordless methods, so email-OTP sign-in bypasses a configured second factor.
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Add the [Email OTP](https://www.better-auth.com/docs/plugins/email-otp) plugin to your server config and wire `sendVerificationOTP` to your email provider. One callback serves all four flows: `type` tells you which one:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { emailOTP } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
emailOTP({ // [!code highlight]
disableSignUp: true, // [!code highlight]
sendVerificationOTP: async ({ email, otp, type }) => { // [!code highlight]
// Send `otp` to `email`. `type` is "sign-in", "email-verification", // [!code highlight]
// "forget-password", or "change-email". // [!code highlight]
} // [!code highlight]
}) // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { emailOTPClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [emailOTPClient()] // [!code highlight]
})
```
### Install the UI plugin [#install-the-ui-plugin]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/email-otp
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/email-otp
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/email-otp
```
```bash
bun x shadcn@latest add @better-auth-ui/email-otp
```
This drops the following into your codebase:
* `src/lib/auth/email-otp-plugin.ts`: the `emailOtpPlugin()` factory
* `src/lib/auth/use-resend-cooldown.ts`: countdown state for resend buttons
* `src/lib/auth/use-sign-in-continuation.ts`: shared post-sign-in handler
* `src/lib/auth/two-factor-methods.ts`: local two-factor redirect metadata support
* `src/components/auth/otp-field.tsx`: the shared code input
* `src/components/auth/email-otp/email-otp.tsx`: the sign-in form
* `src/components/auth/email-otp/email-otp-button.tsx`: the toggle button
* `src/components/auth/email-otp/verify-email-otp.tsx`: code-based email verification
* `src/components/auth/email-otp/forgot-password-otp.tsx` and `reset-password-otp.tsx`: code-based password reset
* `src/components/auth/email-otp/change-email-otp.tsx`: the change-email card
* Provider-button and last-used badge components needed by the passwordless sign-in view
The registry item also refreshes ``, so the change-email override does not require a separate component install.
### Register the plugin [#register-the-plugin]
Turn on the flows you want:
```tsx title="components/providers.tsx"
import { emailOtpPlugin } from "@/lib/auth/email-otp-plugin" // [!code highlight]
import { AuthProvider } from "@/components/auth/auth-provider"
{children}
```
### Allow the new view path [#allow-the-new-view-path]
The plugin contributes an `email-otp` segment to `viewPaths.auth`. Spread `emailOtpPlugin().viewPaths?.auth` into your auth route's allowed-paths set:
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { createFileRoute, notFound } from "@tanstack/react-router"
import { Auth } from "@/components/auth/auth"
import { emailOtpPlugin } from "@/lib/auth/email-otp-plugin" // [!code highlight]
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (
!Object.values({
...viewPaths.auth,
...emailOtpPlugin().viewPaths?.auth // [!code highlight]
}).includes(path)
) {
throw notFound()
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return
}
```
## Choosing which flows use codes [#choosing-which-flows-use-codes]
Each option replaces one link-based surface. Turn a flow on in the UI only when the matching server option is set, otherwise the user waits for a code that never arrives.
| Option | Replaces | Server option it needs |
| ------------------------- | -------------------------------------------- | ------------------------------------------- |
| `signIn` (default `true`) | adds `/auth/email-otp` | none |
| `emailVerification` | `` | `overrideDefaultEmailVerification: true` |
| `passwordReset` | `` and `` | none |
| `changeEmail` | the change-email card in account settings | `changeEmail: { enabled: true }` |
| `verifyCurrentEmail` | adds a step to the change-email flow | `changeEmail: { verifyCurrentEmail: true }` |
```tsx
emailOtpPlugin({
// Keep the emailed sign-in link, use codes for everything else.
signIn: false,
emailVerification: true,
passwordReset: true,
changeEmail: true
})
```
### Confirming the current address [#confirming-the-current-address]
`verifyCurrentEmail` adds a verification step for the current address. After this step, the plugin sends a code to the new address.
This process prevents a hijacked session from moving the account and its password-reset path to an attacker's inbox.
The card handles the extra round trip for you. Set the option on both sides or neither.
## Sign-up and account creation [#sign-up-and-account-creation]
Better Auth creates an account for any address that completes an email-OTP sign-in, unless you set `disableSignUp: true` on the server. The UI mirrors that with `disableSignUp` defaulting to `true`.
Collecting a name only for unregistered addresses reveals which addresses already have accounts. This creates an account-enumeration risk.
Keep sign-up on the password or magic-link path. Alternatively, build a flow that asks every user for the same fields.
## Components [#components]
### `` [#emailotp-]
The form has two states. First, enter an email. Then enter the code:
```tsx
import { EmailOtp } from "@/components/auth/email-otp/email-otp"
```
### `` [#verifyemailotp-]
Rendered at `/auth/verify-email` when `emailVerification` is on. Reads the pending address from session storage (sign-up and sign-in put it there) and asks for it when it is missing.
### `` and `` [#forgotpasswordotp--and-resetpasswordotp-]
With `passwordReset` on, `/auth/forgot-password` emails a code and sends the user straight to `/auth/reset-password`, which takes the code and the new password together. The reset-link-sent view is skipped.
### `` [#changeemailotp-]
With `changeEmail` on, this replaces the built-in change-email card inside ``: no extra wiring needed.
## Options [#options]
## Localization [#localization]
Read these from `useAuthPlugin(emailOtpPlugin).localization` inside custom slot components.
## Email template [#email-template]
Pair the plugin with the [``](/docs/shadcn/components/email/otp-email) component to send a styled code from your `sendVerificationOTP` callback.
# Last Login Method (/docs/shadcn/plugins/last-login-method)
The last-login-method integration floats a compact "Last" indicator over the matching username, email, or social sign-in control. It reads the method after hydration, so server-rendered auth pages do not produce a hydration mismatch.
## Setup [#setup]
### Add the Better Auth server plugin [#add-the-better-auth-server-plugin]
Add Better Auth's [Last Login Method](https://www.better-auth.com/docs/plugins/last-login-method) plugin to your server configuration:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { lastLoginMethod } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
lastLoginMethod() // [!code highlight]
]
})
```
### Add the matching client plugin [#add-the-matching-client-plugin]
Add `lastLoginMethodClient()` to the client passed to ``:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { lastLoginMethodClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [
lastLoginMethodClient() // [!code highlight]
]
})
```
### Install the UI integration [#install-the-ui-integration]
Install the registry item. It includes the updated sign-in components and the UI plugin factory:
```bash
bunx --bun shadcn@latest add @better-auth-ui/last-login-method
```
### Register the UI plugin [#register-the-ui-plugin]
Register the copied plugin factory with your auth provider:
```tsx title="components/providers.tsx"
import { lastLoginMethodPlugin } from "@/lib/auth/last-login-method-plugin" // [!code highlight]
{children}
```
The sign-in view now marks the username or email control, or the matching social provider, when Better Auth has stored a previous method. Sign-up controls do not show the indicator.
## Localization [#localization]
Override the full and compact labels through the UI plugin:
```tsx
lastLoginMethodPlugin({
localization: {
lastUsed: "Previously used",
lastUsedShort: "Previous"
}
})
```
## Custom methods [#custom-methods]
Better Auth tracks email and social providers by default. If `customResolveMethod` stores another method, place the copied `` beside its sign-in control:
```tsx
import { LastUsedBadge } from "@/components/auth/last-login-method/last-used-badge"
```
Pass an array when one control represents more than one stored method:
```tsx
```
## Cookie consent [#cookie-consent]
Whether the plugin's browser-readable cookie is non-essential and requires consent depends on your jurisdiction and how your application uses it. Consult qualified legal counsel for guidance. When consent is required, configure Better Auth's `beforeStoreCookie` option to return a stored user-consent flag or an equivalent condition. Authentication still works when the hook returns `false`.
# Magic Link (/docs/shadcn/plugins/magic-link)
The magic-link plugin adds a passwordless email sign-in flow. The user enters their email, receives a one-time link, and is signed in when they click it.
It contributes:
* A `` view at `/auth/magic-link`
* A `` confirmation view at `/auth/magic-link-sent`
* A "Continue with Magic Link" button rendered alongside the password sign-in button
* A `useSignInMagicLink` mutation hook
When `emailAndPassword.enabled === false`, `` automatically takes over `/auth/sign-in` as the primary passwordless surface.
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Add the [Magic Link](https://www.better-auth.com/docs/plugins/magic-link) plugin to your Better Auth server config and wire up `sendMagicLink` to your email provider:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { magicLink } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
magicLink({ // [!code highlight]
sendMagicLink: async ({ email, url }) => { // [!code highlight]
// Send `url` to `email` via your email provider. // [!code highlight]
} // [!code highlight]
}) // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `magicLinkClient()` to your auth client so `authClient.signIn.magicLink` is available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { magicLinkClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [magicLinkClient()] // [!code highlight]
})
```
### Install the UI plugin [#install-the-ui-plugin]
Run the shadcn CLI to install the magic-link form, the toggle button, and the `magicLinkPlugin()` factory into your project:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/magic-link
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/magic-link
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/magic-link
```
```bash
bun x shadcn@latest add @better-auth-ui/magic-link
```
This drops the following into your codebase:
* `src/lib/auth/auth-plugin.ts`: local `AuthPlugin` typing widener
* `src/lib/auth/magic-link-plugin.ts`: `magicLinkPlugin()` factory
* `src/components/auth/magic-link.tsx`: the magic-link form
* `src/components/auth/magic-link-sent.tsx`: the sent confirmation view
* `src/components/auth/magic-link-button.tsx`: the toggle button
* `src/components/auth/open-email-button.tsx`: the email-provider shortcut
* `src/components/auth/provider-button(s).tsx`: social provider buttons used by the form
### Register the plugin [#register-the-plugin]
Pass `magicLinkPlugin()` to ``:
```tsx title="components/providers.tsx"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin" // [!code highlight]
import { AuthProvider } from "@/components/auth/auth-provider"
{children}
```
### Allow the new view path [#allow-the-new-view-path]
The plugin contributes `magic-link` and `magic-link-sent` segments to `viewPaths.auth`. Spread `magicLinkPlugin().viewPaths?.auth` into your auth route's allowed-paths set so both views resolve correctly:
TanStack Start
Next.js
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { createFileRoute, notFound } from "@tanstack/react-router"
import { Auth } from "@/components/auth/auth"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin" // [!code highlight]
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (
!Object.values({
...viewPaths.auth,
...magicLinkPlugin().viewPaths?.auth // [!code highlight]
}).includes(path)
) {
throw notFound()
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return
}
```
```tsx title="app/auth/[path]/page.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { notFound } from "next/navigation"
import { Auth } from "@/components/auth/auth"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin" // [!code highlight]
export default async function AuthPage({
params
}: {
params: Promise<{ path: string }>
}) {
const { path } = await params
if (
!Object.values({
...viewPaths.auth,
...magicLinkPlugin().viewPaths?.auth // [!code highlight]
}).includes(path)
) {
notFound()
}
return
}
```
## Components [#components]
### `` [#magiclink-]
The `` view is rendered at `/auth/magic-link` when the plugin is registered and the route is wired up (see the setup step above).
**Usage**
```tsx
import { MagicLink } from "@/components/auth/magic-link"
```
**Props**
### `` [#magiclinksent-]
After a magic-link request succeeds, the form stores the submitted email in session storage and navigates to this confirmation view. It shows an email-provider shortcut when one is available. Hover or focus the button to show a QR code for opening the same provider URL on another device.
```tsx
import { MagicLinkSent } from "@/components/auth/magic-link-sent"
```
## Options [#options]
```ts
magicLinkPlugin({
// Override the URL segment. Default: "magic-link"
path: "email-link",
// Override the confirmation segment. Default: "magic-link-sent"
sentPath: "email-link-sent",
// Override any of the plugin's localization strings.
localization: {
sendMagicLink: "Email me a link"
}
})
```
## Localization [#localization]
Read these from `useAuthPlugin(magicLinkPlugin).localization` inside custom slot components.
## Email template [#email-template]
Pair the plugin with the [``](/docs/shadcn/components/email/magic-link-email) component to send a styled email from your `sendMagicLink` callback.
## Passwordless-only flows [#passwordless-only-flows]
If you disable email and password authentication, the magic-link form becomes the primary sign-in view. No additional configuration is required:
```tsx
{children}
```
`/auth/sign-in` now renders ``, and the `signUp`, `forgotPassword`, `resetPassword`, and `resetLinkSent` routes redirect to it.
# Multi Session (/docs/shadcn/plugins/multi-session)
The multi-session plugin enables users to maintain multiple active sessions simultaneously. Users can switch between accounts without signing out, manage all their device sessions, and quickly add new accounts from the user menu.
It contributes:
* A "Switch Account" submenu in the user button dropdown showing all active device sessions
* A `` card in account settings for viewing and revoking device sessions
* `useListDeviceSessions`, `useSetActiveSession`, and `useRevokeMultiSession` hooks
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Add the [Multi Session](https://www.better-auth.com/docs/plugins/multi-session) plugin to your Better Auth server config:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { multiSession } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
multiSession() // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `multiSessionClient()` to your auth client so `authClient.multiSession.*` methods are available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { multiSessionClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [multiSessionClient()] // [!code highlight]
})
```
### Install the UI plugin [#install-the-ui-plugin]
Run the shadcn CLI to install the multi-session management components and the `multiSessionPlugin()` factory into your project:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/multi-session
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/multi-session
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/multi-session
```
```bash
bun x shadcn@latest add @better-auth-ui/multi-session
```
This drops the following into your codebase:
* `src/lib/auth/auth-plugin.ts`: local `AuthPlugin` typing widener
* `src/lib/auth/multi-session-plugin.ts`: `multiSessionPlugin()` factory
* `src/components/multi-session/manage-accounts.tsx`: the account management card
* `src/components/multi-session/manage-account.tsx`: individual account row
* `src/components/multi-session/switch-account-submenu.tsx`: the submenu trigger
* `src/components/multi-session/switch-account-submenu-content.tsx`: the submenu content
* `src/components/multi-session/switch-account-submenu-item.tsx`: individual session item
### Register the plugin [#register-the-plugin]
Pass `multiSessionPlugin()` to ``:
```tsx title="components/providers.tsx"
import { multiSessionPlugin } from "@/lib/auth/multi-session-plugin" // [!code highlight]
import { AuthProvider } from "@/components/auth/auth-provider"
{children}
```
## Components [#components]
### `` [#userbutton-]
The plugin adds a "Switch Account" submenu to ``. It shows all active device sessions.
Users can switch accounts, add an account, and identify the current account.
**Usage**
```tsx
import { SwitchAccountSubmenu } from "@/components/auth/multi-session/switch-account-submenu"
```
**Props**
### `` [#manageaccounts-]
The `` card is rendered in account settings when your layout renders plugin-contributed `accountCards` and `multiSessionPlugin()` is in `plugins`.
**Usage**
```tsx
import { ManageAccounts } from "@/components/auth/multi-session/manage-accounts"
```
**Props**
## Options [#options]
```ts
multiSessionPlugin({
// Override any of the plugin's localization strings.
localization: {
switchAccount: "Switch Account",
addAccount: "Add Account",
manageAccounts: "Manage Accounts"
}
})
```
## Localization [#localization]
## Session management [#session-management]
The plugin provides hooks for managing multiple sessions:
* `useListDeviceSessions` - List all device sessions for the current user
* `useSetActiveSession` - Switch to a different session
* `useRevokeMultiSession` - Sign out from a specific session
# OAuth Provider (/docs/shadcn/plugins/oauth-provider)
The OAuth Provider plugin covers the user-facing screens Better Auth's [OAuth 2.1 Provider](https://better-auth.com/docs/plugins/oauth-provider) redirects to. It also provides connected application security settings and OAuth client developer settings.
## Client management [#client-management]
Enable personal client management to add an OAuth clients tab to user settings. The tab lists, creates, edits, deletes, and rotates secrets through Better Auth's signed-in client endpoints.
```tsx
oauthProviderPlugin({
clientManagement: true
})
```
The client secret appears only after creation or rotation. The user must copy it before closing the dialog.
Pass `clientManager` when personal clients also need server-only operations such as enable or disable. This manager uses `{ type: "user" }` as its owner. The UI shows the enable or disable control when the manager implements `setDisabled`.
Organization clients need an application-owned `OAuthClientManager`. Every operation receives both the organization ID and slug. Authorize both values in your server endpoint. Do not infer the organization from active session state.
```tsx
oauthProviderPlugin({
organizationClientManager: {
list: (owner, signal) => api.oauthClients.list(owner, signal),
create: (owner, input) => api.oauthClients.create(owner, input),
update: (owner, clientId, update) =>
api.oauthClients.update(owner, clientId, update),
delete: (owner, clientId) => api.oauthClients.delete(owner, clientId),
rotateSecret: (owner, clientId) =>
api.oauthClients.rotateSecret(owner, clientId),
setDisabled: (owner, clientId, disabled) =>
api.oauthClients.setDisabled(owner, clientId, disabled)
}
})
```
Better Auth 1.7 exposes enable or disable through server admin APIs, so BAUI does not call it from the default browser adapter.
It contributes:
* An `` view at `/auth/oauth-consent`
* An `` view at `/auth/oauth-sign-up`, for `prompt=create`
* An `` view at `/auth/select-account`, for `prompt=select_account`
* An `` card in security settings
* Public OAuth client metadata loading
* Scope labels as a map, a list, or a resolver
* Headless continuation through `useOAuthContinue`, for your own post-login screens
## How the redirect screens fit together [#how-the-redirect-screens-fit-together]
Better Auth owns the authorization request. When it needs user input, it redirects to one of your pages.
The redirect includes the signed authorization query. Call `oauth2.continue` after the user provides the input:
| Prompt | Page | Continuation |
| ---------------- | -------------------- | -------------------------------------- |
| `consent` | `consentPage` | `oauth2.consent({ accept })` |
| `create` | `signup.page` | `oauth2.continue({ created: true })` |
| `select_account` | `selectAccount.page` | `oauth2.continue({ selected: true })` |
| None | `postLogin.page` | `oauth2.continue({ postLogin: true })` |
Keep the query string on every one of those pages. Do not strip it, rebuild it from `redirect_uri`, or navigate to the requested redirect yourself. `oauthProviderClient()` forwards the signed query to Better Auth, and Better Auth validates it and completes the redirect.
## Setup [#setup]
### Configure the Better Auth server [#configure-the-better-auth-server]
Install the provider package:
```bash
bun add @better-auth/oauth-provider
```
Add the JWT and OAuth Provider plugins, and point each page option at the route that renders the matching view:
```ts title="lib/auth.ts"
import { oauthProvider } from "@better-auth/oauth-provider"
import { betterAuth } from "better-auth"
import { jwt, multiSession } from "better-auth/plugins"
export const auth = betterAuth({
disabledPaths: ["/token"],
plugins: [
jwt(),
multiSession(),
oauthProvider({
loginPage: "/auth/sign-in",
consentPage: "/auth/oauth-consent",
signup: {
page: "/auth/oauth-sign-up"
},
selectAccount: {
page: "/auth/select-account",
shouldRedirect: async () => true
}
})
]
})
```
`signup` and `selectAccount` both use `loginPage` by default. Set each `page` explicitly.
Each page uses a plugin route and does not replace `/auth/sign-up`. `selectAccount.shouldRedirect` controls when the application shows the chooser.
Return `true` to always show the chooser. Otherwise, use the session and scopes to make the decision.
Generate or migrate your Better Auth schema after enabling the server plugin:
```bash
bunx auth@latest migrate
```
### Configure the browser client [#configure-the-browser-client]
Add `oauthProviderClient()` to the auth client. It preserves Better Auth's signed authorization query when the user responds:
```ts title="lib/auth-client.ts"
import { oauthProviderClient } from "@better-auth/oauth-provider/client"
import { createAuthClient } from "better-auth/react"
import { multiSessionClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [oauthProviderClient(), multiSessionClient()]
})
```
`multiSessionClient()` is what makes the account chooser work: it lists the device sessions and switches the active one. Skip it if you do not use `prompt=select_account`.
### Install the views [#install-the-views]
```bash
bunx shadcn@latest add @better-auth-ui/oauth-provider
```
This installs:
* `src/lib/auth/oauth-provider-plugin.ts`
* `src/components/auth/oauth-provider/oauth-consent.tsx`
* `src/components/auth/oauth-provider/oauth-sign-up.tsx`
* `src/components/auth/oauth-provider/oauth-select-account.tsx`
* `src/components/auth/oauth-provider/authorized-applications.tsx` and its row, empty-state, loading, and confirmation-dialog components
### Register the UI plugin [#register-the-ui-plugin]
```tsx title="components/providers.tsx"
import { AuthProvider } from "@/components/auth/auth-provider"
import { oauthProviderPlugin } from "@/lib/auth/oauth-provider-plugin"
{children}
```
### Allow the OAuth routes [#allow-the-oauth-routes]
Include the plugin paths in the route that renders ``:
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { oauthProviderPlugin } from "@/lib/auth/oauth-provider-plugin"
const validAuthPathSegments = new Set([
...Object.values(viewPaths.auth),
...Object.values(oauthProviderPlugin().viewPaths.auth)
])
```
Keep these paths aligned with the server's `consentPage`, `signup.page`, and `selectAccount.page`. If your route already resolves plugin paths without an allowlist, no route change is needed.
## Scope metadata [#scope-metadata]
`scopeMetadata` accepts three shapes. Every requested scope remains visible.
If a scope has no match, the plugin uses built-in metadata. The raw scope value is the final fallback.
### Map [#map]
The original form. Good when the scope set is known up front:
```tsx title="components/providers.tsx"
oauthProviderPlugin({
scopeMetadata: {
calendar: {
label: "View your calendar",
description: "Read your calendar events and availability."
}
}
})
```
### List [#list]
Convenient when metadata comes out of a database or an API and arrives as an array:
```tsx title="components/providers.tsx"
oauthProviderPlugin({
scopeMetadata: [
{ scope: "calendar", label: "View your calendar" },
{ scope: "files", label: "View your files" }
]
})
```
### Resolver [#resolver]
For labels that depend on the requesting client or the rest of the scope set:
```tsx title="components/providers.tsx"
oauthProviderPlugin({
scopeMetadata: (scope, { clientId, requestedScopes }) => {
if (scope.startsWith("project:")) {
return { label: `Access ${scope.slice("project:".length)}` }
}
if (scope === "admin" && requestedScopes.includes("offline_access")) {
return {
label: "Administer your workspace",
description: `${clientId} can act on your behalf indefinitely.`
}
}
// Fall back to the built-in or raw label.
return undefined
}
})
```
Returning `undefined` means "use the fallback", not "hide this scope".
Resolvers stay synchronous, so rendering is deterministic and behaves the same under SSR. If you need remote metadata, load it before render and pass a map or a list.
## Sign-up continuation [#sign-up-continuation]
`` lives at its own route and wraps the same `` component your app already uses: it does not replace the built-in sign-up view. Users who never go through OAuth never touch it.
When Better Auth redirects there with `prompt=create`, the ordinary sign-up implementation creates the account, and only after that succeeds does the view call:
```ts
authClient.oauth2.continue({ created: true })
```
If the continuation request fails, the form shows a retry action. The account already exists, so do not submit the sign-up form again.
Reached without `prompt=create`, it renders plain sign-up and redirects the way sign-up normally does.
### Limitations [#limitations]
Two flows deliberately do not continue on their own:
* **Email verification.** When `requireEmailVerification` is on, sign-up has no usable session yet, so the view sends the user to the verify-email screen instead. Resume after verification yourself with [`useOAuthContinue`](/docs/react/mutations/oauth-continue).
* **Social sign-up.** The provider redirect leaves and re-enters your app, so there is no "sign-up just succeeded" moment to hook into. Resume from your social callback route instead.
In both cases only call `{ created: true }` if the account really was created during this flow. An already signed-in user is not a newly created one.
## Account selection [#account-selection]
`` lists the device sessions from `multiSession` and lets the user pick one. Choosing the account that is already active continues directly. Choosing a different one calls `multiSession.setActive()` first, then continues: the switch always lands before Better Auth resumes.
Sessions are compared by session ID, never by user ID or list position.
The chooser has no sign-out or revoke actions on purpose. Session management belongs in security settings, not in the middle of an authorization request.
## Post-login selection [#post-login-selection]
There is no post-login view to install. An application can select a workspace, tenant, team, project, role, or another resource.
Build this selection interface in the application. When the selection is complete, use the headless continuation helper:
```tsx title="routes/auth/select-workspace.tsx"
import { useAuth } from "@better-auth-ui/react"
import { useOAuthContinue } from "@better-auth-ui/react/plugins/oauth-provider"
type Workspace = { slug: string; name: string }
function SelectWorkspace({ workspaces }: { workspaces: Workspace[] }) {
const { authClient } = useAuth()
const oauthContinue = useOAuthContinue(authClient)
const select = async (slug: string) => {
// Persist the selection the way your app normally does — by slug or ID.
await setActiveWorkspaceSlug(slug)
await oauthContinue.mutateAsync({ postLogin: true })
}
return workspaces.map((workspace) => (
))
}
```
Point the server's `postLogin.page` at that route.
Do not use Better Auth active organizations here. Persist the selection with your own slug- or ID-based mechanism.
## Connected applications [#connected-applications]
`` is a security card for authorized applications. It shows the client name, logo, granted scopes, and latest authorization date.
The card also provides a "Remove authorization" action.
Better Auth can store several consent records for one client, so records are grouped by client ID and rendered as a single application. Removing an application deletes every consent ID in that group. Each row loads its own client metadata, so one slow or missing application never blocks the rest of the card.
Turn the card off with:
```tsx
oauthProviderPlugin({ showConnectedApplications: false })
```
Removing an authorization deletes the stored consent record. The application needs the user's approval before it receives new access.
Existing access and refresh tokens stay valid until they expire. Better Auth does not provide complete token revocation through this endpoint.
Do not tell users that this action revokes existing tokens.
The card manages consent records only. It is not a session list or token list.
The card has no revoke-all control because Better Auth does not provide the required token operations.
## Consent behavior [#consent-behavior]
The consent view accepts or denies the complete requested scope set. It does not render per-scope controls. Omitting `scope` from the consent mutation tells Better Auth to accept the scopes from the original signed request.
The public client endpoint requires a signed-in session. Direct visits with missing request data, no session, or an unknown client render an invalid-request state.
Login reuses the existing `signIn` view and resumes automatically when Better Auth creates the session.
## Components [#components]
```tsx
import { AuthorizedApplications } from "@/components/auth/oauth-provider/authorized-applications"
import { OAuthConsent } from "@/components/auth/oauth-provider/oauth-consent"
import { OAuthSelectAccount } from "@/components/auth/oauth-provider/oauth-select-account"
import { OAuthSignUp } from "@/components/auth/oauth-provider/oauth-sign-up"
```
## Plugin options [#plugin-options]
## React APIs [#react-apis]
* [`usePublicOAuthClient`](/docs/react/queries/public-oauth-client) loads application metadata
* [`useOAuthConsent`](/docs/react/mutations/oauth-consent) submits the user's decision
* [`useOAuthContinue`](/docs/react/mutations/oauth-continue) resumes the request after a redirect screen
* [`useListOAuthConsents`](/docs/react/queries/list-oauth-consents) lists authorized applications
* [`useDeleteOAuthConsent`](/docs/react/mutations/delete-oauth-consent) removes a stored consent
Each hook also exports its TanStack Query options factory.
# One Tap (/docs/shadcn/plugins/one-tap)
The One Tap UI plugin opens Better Auth's native One Tap flow when an authentication view mounts. It refreshes the session after success, follows the configured `redirectTo`, and continues into the two-factor view when the server requests a second factor.
Keep Google in `socialProviders` as a visible fallback. One Tap is a passive prompt, and browsers can decide not to show it.
## Setup [#setup]
### Configure the Better Auth server plugin [#configure-the-better-auth-server-plugin]
Add Better Auth's [One Tap](https://www.better-auth.com/docs/plugins/one-tap) plugin with the OAuth client ID from your Google Cloud project:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { oneTap } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
oneTap({
clientId: process.env.GOOGLE_CLIENT_ID as string
}) // [!code highlight]
]
})
```
### Configure the Better Auth client plugin [#configure-the-better-auth-client-plugin]
Use the same client ID in the browser client:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { oneTapClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [
oneTapClient({
clientId: import.meta.env.VITE_GOOGLE_CLIENT_ID,
promptOptions: {
baseDelay: 1_000,
maxAttempts: 3
}
}) // [!code highlight]
]
})
```
### Update the copied authentication views [#update-the-copied-authentication-views]
Install the registry item so the sign-in and sign-up surfaces include the headless prompt slot:
```bash
bunx --bun shadcn@latest add @better-auth-ui/one-tap
```
### Register the Better Auth UI plugin [#register-the-better-auth-ui-plugin]
Pass `oneTapPlugin()` to your copied ``. The prompt opens on sign-in by default.
```tsx title="components/providers.tsx"
import { oneTapPlugin } from "@better-auth-ui/react/plugins/one-tap" // [!code highlight]
{children}
```
### Add your authorized origins [#add-your-authorized-origins]
Add every application origin that can render the prompt to the OAuth client's **Authorized JavaScript origins** in Google Cloud. Include the exact protocol, host, and development port, such as `http://localhost:3000`.
## Show One Tap on sign-up [#show-one-tap-on-sign-up]
Pass both auth views when you also want the prompt on sign-up:
```tsx
oneTapPlugin({
views: ["signIn", "signUp"]
})
```
The plugin sends the matching `signin` or `signup` context to Better Auth. This preserves server-side sign-up controls and redirects.
## Prompt options [#prompt-options]
Better Auth's prompt settings can be passed directly to the UI plugin:
```tsx
oneTapPlugin({
autoSelect: true,
cancelOnTapOutside: false,
onPromptNotification: (notification) => {
// Track when Google skips or dismisses the prompt.
}
})
```
The integration supports the stricter One Tap responses in Better Auth 1.7. Errors such as `EMAIL_NOT_VERIFIED` are sent through the normal authentication error handler instead of being hidden.
## Last login method [#last-login-method]
Better Auth's last-login-method plugin does not classify the One Tap callback as Google by default. If you use its badge, resolve the callback explicitly:
```ts title="lib/auth.ts"
lastLoginMethod({
customResolveMethod: (context) =>
context.path === "/one-tap/callback" ? "google" : null
})
```
# Organization (/docs/shadcn/plugins/organization)
The organization plugin adds multi-tenant organization management to your authentication UI. Users can create, switch between, and manage organizations with members, invitations, and roles.
It contributes:
* An `organizations` tab to `` listing every organization the user belongs to plus pending invitations to them
* An `` shell mounted at `/organization/` with `settings` and `people` tabs
* An `` dropdown to switch the active organization, manage it, or create a new one
* An `organizationCards` plugin slot rendered inside `` so other plugins (for example [api-key](/docs/shadcn/plugins/api-key)) can attach org-scoped cards
* Hooks and mutations for organization endpoints, including `useActiveOrganization`, `useListOrganizations`, and `useInviteMember`
## Setup [#setup]
### Install the server plugin [#install-the-server-plugin]
Add the [`organization`](https://www.better-auth.com/docs/plugins/organization) plugin to your Better Auth server config:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { organization } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
organization() // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `organizationClient()` to your auth client so `authClient.organization.*` methods are available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { organizationClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [organizationClient()] // [!code highlight]
})
```
### Install the UI plugin [#install-the-ui-plugin]
Run the shadcn CLI to install every organization component and the `organizationPlugin()` factory into your project:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/organization
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/organization
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/organization
```
```bash
bun x shadcn@latest add @better-auth-ui/organization
```
This drops the following into your codebase:
* `src/lib/auth/auth-plugin.ts`: local `AuthPlugin` typing widener
* `src/lib/auth/organization-plugin.tsx`: `organizationPlugin()` factory
* `src/components/auth/organization/accept-invitation.tsx`: direct invitation acceptance view
* `src/components/auth/organization/organization.tsx`: the `/organization/` shell
* `src/components/auth/organization/organization-roles.tsx`: dynamic role and permission editor
* `src/components/auth/organization/organization-switcher.tsx`: header dropdown for switching organizations
* `src/components/auth/organization/team-switcher.tsx`: organization-scoped team selection
* `src/components/auth/organization/organization-settings.tsx`: settings tab contents (profile + danger zone + plugin cards)
* `src/components/auth/organization/organization-people.tsx`: people tab contents (members + invitations)
* `src/components/auth/organization/organization-profile.tsx`: profile card (logo, name, slug)
* `src/components/auth/organization/organization-danger-zone.tsx`: danger zone card
* `src/components/auth/organization/organization-members.tsx`: members table with search/filter/sort
* `src/components/auth/organization/organization-member-row.tsx`: member row with role and remove actions
* `src/components/auth/organization/organization-invitations.tsx`: invitations table with search/filter/sort
* `src/components/auth/organization/organization-invitation-row.tsx`: invitation row with cancel action
* `src/components/auth/organization/organizations.tsx`: list of organizations the user belongs to
* `src/components/auth/organization/organization-row.tsx`: single organization row in the list
* `src/components/auth/organization/organizations-settings.tsx`: `/settings/organizations` panel
* `src/components/auth/organization/user-invitations.tsx`: invitations addressed to the user
* `src/components/auth/organization/user-invitation-row.tsx`: invitation row with accept/reject actions
* `src/components/auth/organization/create-organization-dialog.tsx`: new-organization dialog
* `src/components/auth/organization/invite-member-dialog.tsx`: invite-by-email dialog
* `src/components/auth/organization/delete-organization-dialog.tsx`: delete confirmation dialog
* `src/components/auth/organization/leave-organization-dialog.tsx`: leave confirmation dialog
* `src/components/auth/organization/remove-member-dialog.tsx`: remove-member confirmation dialog
* `src/components/auth/organization/leave-organization.tsx` / `delete-organization.tsx`: danger-zone rows
* `src/components/auth/organization/change-organization-logo.tsx`: logo upload control
* `src/components/auth/organization/organization-logo.tsx`, `organization-view.tsx`, `slug-field.tsx`, plus matching skeletons and empty states
### Register the UI plugin [#register-the-ui-plugin]
Pass `organizationPlugin()` to `` so the organizations settings tab, the `` shell, and `` can read plugin localization and view paths:
```tsx title="components/providers.tsx"
import { AuthProvider } from "@/components/auth/auth-provider"
import { organizationPlugin } from "@/lib/auth/organization-plugin" // [!code highlight]
import { authClient } from "@/lib/auth-client"
{children}
```
### Allow the invitation auth path [#allow-the-invitation-auth-path]
The organization plugin registers a direct invitation view at `/auth/accept-invitation`. Include its auth path in any route allow-list that wraps ``:
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { Auth } from "@/components/auth/auth"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
const validAuthPaths = [
...Object.values(viewPaths.auth),
...Object.values(organizationPlugin().viewPaths.auth) // [!code highlight]
]
```
Invitation links use `/auth/accept-invitation?invitationId=`. Signed-out recipients return to the invitation after signing in, and successful accept or reject actions continue to the configured `redirectTo`.
### Mount the organization switcher [#mount-the-organization-switcher]
Add `` to the application shell, usually next to `` in the header.
It shows the selected organization and provides switch and create actions:
```tsx title="components/header.tsx" {2,7}
import { UserButton } from "@/components/auth/user/user-button"
import { OrganizationSwitcher } from "@/components/auth/organization/organization-switcher"
export function Header() {
return (
)
}
```
### Allow the `organizations` settings path [#allow-the-organizations-settings-path]
The plugin contributes an `organizations` segment to `viewPaths.settings`. Spread `organizationPlugin().viewPaths.settings` into your settings route's allowed-path set so `/settings/organizations` resolves correctly:
TanStack Start
Next.js
```tsx title="routes/settings/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { ensureSession } from "@better-auth-ui/core"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { createFileRoute, notFound, redirect } from "@tanstack/react-router"
import { createIsomorphicFn } from "@tanstack/react-start"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { Settings } from "@/components/auth/settings/settings"
import { auth } from "@/lib/auth"
import { authClient } from "@/lib/auth-client"
import { organizationPlugin } from "@/lib/auth/organization-plugin" // [!code highlight]
const validSettingsPaths = [
...Object.values(viewPaths.settings),
...Object.values(organizationPlugin().viewPaths.settings) // [!code highlight]
]
export const Route = createFileRoute("/settings/$path")({
async beforeLoad({ params: { path }, context: { queryClient }, location }) {
if (!validSettingsPaths.includes(path)) {
throw notFound()
}
const ensureSessionIso = createIsomorphicFn()
.server(() =>
ensureSessionServer(queryClient, auth, { headers: getRequestHeaders() })
)
.client(() => ensureSession(queryClient, authClient))
const session = await ensureSessionIso()
if (!session) {
throw redirect({
to: "/auth/$path",
params: { path: "sign-in" },
search: { redirectTo: location.href }
})
}
return { session }
},
component: SettingsPage
})
function SettingsPage() {
const { path } = Route.useParams()
return (
)
}
```
`/organization/settings` and `/organization/people` now render the org management UI. Internal links from `` and from `` (in the settings tab) wire up automatically.
Users can now create organizations from `/settings/organizations`. They can use the header switcher and manage members or invitations at `/organization/`.
## Slug-based routes [#slug-based-routes]
By default, the switcher stores the selected organization in the session. Enable **slug-based routing** to select the organization from the URL.
For example, use `acme.your-app.com` or `your-app.com/organization/acme/settings`.
This unlocks:
* Shareable, bookmarkable per-org URLs that do not depend on session state
* Server-side prefetching of the correct organization without round-tripping `setActive`
* Letting members keep multiple organizations open in separate tabs without thrashing the active organization
You wire this up by passing the URL slug into `organizationPlugin({ slug })`. The UI plugin then:
1. Drives `useActiveOrganization()` to fetch the org matching that slug (instead of reading the session's active org)
2. Rewrites every link from ``, ``, and the `` tabs to include `//`
3. Swaps the switcher's behavior from `setActive` to `navigate`: clicking an organization takes the user to its slug-prefixed route
Set `slugPrefix` to include a marker in the visible route, such as `/organization/@acme/settings`. Keep `slug` itself unprefixed.
For example, `/organization/@{$slug}/$path` captures `acme`. The plugin adds the prefix to fields, labels, and generated links.
### Read the slug from the URL in your providers [#read-the-slug-from-the-url-in-your-providers]
Read the slug param wherever you render `` and forward it to `organizationPlugin`.
TanStack Start
Next.js
```tsx title="components/providers.tsx"
import { useNavigate, useParams } from "@tanstack/react-router"
import type { ReactNode } from "react"
import { AuthProvider } from "@/components/auth/auth-provider"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
import { authClient } from "@/lib/auth-client"
export function Providers({ children }: { children: ReactNode }) {
const navigate = useNavigate()
const { slug } = useParams({ strict: false }) // [!code highlight]
return (
{children}
)
}
```
```tsx title="components/providers.tsx"
"use client"
import { useParams, useRouter } from "next/navigation"
import type { ReactNode } from "react"
import { AuthProvider } from "@/components/auth/auth-provider"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
import { authClient } from "@/lib/auth-client"
export function Providers({ children }: { children: ReactNode }) {
const router = useRouter()
const params = useParams<{ slug?: string | string[] }>() // [!code highlight]
const slug = typeof params?.slug === "string" ? params.slug : null // [!code highlight]
return (
replace ? router.replace(to) : router.push(to)
}
plugins={[organizationPlugin({ slug })]} // [!code highlight]
>
{children}
)
}
```
On a non-slug page, set `slug` to `null`. This value tells the plugin to use the user's personal account.
Do not leave `slug` as `undefined`. That value uses the organization from the session and can show the wrong organization.
### Add the slug-prefixed organization route [#add-the-slug-prefixed-organization-route]
Move `/organization/$path` → `/organization/$slug/$path`. Validate both segments and gate on session as before.
TanStack Start
Next.js
```tsx title="routes/organization/$slug/$path.tsx"
import { ensureSession } from "@better-auth-ui/core"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { createFileRoute, notFound, redirect } from "@tanstack/react-router"
import { createIsomorphicFn } from "@tanstack/react-start"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { Organization } from "@/components/auth/organization/organization"
import { auth } from "@/lib/auth"
import { authClient } from "@/lib/auth-client"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
const validOrganizationPaths = Object.values(
organizationPlugin().viewPaths.organization
)
export const Route = createFileRoute("/organization/$slug/$path")({
async beforeLoad({ params: { path }, context: { queryClient }, location }) {
if (!validOrganizationPaths.includes(path)) {
throw notFound()
}
const ensureSessionIso = createIsomorphicFn()
.server(() =>
ensureSessionServer(queryClient, auth, { headers: getRequestHeaders() })
)
.client(() => ensureSession(queryClient, authClient))
const session = await ensureSessionIso()
if (!session) {
throw redirect({
to: "/auth/$path",
params: { path: "sign-in" },
search: { redirectTo: location.href }
})
}
return { session }
},
component: OrganizationPage
})
function OrganizationPage() {
const { path } = Route.useParams()
return (
)
}
```
After you configure `organizationPlugin({ slug })`, internal organization links include `//` automatically.
These links include the switcher, organization rows, and the tab bar in ``.
### Customize where the switcher navigates [#customize-where-the-switcher-navigates]
When slug-based routing is enabled, clicking an organization in `` navigates to `/organization//settings` by default, and clicking the personal account navigates to `/settings/account`.
To use a custom destination such as `//dashboard`, pass the `setActive` prop. This prop replaces the plugin's default navigation.
The callback receives the selected `Organization`, or `null` for the personal account:
TanStack Start
Next.js
```tsx title="components/header.tsx"
import { useNavigate } from "@tanstack/react-router"
import { OrganizationSwitcher } from "@/components/auth/organization/organization-switcher"
export function Header() {
const navigate = useNavigate()
return (
{
navigate({
to: organization ? `/${organization.slug}/dashboard` : "/dashboard"
})
}}
/>
)
}
```
```tsx title="components/header.tsx"
"use client"
import { useRouter } from "next/navigation"
import { OrganizationSwitcher } from "@/components/auth/organization/organization-switcher"
export function Header() {
const router = useRouter()
return (
{
router.push(
organization ? `/${organization.slug}/dashboard` : "/dashboard"
)
}}
/>
)
}
```
Without slug-based routing, the default behavior is `authClient.organization.setActive`. Pass `setActive` to intercept the selection and apply custom behavior.
## Hide organization slugs [#hide-organization-slugs]
Set `hideSlug: true` to hide slugs in creation dialogs, profile forms, organization views, and switchers:
```ts
organizationPlugin({ hideSlug: true })
```
Creation still generates a slug from the organization name. Names without Latin letters or digits use a generated identifier instead. The server still validates the slug and its availability. Profile edits preserve the existing slug.
Each component's `hideSlug` prop overrides the plugin configuration. Without either option, forms and organization views show slugs, while switchers hide them.
## Options [#options]
```ts
organizationPlugin({
// Display slugs as @acme and include the prefix in generated URLs.
slugPrefix: "@",
// Disable the slug-availability check that runs while typing a slug.
checkSlug: false,
// Override path segments (defaults shown).
viewPaths: {
auth: { acceptInvitation: "accept-invitation" },
settings: { organizations: "organizations" },
organization: { settings: "settings", people: "people" }
},
// Disable logo upload, or customize the resize / size.
logo: { enabled: false },
// Replace the default role labels.
roles: { owner: "Owner", admin: "Admin", member: "Member" },
// Add labels for custom server roles without redefining built-ins.
additionalRoles: { billing: "Billing" },
// Limit invitations and members to one role.
allowMultipleRoles: false
})
```
## Localization [#localization]
Read these inside custom slot components via `useAuthPlugin(organizationPlugin).localization`.
## React Hooks [#react-hooks]
### Queries [#queries]
* `useActiveOrganization()`: Full organization for the active session (or the URL slug when `organizationPlugin({ slug })` is set)
* `useListOrganizations()`: All organizations the signed-in user belongs to
* `useListOrganizationMembers()`: Members of the active organization
* `useListOrganizationInvitations()`: Pending invitations for the active organization
* `useListUserInvitations()`: Pending invitations addressed to the signed-in user
* `useHasPermission({ permissions })`: Check the current member's permission against the active organization
### Mutations [#mutations]
* `useCreateOrganization()`: Create a new organization
* `useUpdateOrganization()`: Update name / slug / logo of the active organization
* `useDeleteOrganization()`: Delete an organization
* `useSetActiveOrganization()`: Switch the active organization (server-side, persists on session)
* `useLeaveOrganization()`: Leave an organization
* `useInviteMember()`: Invite a member by email
* `useRemoveMember()`: Remove a member
* `useUpdateMemberRole()`: Update a member's role
* `useCancelInvitation()`: Cancel a pending invitation
* `useAcceptInvitation()`: Accept an invitation
* `useRejectInvitation()`: Reject an invitation
* `useCheckSlug()`: Check whether an organization slug is available
## Components [#components]
### `` [#acceptinvitation-]
Direct invitation acceptance view registered by `organizationPlugin()` at `/auth/accept-invitation`. It reads `invitationId` from the query string, preserves the link through sign-in, and lets the recipient accept or reject the pending invitation.
### `` [#organizationswitcher-]
Dropdown that shows the active organization, lets the user switch between organizations, and exposes a "Create organization" entry. Drop it in your app shell: typically next to ``.
```tsx file=/src/demos/shadcn/organization/organization-switcher.tsx
import { OrganizationSwitcher } from "@/components/auth/organization/organization-switcher"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationSwitcherDemo() {
return (
)
}
```
### `` [#organization-]
The full organization management shell mounted at `/organization/`. Renders `settings` (profile + danger zone) and `people` (members + invitations) tabs for the active organization.
```tsx file=/src/demos/shadcn/organization/organization.tsx
import { Organization } from "@/components/auth/organization/organization"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationDemo() {
return (
)
}
```
### `` [#organizationsettings-]
The contents of the `settings` tab: ``, any plugin-contributed `organizationCards` (for example `` from the [api-key plugin](/docs/shadcn/plugins/api-key)), then ``. Drop it into a custom layout if you do not want the tabbed shell.
```tsx file=/src/demos/shadcn/organization/organization-settings.tsx
import { OrganizationSettings } from "@/components/auth/organization/organization-settings"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationSettingsDemo() {
return (
)
}
```
### `` [#organizationprofile-]
Editable profile card for the active organization: logo, display name, and slug. Submits via `useUpdateOrganization`.
```tsx file=/src/demos/shadcn/organization/organization-profile.tsx
import { OrganizationProfile } from "@/components/auth/organization/organization-profile"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationProfileDemo() {
return (
)
}
```
### `` [#organizationdangerzone-]
Danger-zone card with `` and `` rows.
```tsx file=/src/demos/shadcn/organization/organization-danger-zone.tsx
import { OrganizationDangerZone } from "@/components/auth/organization/organization-danger-zone"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationDangerZoneDemo() {
return (
)
}
```
### `` [#organizationpeople-]
The contents of the `people` tab: `` on top, `` below.
```tsx file=/src/demos/shadcn/organization/organization-people.tsx
import { OrganizationPeople } from "@/components/auth/organization/organization-people"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationPeopleDemo() {
return (
)
}
```
### `` [#organizationmembers-]
Searchable, sortable, filter-by-role table of the active organization's members with an invite control and per-row role / remove actions.
```tsx file=/src/demos/shadcn/organization/organization-members.tsx
import { OrganizationMembers } from "@/components/auth/organization/organization-members"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationMembersDemo() {
return (
)
}
```
### Paginating members [#paginating-members]
By default `` asks for the whole list and filters it in
the browser. That is fine for a handful of members and quietly wrong for a
large organization: `list-members` caps its response at 100 rows (or your
`membershipLimit`) and says nothing about the rest, so members past that point
do not appear.
Pass `pageSize` to move paging onto the server:
```tsx
```
In paged mode the component sends `limit` and `offset`, applies the role filter
server-side through `filterField`/`filterValue`, sorts by role through
`sortBy`/`sortDirection`, and reads the row count from the endpoint's `total`.
Two controls step aside in paged mode, because `list-members` cannot honour
them across pages:
* **The search box.** The endpoint has no search parameter. Name and email live
on the joined user row, which it does not query.
* **Sorting by member.** Same reason: it can only sort columns on the member
row itself.
The signed-in user's own role comes from `getActiveMemberRole` rather than from
scanning the loaded page because that member can be on a different page. This is
exposed as `useActiveMemberRole` for custom components.
`list-invitations` and `list-sessions` take no pagination parameters at all, so
those lists stay client-side until Better Auth adds them. `list-api-keys` does
support `limit` and `offset`. Wiring it up is a separate change.
### `` [#organizationinvitations-]
Pending invitations for the active organization as a table aligned with ``. Includes role / status filters, plus per-row resend and cancel actions.
Resending re-invites the same address with `resend: true`, which pushes the
invitation's expiry out and sends the email again instead of creating a second
invitation. The action needs the `invitation: ["create"]` permission, the same
one that gates ``.
```tsx file=/src/demos/shadcn/organization/organization-invitations.tsx
import { OrganizationInvitations } from "@/components/auth/organization/organization-invitations"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationInvitationsDemo() {
return (
)
}
```
### `` [#organizationssettings-]
The user-level organizations panel rendered at `/settings/organizations`. Lists every organization the user belongs to (with a "Create organization" button and empty state) plus invitations addressed to the user.
```tsx file=/src/demos/shadcn/organization/organizations-settings.tsx
import { OrganizationsSettings } from "@/components/auth/organization/organizations-settings"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationsSettingsDemo() {
return (
)
}
```
### `` [#organizations-]
List of organizations the user belongs to with a "Create organization" button and per-row Manage control. Embedded inside ``.
```tsx file=/src/demos/shadcn/organization/organizations.tsx
import { Organizations } from "@/components/auth/organization/organizations"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function OrganizationsDemo() {
return (
)
}
```
### `` [#userinvitations-]
Invitations addressed to the signed-in user across every organization, with Accept / Reject actions. Embedded inside ``.
```tsx file=/src/demos/shadcn/organization/user-invitations.tsx
import { UserInvitations } from "@/components/auth/organization/user-invitations"
import { OrganizationDemoWrapper } from "./organization-demo-wrapper"
export function UserInvitationsDemo() {
return (
)
}
```
### `` [#createorganizationdialog-]
Modal dialog with the new-organization form. Owned by `` and ``. Mount it directly when you want to open the create flow from your own surface.
## Multiple roles per member [#multiple-roles-per-member]
Better Auth accepts `role` as a string or an array. It stores multiple roles as
one comma-joined value (`"admin,member"`). The role pickers allow multiple
roles by default. Set `allowMultipleRoles: false` to limit invitations and
members to one role. The pickers always keep at least one role selected.
Anywhere you read a raw role, split it first. `@better-auth-ui/core/plugins/organization`
exports helpers for this:
```ts
import {
hasMemberRole,
memberRoleLabels,
parseMemberRoles
} from "@better-auth-ui/core/plugins/organization"
parseMemberRoles("admin,member") // ["admin", "member"]
hasMemberRole("owner,admin", "owner") // true
memberRoleLabels("admin,auditor", roles) // ["Admin", "auditor"]
```
`hasMemberRole` matters more than it looks: `member.role === "owner"` silently
misses an owner who also carries a second role, which is what the members list
used to do when deciding whether to show owner-only actions.
### `` [#invitememberdialog-]
Modal dialog for inviting a new member by email. Owned by `` and ``. Mount it directly to drive the invite flow from a custom action.
### `` [#deleteorganizationdialog-]
Confirmation dialog for deleting an organization (owner permission, server-side).
## Dynamic organization roles [#dynamic-organization-roles]
Enable dynamic access control in Better Auth before you enable the roles tab. The server, client, and UI must use the same resource and action keys.
```ts title="lib/organization-access.ts"
import { createAccessControl } from "better-auth/plugins/access"
export const organizationStatements = {
project: ["create", "read", "update", "delete"]
} as const
export const organizationAccess = createAccessControl(organizationStatements)
```
```ts title="lib/auth.ts"
import { organization } from "better-auth/plugins"
import { organizationAccess } from "./organization-access"
organization({
ac: organizationAccess,
dynamicAccessControl: { enabled: true }
})
```
```ts title="lib/auth-client.ts"
import { organizationClient } from "better-auth/client/plugins"
import { organizationAccess } from "./organization-access"
organizationClient({
ac: organizationAccess,
dynamicAccessControl: { enabled: true }
})
```
Configure the labels that the permission matrix shows:
```tsx title="components/providers.tsx"
organizationPlugin({
dynamicAccessControl: {
enabled: true,
permissions: {
project: {
label: "Projects",
actions: {
create: "Create projects",
read: "View projects",
update: "Edit projects",
delete: "Delete projects"
}
}
}
}
})
```
This adds the `roles` organization tab. The tab supports role creation, editing, and deletion. Member and invitation controls include both configured roles and roles returned by Better Auth. A role cannot be deleted while a member holds it.
The React hooks also expose the underlying endpoints. Always pass the organization ID:
```tsx
const roles = useListRoles(authClient, {
query: { organizationId }
})
const role = useRole(authClient, {
query: { organizationId, roleId }
})
const createRole = useCreateRole(authClient, organizationId)
const updateRole = useUpdateRole(authClient, organizationId)
const deleteRole = useDeleteRole(authClient, organizationId)
```
## Teams and policy controls [#teams-and-policy-controls]
Enable teams on the Better Auth server, client, and UI plugin:
```tsx
organization({ teams: { enabled: true } })
organizationClient({ teams: { enabled: true } })
organizationPlugin({
teams: {
maximumTeams: 5,
maximumMembersPerTeam: 20,
allowRemovingAllTeams: false
}
})
```
The organization shell adds a Teams tab. Users can create, rename, and delete teams. They can also add and remove team members. Invitations can assign a team.
The UI disables team creation and member assignment when these limits are reached. It also protects the final team unless `allowRemovingAllTeams` is true. Better Auth also accepts callback limits on the server. The browser cannot run those callbacks, so pass their resolved values into the UI configuration when the limits vary by organization.
The `useListTeams`, `useListTeamMembers`, `useCreateTeam`, `useUpdateTeam`, `useRemoveTeam`, `useAddTeamMember`, and `useRemoveTeamMember` hooks cover the team lifecycle.
### User team selection [#user-team-selection]
`TeamSwitcher` lists only the teams that the current user can access in the specified organization. The application owns the selected team. This keeps team routing independent from Better Auth active organizations.
```tsx
import { useState } from "react"
import { TeamSwitcher } from "@/components/auth/organization/team-switcher"
export function ProjectTeamFilter({ organizationId }: { organizationId: string }) {
const [teamId, setTeamId] = useState(null)
return (
setTeamId(team?.id ?? null)}
/>
)
}
```
Set `syncSession` when an application still reads `session.activeTeamId`. This opt-in mode calls Better Auth `setActiveTeam` after the callback.
The `useListUserTeams` and `useSetActiveTeam` hooks are also available for custom controls. Always pass `organizationId` to both APIs.
### Model-specific fields [#model-specific-fields]
Configure fields by the Better Auth model that stores them. The names and types must match the `schema` fields in the server organization plugin.
```tsx
organizationPlugin({
modelFields: {
organization: [
{ name: "billingCode", type: "string", label: "Billing code" }
],
member: [
{ name: "title", type: "string", label: "Job title" }
],
invitation: [
{ name: "department", type: "string", label: "Department" }
],
team: [
{ name: "description", type: "string", inputType: "textarea", label: "Description" }
],
role: [
{ name: "color", type: "string", label: "Color" }
]
}
})
```
Organization fields appear in creation and profile forms. Invitation, team, and role fields appear in their create and edit workflows. Member fields appear as read-only details because Better Auth does not provide browser client endpoints for creating or editing a member model directly.
Use `organizationLimit`, `membershipLimit`, and `invitationLimit` to disable actions when a configured limit is reached. Use `allowOrganizationCreation: false` to hide creation controls.
Plugins can add an organization settings tab through `organizationTabs`. Each tab receives `organizationId` and `organizationSlug` for the active organization.
# Passkey (/docs/shadcn/plugins/passkey)
The passkey plugin adds passwordless authentication using WebAuthn. Users can sign in with their device authenticator (Touch ID, Face ID, Windows Hello) and manage registered passkeys from their security settings.
It contributes:
* A "Continue with Passkey" button rendered on the sign-in and magic-link views (hidden on sign-up)
* A `` security card for listing, adding, renaming, and deleting registered passkeys
* `useSignInPasskey`, `useAddPasskey`, `useUpdatePasskey`, `useDeletePasskey`, and `useListPasskeys` hooks
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Install the [`@better-auth/passkey`](https://www.better-auth.com/docs/plugins/passkey) package and add it to your Better Auth server config:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { passkey } from "@better-auth/passkey" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
passkey() // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `passkeyClient()` to your auth client so `authClient.signIn.passkey` and `authClient.passkey.*` are available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { passkeyClient } from "@better-auth/passkey/client" // [!code highlight]
export const authClient = createAuthClient({
plugins: [passkeyClient()] // [!code highlight]
})
```
### Install the UI plugin [#install-the-ui-plugin]
Run the shadcn CLI to install the passkey button, passkey management card, and the `passkeyPlugin()` factory into your project:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/passkey
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/passkey
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/passkey
```
```bash
bun x shadcn@latest add @better-auth-ui/passkey
```
This drops the following into your codebase:
* `src/lib/auth/auth-plugin.ts`: local `AuthPlugin` typing widener
* `src/lib/auth/passkey-plugin.ts`: `passkeyPlugin()` factory
* `src/components/auth/passkey/passkey-button.tsx`: the "Continue with Passkey" sign-in button
* `src/components/auth/passkey/passkeys.tsx`: the passkey management card
* `src/components/auth/passkey/passkey.tsx`: individual passkey row
* `src/components/auth/passkey/passkey-skeleton.tsx`: skeleton shown while passkeys load
* `src/components/auth/passkey/passkeys-empty.tsx`: empty state shown when no passkeys exist
* `src/components/auth/passkey/add-passkey-dialog.tsx`: dialog for registering a new passkey
* `src/components/auth/passkey/delete-passkey-dialog.tsx`: confirmation dialog for revoking a passkey
### Register the plugin [#register-the-plugin]
Pass `passkeyPlugin()` to ``:
```tsx title="components/providers.tsx"
import { passkeyPlugin } from "@/lib/auth/passkey-plugin" // [!code highlight]
import { AuthProvider } from "@/components/auth/auth-provider"
{children}
```
## Components [#components]
### `` [#signin-]
A "Continue with Passkey" button is automatically rendered on the `` and `` views when the plugin is registered (hidden on sign-up).
**Usage**
```tsx
import { PasskeyButton } from "@/components/auth/passkey/passkey-button"
```
**Props**
### `` [#passkeys-]
The security settings page shows `` when the layout renders plugin `securityCards`. Add `passkeyPlugin()` to `plugins` to provide this card.
**Usage**
```tsx
import { Passkeys } from "@/components/auth/passkey/passkeys"
```
**Props**
## Passkey registration policy [#passkey-registration-policy]
By default, the add-passkey dialog does not set `authenticatorAttachment`.
The browser and operating system show the available passkey options.
Set a preference in the plugin when all registrations must use one authenticator type:
```ts
passkeyPlugin({ authenticatorAttachment: "platform" })
passkeyPlugin({ authenticatorAttachment: "cross-platform" })
```
`"platform"` prefers the current device. `"cross-platform"` prefers a security key or another device. The dialog does not show an attachment selector.
`useAddPasskey` accepts every parameter exposed by `authClient.passkey.addPasskey`. Use this hook for custom registration flows:
```tsx
const { mutate: addPasskey } = useAddPasskey(
authClient as PasskeyAuthClient
)
addPasskey({
name: "Work laptop",
authenticatorAttachment: "platform",
extensions: { credProps: true },
returnWebAuthnResponse: true
})
```
`residentKey` and `userVerification` are server plugin policies. They are not
parameters of `authClient.passkey.addPasskey`, so the client UI does not expose
them.
## Passkey autofill [#passkey-autofill]
With the plugin registered, the sign-in form asks the browser to offer saved
passkeys straight from its autofill dropdown, so most people never press the
passkey button at all. This is the WebAuthn conditional UI flow.
Two pieces make it work, and the built-in components already handle both:
* The identifier and password fields carry `webauthn` as the last token of
their `autocomplete` attribute, added by `withPasskeyAutoFill`.
* `` calls `usePasskeyAutoFill`, which opens a conditionally
mediated request once the browser reports that it supports one.
Browsers without conditional mediation ignore the extra token and never get the
request, so the button stays as the fallback everywhere.
Turn the whole thing off with:
```ts
passkeyPlugin({ autoFill: false })
```
If you write your own sign-in form, add the token and start the conditional
request:
```tsx
import type { PasskeyAuthClient } from "@better-auth-ui/core/plugins/passkey"
import {
isPasskeyAutoFillEnabled,
withPasskeyAutoFill
} from "@better-auth-ui/core/plugins/passkey"
import { usePasskeyAutoFill } from "@better-auth-ui/react/plugins/passkey"
const { authClient, plugins } = useAuth()
const passkeyAutoFill = isPasskeyAutoFillEnabled(plugins)
usePasskeyAutoFill(authClient as PasskeyAuthClient)
```
`navigator.credentials.get()` accepts an `AbortSignal`. The bundled hook calls
the Better Auth passkey client, which does not expose that signal. Unmounting
the form only stops a pending availability probe. It does not cancel a request
that already started.
If your custom implementation calls `navigator.credentials.get()` directly,
pass an `AbortSignal` and abort it during cleanup.
## Options [#options]
```ts
passkeyPlugin({
// Omit this option to let the browser show all available choices.
authenticatorAttachment: "platform",
// Override any of the plugin's localization strings.
localization: {
passkeys: "Security Keys"
}
})
```
## Localization [#localization]
# Phone Number (/docs/shadcn/plugins/phone-number)
The phone-number plugin adds a dedicated `/auth/phone-number` view. It supports passwordless verification codes, phone number and password sign-in, password recovery, and a verified phone-number card in account settings. Its country selector formats national input as the user types, validates it, and sends an E.164 number to Better Auth.
Keep server-side phone validation as a trust boundary. The UI sends E.164 numbers, but clients can bypass UI validation.
## Setup [#setup]
### Configure Better Auth [#configure-better-auth]
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { phoneNumber } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
phoneNumber({
otpLength: 6,
requireVerification: true,
sendOTP: ({ phoneNumber, code }) => {
void sms.send({ to: phoneNumber, body: `Your code is ${code}` })
},
sendPasswordResetOTP: ({ phoneNumber, code }) => {
void sms.send({ to: phoneNumber, body: `Your reset code is ${code}` })
}
})
]
})
```
Do not log codes in production. Better Auth recommends returning from `sendOTP` without waiting for the SMS provider. Use your runtime's background-task primitive when delivery must outlive the request.
### Update the database [#update-the-database]
Generate or migrate the Better Auth schema using your existing database workflow. The plugin adds nullable `phoneNumber` and `phoneNumberVerified` fields to the user model. Keep `phoneNumber` unique.
For Drizzle projects, regenerate the Better Auth schema first, then generate the Drizzle migration from that schema. Do not hand-edit generated migration metadata.
### Add the client plugin [#add-the-client-plugin]
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { phoneNumberClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [phoneNumberClient()]
})
```
### Install and register the UI plugin [#install-and-register-the-ui-plugin]
```bash
bunx --bun shadcn@latest add @better-auth-ui/phone-number
```
```tsx title="components/providers.tsx"
import { phoneNumberPlugin } from "@/lib/auth/phone-number-plugin"
{children}
```
### Allow the plugin routes [#allow-the-plugin-routes]
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { phoneNumberPlugin } from "@/lib/auth/phone-number-plugin"
const validAuthPaths = new Set([
...Object.values(viewPaths.auth),
...Object.values(phoneNumberPlugin().viewPaths.auth)
])
```
Use `validAuthPaths` in the route guard that renders ``.
## Match UI options to server options [#match-ui-options-to-server-options]
| UI option | Default | Server requirement |
| ------------------- | ------: | ----------------------------------------------------------------- |
| `signIn` | `true` | `sendOTP` |
| `passwordSignIn` | `false` | A password credential. Use `requireVerification` when appropriate |
| `passwordReset` | `false` | `sendPasswordResetOTP` |
| `changePhoneNumber` | `true` | `sendOTP`. Verification uses `updatePhoneNumber: true` |
| `otpLength` | `6` | Must match Better Auth `otpLength` |
Use `defaultCountry`, `countries`, and `locale` to control the selector. Supply an `adapter` when your application needs different formatting or validation rules.
When both sign-in modes are enabled, the form lets users switch between a code and a password. If password sign-in returns `PHONE_NUMBER_NOT_VERIFIED`, it moves directly to the code step because Better Auth has already started verification.
Passwordless verification is not a second factor. Better Auth applies 2FA to phone number and password sign-in, but not to passwordless phone verification.
## Account creation [#account-creation]
Set Better Auth `signUpOnVerification` to create a user after it verifies an unknown number. This option requires a temporary email generator.
The Better Auth user model still requires an email address.
If the user schema requires more fields, replace the `phoneNumber` view with a custom component. Pass the collected values to `authClient.phoneNumber.verify`.
## Components [#components]
* `` handles code and password sign-in.
* `` and `` handle phone password recovery.
* `` adds, replaces, verifies, and removes the current user's phone number.
## Options [#options]
## Localization [#localization]
## Headless mutations [#headless-mutations]
The UI uses [`useSendPhoneNumberOtp`](/docs/react/mutations/send-phone-number-otp), [`useVerifyPhoneNumber`](/docs/react/mutations/verify-phone-number), [`useSignInPhoneNumber`](/docs/react/mutations/sign-in-phone-number), [`useRequestPhoneNumberPasswordReset`](/docs/react/mutations/request-phone-number-password-reset), and [`useResetPhoneNumberPassword`](/docs/react/mutations/reset-phone-number-password).
See the [Better Auth phone-number plugin](https://better-auth.com/docs/plugins/phone-number) for server options, external OTP verification, attempt limits, and endpoint behavior.
# Sign In With Ethereum (/docs/shadcn/plugins/siwe)
The SIWE registry item adds wallet sign-in, optional email collection, and a security settings card for connected wallets.
## Setup [#setup]
### Configure Better Auth [#configure-better-auth]
Add `siwe()` to the server. Provide a secure nonce generator and an ERC-4361 verifier. Apply the plugin schema before sign-in.
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { siwe } from "better-auth/plugins"
import { verifyMessage } from "viem"
import { generateSiweNonce } from "viem/siwe"
export const auth = betterAuth({
plugins: [
siwe({
domain: "app.example.com",
getNonce: async () => generateSiweNonce(),
verifyMessage: async ({ message, signature, address }) =>
verifyMessage({
address: address as `0x${string}`,
message,
signature: signature as `0x${string}`
})
})
]
})
```
See the [Better Auth SIWE guide](https://www.better-auth.com/docs/plugins/siwe) for the complete server setup.
### Add the client plugin [#add-the-client-plugin]
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { siweClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({ plugins: [siweClient()] })
```
### Install the registry item [#install-the-registry-item]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/siwe
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/siwe
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/siwe
```
```bash
bun x shadcn@latest add @better-auth-ui/siwe
```
### Register the UI plugin [#register-the-ui-plugin]
```tsx title="components/providers.tsx"
import { createEip1193WalletConnector } from "@better-auth-ui/core/plugins/siwe"
import { siwePlugin } from "@/lib/auth/siwe-plugin"
{children}
```
## Wallet settings [#wallet-settings]
Better Auth does not expose browser endpoints for SIWE wallet management. Create authenticated server routes and connect them through `SiweWalletManager`.
Resolve the user from the server session. Do not accept a user ID from the browser for wallet operations.
```ts title="lib/wallet-manager.ts"
import type {
SiweWalletAccount,
SiweWalletLinkChallenge,
SiweWalletManager
} from "@better-auth-ui/core/plugins/siwe"
const assertOk = async (response: Response) => {
if (!response.ok) {
throw new Error(`Wallet request failed with status ${response.status}.`)
}
}
const get = async (
url: string,
signal?: AbortSignal
): Promise => {
const response = await fetch(url, { signal })
await assertOk(response)
return response.json() as Promise
}
const post = async (
url: string,
body: unknown
): Promise => {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
})
await assertOk(response)
if (response.status === 204) return undefined as TResponse
return response.json() as Promise
}
export const walletManager: SiweWalletManager = {
list: (signal) => get("/api/wallets", signal),
createLinkChallenge: (wallet) =>
post("/api/wallets/link-challenge", wallet),
link: (proof) => post("/api/wallets/link", proof),
unlink: (walletId) => fetch(`/api/wallets/${walletId}`, { method: "DELETE" }).then(assertOk),
setPrimary: (walletId) => fetch(`/api/wallets/${walletId}/primary`, { method: "POST" }).then(assertOk)
}
```
Pass `walletManager` to `siwePlugin()`. The registry item then adds connect, list, primary, and remove controls.
The link challenge must use a single-use nonce. Verify its domain, chain, address, nonce, and signature before attaching the wallet.
Use `email: "required"` to require an email. Use `email: "none"` to open the wallet without an email dialog.
The wallet signature does not verify the supplied email address.
# SSO (/docs/shadcn/plugins/sso)
The SSO registry item installs a sign-in view that discovers organization SSO from an email address. It redirects to the identity provider when one exists. Otherwise, it shows password, magic-link, email-OTP, and other registered sign-in methods.
## Setup [#setup]
### Configure Better Auth [#configure-better-auth]
Install `@better-auth/sso`, add `sso()` to the server, add `ssoClient()` to the client, and apply the plugin schema. See the [Better Auth SSO guide](https://www.better-auth.com/docs/plugins/sso) for provider setup.
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { sso } from "@better-auth/sso"
export const auth = betterAuth({
plugins: [sso()]
})
```
```ts title="lib/auth-client.ts"
import { ssoClient } from "@better-auth/sso/client"
import { createAuthClient } from "better-auth/react"
export const authClient = createAuthClient({
plugins: [ssoClient()]
})
```
### Install the registry item [#install-the-registry-item]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/sso
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/sso
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/sso
```
```bash
bun x shadcn@latest add @better-auth-ui/sso
```
This installs `EmailFirstSignIn` and the local `ssoPlugin()` factory.
### Register the UI plugin [#register-the-ui-plugin]
Place `ssoPlugin()` before another plugin that replaces the sign-in view.
```tsx title="components/providers.tsx"
import { ssoPlugin } from "@/lib/auth/sso-plugin"
{children}
```
The flow stores the submitted email in session storage. The installed magic-link and email-OTP views use it as their initial email.
# Theme (/docs/shadcn/plugins/theme)
The theme plugin adds theme selection to your authentication UI. Users can switch between system, light, and dark themes from the user button dropdown and account settings.
## Setup [#setup]
### Install the UI plugin [#install-the-ui-plugin]
Run the shadcn CLI to install the theme plugin components:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/theme
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/theme
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/theme
```
```bash
bun x shadcn@latest add @better-auth-ui/theme
```
### Register the UI plugin [#register-the-ui-plugin]
The plugin works with any theme library. Pass the theme library's hook, such as `useTheme` from [next-themes](https://github.com/pacocoursey/next-themes).
The slot components can then read the current theme inside ``. You do not need another wrapper.
```tsx title="components/providers.tsx"
import { ThemeProvider, useTheme } from "next-themes"
import type { ReactNode } from "react"
import { AuthProvider } from "@/components/auth/auth-provider"
import { authClient } from "@/lib/auth-client"
import { themePlugin } from "@/lib/auth/theme-plugin" // [!code highlight]
export function Providers({ children }: { children: ReactNode }) {
return (
{children}
)
}
```
The plugin calls `useTheme()` inside its slot components during each render. The call stays inside `` when both providers share a component.
### Or pass static theme state [#or-pass-static-theme-state]
If the theme source has no hook, pass both `theme` and `setTheme`. The source can use `useState` or a custom controller.
The plugin runs during each parent render. The slot components update when the state owner renders with a new value.
```tsx title="components/providers.tsx"
const [theme, setTheme] = useState("system") // [!code highlight]
{children}
```
The two forms are mutually exclusive: you pass either `useTheme` **or** the `theme`/`setTheme` pair.
## Components [#components]
### `` [#userbutton-]
### `` [#appearance-]
The `` card is automatically rendered in `` when the plugin is registered.
**Usage**
```tsx
import { Appearance } from "@/components/auth/theme/appearance"
```
**Props**
## Options [#options]
## Localization [#localization]
# Two Factor (/docs/shadcn/plugins/two-factor)
The two-factor plugin adds a second step to password sign-in. Better Auth withholds the session until that step succeeds, answering the sign-in request with `{ twoFactorRedirect: true, twoFactorMethods }` instead.
It contributes:
* A `` view at `/auth/two-factor` covering authenticator codes, emailed codes, backup codes, and "trust this device"
* A `` card in security settings for enrolling, showing the QR code, and managing backup codes
* Mutation hooks for every two-factor endpoint (`useEnableTwoFactor`, `useDisableTwoFactor`, `useVerifyTotp`, `useSendTwoFactorOtp`, `useVerifyTwoFactorOtp`, `useVerifyBackupCode`, `useGenerateBackupCodes`, `useGetTotpUri`)
The copied sign-in forms detect `twoFactorRedirect` for you and route to the challenge with `redirectTo` preserved.
Better Auth does not apply two-factor to passwordless sign-in. Magic link, email OTP, passkeys, and OAuth all bypass the challenge: the second factor only guards password (and username) sign-in.
## Setup [#setup]
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Add the [2FA](https://www.better-auth.com/docs/plugins/2fa) plugin to your server config. Wire `otpOptions.sendOTP` if you want to offer emailed codes as a second factor:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { twoFactor } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
twoFactor({ // [!code highlight]
issuer: "My App", // [!code highlight]
otpOptions: { // [!code highlight]
sendOTP: async ({ user, otp }) => { // [!code highlight]
// Email `otp` to `user.email`. // [!code highlight]
} // [!code highlight]
} // [!code highlight]
}) // [!code highlight]
]
})
```
### Migrate your database [#migrate-your-database]
The plugin adds a `twoFactor` table and a `twoFactorEnabled` field on `user`:
```bash
npx @better-auth/cli generate
npx @better-auth/cli migrate
```
### Install the matching client plugin [#install-the-matching-client-plugin]
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { twoFactorClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [twoFactorClient()] // [!code highlight]
})
```
Leave `twoFactorPage` and `onTwoFactorRedirect` unset: the copied components handle the redirect themselves and keep `redirectTo` intact, while `twoFactorPage` forces a full page reload.
### Install the UI plugin [#install-the-ui-plugin]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add @better-auth-ui/two-factor
```
```bash
pnpm dlx shadcn@latest add @better-auth-ui/two-factor
```
```bash
yarn dlx shadcn@latest add @better-auth-ui/two-factor
```
```bash
bun x shadcn@latest add @better-auth-ui/two-factor
```
This drops the following into your codebase:
* `src/lib/auth/two-factor-plugin.ts`: the `twoFactorPlugin()` factory
* `src/lib/auth/use-sign-in-continuation.ts`: shared post-sign-in handler
* `src/lib/auth/two-factor-methods.ts`: local redirect metadata storage and validation
* `src/lib/auth/use-two-factor-password.ts`: decides whether to ask for a password
* `src/components/auth/otp-field.tsx`: the shared code input
* `src/components/auth/two-factor/two-factor-challenge.tsx`: the challenge view
* `src/components/auth/two-factor/two-factor-settings.tsx`: the settings card
* `src/components/auth/two-factor/*-dialog.tsx`: enable, disable, and regenerate dialogs
* `src/components/auth/two-factor/backup-codes.tsx`: the one-time code display
The registry also refreshes the email/password and username sign-in forms so both detect the two-factor redirect.
### Register the plugin [#register-the-plugin]
```tsx title="components/providers.tsx"
import { twoFactorPlugin } from "@/lib/auth/two-factor-plugin" // [!code highlight]
import { AuthProvider } from "@/components/auth/auth-provider"
{children}
```
### Allow the new view path [#allow-the-new-view-path]
```tsx title="routes/auth/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { createFileRoute, notFound } from "@tanstack/react-router"
import { Auth } from "@/components/auth/auth"
import { twoFactorPlugin } from "@/lib/auth/two-factor-plugin" // [!code highlight]
export const Route = createFileRoute("/auth/$path")({
beforeLoad({ params: { path } }) {
if (
!Object.values({
...viewPaths.auth,
...twoFactorPlugin().viewPaths?.auth // [!code highlight]
}).includes(path)
) {
throw notFound()
}
},
component: AuthPage
})
function AuthPage() {
const { path } = Route.useParams()
return
}
```
## The sign-in flow [#the-sign-in-flow]
```text
email/password or username/password
↓
{ twoFactorRedirect: true, twoFactorMethods: ["totp", "otp"] }
↓
/auth/two-factor?redirectTo=…
↓ authenticator code, emailed code, or backup code
authenticated session
```
The method names ride along in session storage: names only, never a code or token. The two-factor cookie that authorizes the challenge stays HTTP-only, exactly as Better Auth set it.
Building a custom sign-in form? Check for the redirect yourself:
```tsx
import {
isTwoFactorRedirect,
storeTwoFactorMethods
} from "@/lib/auth/two-factor-methods"
const { mutate: signInEmail } = useSignInEmail(authClient, {
onSuccess: (data) => {
if (isTwoFactorRedirect(data)) {
storeTwoFactorMethods(data.twoFactorMethods)
navigate({ to: "/auth/two-factor" })
return
}
navigate({ to: redirectTo })
}
})
```
## Components [#components]
### `` [#twofactorchallenge-]
Offers the methods the sign-in response reported, plus backup-code recovery and an optional "trust this device" checkbox. Emailed codes are sent on request rather than automatically, so a user with an authenticator app never triggers a pointless email.
### `` [#twofactorsettings-]
Added to `` automatically. Users can enroll with an authenticator app or a delivered code. Enrolled users can regenerate backup codes or turn two-factor off.
Backup codes live in component state and are never written to storage or the query cache: once the dialog closes they are gone.
## Delivered-code enrollment [#delivered-code-enrollment]
The enrollment dialog offers authenticator apps by default. Configure OTP delivery on the server before you add the delivered-code option:
```ts
// Server
twoFactor({
otpOptions: { sendOTP }
})
```
```tsx
// UI
twoFactorPlugin({ enrollmentMethods: ["totp", "otp"] })
```
Better Auth activates OTP enrollment immediately. The dialog closes after `authClient.twoFactor.enable({ method: "otp" })` succeeds.
## Passwordless accounts [#passwordless-accounts]
Set `allowPasswordless` on both sides to let passkey-only users manage two-factor without a password:
```ts
// Server
twoFactor({ allowPasswordless: true })
```
```tsx
// UI
twoFactorPlugin({ allowPasswordless: true })
```
The UI still asks for a password when the account has a credential account, matching the server's rule. It reads the linked accounts to decide, so users who do have a password are not offered a shortcut around it.
## Options [#options]
## Localization [#localization]
Read these from `useAuthPlugin(twoFactorPlugin).localization` inside custom slot components.
## Email template [#email-template]
Pair `otpOptions.sendOTP` with the [``](/docs/shadcn/components/email/otp-email) component for a styled code email.
# Username (/docs/shadcn/plugins/username)
The username plugin adds username-based authentication to your auth UI. Users can sign in with a username instead of an email address, and optionally check username availability during sign-up and profile updates.
It contributes:
* A `` view that accepts both username and email, routing to the appropriate sign-in method
* A `` renderer for the username additional field with real-time availability checking
* `useSignInUsername` and `useIsUsernameAvailable` hooks
* Automatic username field injection into sign-up and user profile forms
## Setup [#setup]
The username plugin requires no additional UI installation: the components are built-in. You only need to configure the Better Auth server plugin and register the client plugin.
### Install the Better Auth plugin [#install-the-better-auth-plugin]
Install the [`better-auth`](https://www.better-auth.com/docs/plugins/username) package and add it to your Better Auth server config:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { username } from "better-auth/plugins" // [!code highlight]
export const auth = betterAuth({
// ...
plugins: [
username() // [!code highlight]
]
})
```
### Install the matching client plugin [#install-the-matching-client-plugin]
Add `usernameClient()` to your auth client so `authClient.signIn.username` and `authClient.username.*` are available:
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/react"
import { usernameClient } from "better-auth/client/plugins" // [!code highlight]
export const authClient = createAuthClient({
plugins: [usernameClient()] // [!code highlight]
})
```
### Register the UI plugin [#register-the-ui-plugin]
Pass `usernamePlugin()` to ``:
```tsx title="components/providers.tsx"
import { usernamePlugin } from "@/lib/auth/username-plugin" // [!code highlight]
import { AuthProvider } from "@/components/auth/auth-provider"
{children}
```
## Components [#components]
### `` [#signin-]
### `` [#signup-]
### `` [#userprofile-]
## Options [#options]
Use `usernamePrefix` when usernames are displayed with a marker such as `@`. The prefix appears inside username fields but is not included in the value sent to Better Auth.
```tsx
usernamePlugin({
usernamePrefix: "@",
localization: {
usernamePlaceholder: "username"
}
})
```
## Localization [#localization]
# Overview (/docs/solid)
`@better-auth-ui/solid` provides the Solid hooks and provider configuration. Core packages provide shared options factories and query helpers.
Plugin hooks are available from `@better-auth-ui/solid/plugins/*`. Server helpers are available from core server entrypoints.
Use [Zaidan](/docs/zaidan) to install copied Solid components or configure TanStack Start routes. Use this section for the Solid package APIs.
## Quick path [#quick-path]
1. Install the Solid package and Solid Query.
2. Create a Better Auth client with `createAuthClient`.
3. Wrap your app with `AuthProvider` and pass a `QueryClient`.
4. Use base hooks from `@better-auth-ui/solid`, plugin hooks from `@better-auth-ui/solid/plugins/`, and shared option factories/helpers from `@better-auth-ui/core`.
5. Use the core server entrypoints when a server boundary needs authentication data.
npm
pnpm
yarn
bun
```bash
npm install @better-auth-ui/solid @tanstack/solid-query better-auth solid-js
```
```bash
pnpm add @better-auth-ui/solid @tanstack/solid-query better-auth solid-js
```
```bash
yarn add @better-auth-ui/solid @tanstack/solid-query better-auth solid-js
```
```bash
bun add @better-auth-ui/solid @tanstack/solid-query better-auth solid-js
```
## Package exports [#package-exports]
| Export | Owns |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `better-auth/solid` | Better Auth Solid client factory, including `createAuthClient`. |
| `@better-auth-ui/solid` | `AuthProvider`, auth config types, provider icons, base query/mutation helpers, and hooks such as `useSession`. |
| `@better-auth-ui/solid/plugins/` | Optional plugin hooks such as API key, passkey, multi-session, organization, magic-link, and username APIs. |
| `@better-auth-ui/core` / `@better-auth-ui/core/server` | Core-owned option factories plus client/server-safe query helpers and auth server types for SSR boundaries. |
| `@better-auth-ui/core/plugins//server` | Plugin server-auth helpers for SSR boundaries that call your Better Auth server instance directly. |
## What belongs elsewhere [#what-belongs-elsewhere]
* Use [Zaidan](/docs/zaidan) for component installation, generated payloads, and copied-file ownership.
* Use [Zaidan integrations](/docs/zaidan/integrations/tanstack-start) for TanStack Start application setup.
* Use [Zaidan Email](/docs/zaidan/components/email/email-verification-email) for native Solid email templates.
* Use the Zaidan documentation for registry generation. This package reference does not describe React or HeroUI components.
## Next [#next]
Review Solid Query factories, cache keys, and loader helpers.
Use mutation option factories for Better Auth write endpoints.
Use server-safe helpers to prefetch session data at SSR boundaries.
Switch tracks when you need installable Solid UI.
# useAcceptInvitation (/docs/solid/mutations/accept-invitation)
## Usage [#usage]
```tsx
import { useAcceptInvitation } from "@better-auth-ui/solid/plugins/organization"
const mutation = useAcceptInvitation(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { acceptInvitationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => acceptInvitationOptions(authClient))
```
## Params [#params]
# useAddPasskey (/docs/solid/mutations/add-passkey)
Requires the Better Auth passkey plugin.
## Usage [#usage]
```tsx
import { addPasskeyOptions } from "@better-auth-ui/core/plugins/passkey"
import { useMutation } from "@tanstack/solid-query"
const addPasskey = useMutation(() => addPasskeyOptions(authClient))
addPasskey.mutate({ name: "Work laptop" })
```
## Params [#params]
# approveDeviceOptions (/docs/solid/mutations/approve-device)
Requires `deviceAuthorizationClient()` from `better-auth/client/plugins`. Call this mutation only after the current session has verified and claimed the user code.
## Options factory [#options-factory]
```tsx
import { approveDeviceOptions } from "@better-auth-ui/core/plugins/device-authorization"
import { createMutation } from "@tanstack/solid-query"
const approveDevice = createMutation(() => approveDeviceOptions(authClient))
approveDevice.mutate({
userCode: "ABCD1234"
})
```
## Params [#params]
# useCancelInvitation (/docs/solid/mutations/cancel-invitation)
## Usage [#usage]
```tsx
import { useCancelInvitation } from "@better-auth-ui/solid/plugins/organization"
const mutation = useCancelInvitation(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { cancelInvitationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => cancelInvitationOptions(authClient))
```
## Params [#params]
# changeEmailOtpOptions (/docs/solid/mutations/change-email-otp)
Requires the Better Auth email-OTP plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `EmailOtpAuthClient` from `@better-auth-ui/core/plugins/email-otp`.
The address on the session changes, so the session query is refetched on success.
## Options factory [#options-factory]
```tsx
import { changeEmailOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { createMutation } from "@tanstack/solid-query"
const changeEmail = createMutation(() => changeEmailOtpOptions(authClient))
changeEmail.mutate({ newEmail: "new@example.com", otp: "123456" })
```
## Params [#params]
# useChangeEmail (/docs/solid/mutations/change-email)
## Usage [#usage]
```tsx
import { changeEmailOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const changeEmail = useMutation(() => changeEmailOptions(authClient))
changeEmail.mutate({ newEmail: "alice@example.com" })
```
## Params [#params]
# useChangePassword (/docs/solid/mutations/change-password)
## Usage [#usage]
```tsx
import { changePasswordOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const changePassword = useMutation(() => changePasswordOptions(authClient))
changePassword.mutate({ currentPassword: "old", newPassword: "new" })
```
## Params [#params]
# useCheckSlug (/docs/solid/mutations/check-organization-slug)
## Usage [#usage]
```tsx
import { useCheckSlug } from "@better-auth-ui/solid/plugins/organization"
const mutation = useCheckSlug(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { checkSlugOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => checkSlugOptions(authClient))
```
## Params [#params]
# useCreateApiKey (/docs/solid/mutations/create-api-key)
Requires the Better Auth API key plugin.
## Usage [#usage]
```tsx
import { useCreateApiKey } from "@better-auth-ui/solid/plugins/api-key"
const createApiKey = useCreateApiKey(authClient)
createApiKey.mutate({ name: "CLI" })
```
## Options factory [#options-factory]
```tsx
import { createApiKeyOptions } from "@better-auth-ui/core/plugins/api-key"
import { useMutation } from "@tanstack/solid-query"
const createApiKey = useMutation(() => createApiKeyOptions(authClient, userId))
```
## Params [#params]
# useCreateOrganization (/docs/solid/mutations/create-organization)
## Usage [#usage]
```tsx
import { useCreateOrganization } from "@better-auth-ui/solid/plugins/organization"
const mutation = useCreateOrganization(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { createOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => createOrganizationOptions(authClient))
```
## Params [#params]
# useDeleteApiKey (/docs/solid/mutations/delete-api-key)
Requires the Better Auth API key plugin.
## Usage [#usage]
```tsx
import { useDeleteApiKey } from "@better-auth-ui/solid/plugins/api-key"
const deleteApiKey = useDeleteApiKey(authClient)
deleteApiKey.mutate({ keyId: "api-key-id" })
```
## Options factory [#options-factory]
```tsx
import { deleteApiKeyOptions } from "@better-auth-ui/core/plugins/api-key"
import { useMutation } from "@tanstack/solid-query"
const deleteApiKey = useMutation(() => deleteApiKeyOptions(authClient, userId))
```
## Params [#params]
# useDeleteOAuthConsent (/docs/solid/mutations/delete-oauth-consent)
Requires `oauthProviderClient()` from `@better-auth/oauth-provider/client`.
## Usage [#usage]
```tsx
import { useDeleteOAuthConsent } from "@better-auth-ui/solid/plugins/oauth-provider"
const deleteConsent = useDeleteOAuthConsent(authClient)
for (const id of application.consentIds) {
await deleteConsent.mutateAsync({ id })
}
```
Better Auth can store more than one consent record per OAuth client. Group the records with `groupOAuthConsents` and delete every ID in the group so the application really has to ask again.
Deleting a consent removes the stored approval, so the application needs the user's approval before it receives new access. It does not revoke access tokens or refresh tokens that were already issued: those stay valid until they expire. Better Auth's consent deletion endpoint does not offer complete token revocation semantics, so do not tell users their access was cut off.
On success the user's consent list is invalidated and refetched, so a removal that fails halfway still shows the server's real state.
## Options factory [#options-factory]
```ts
import { deleteOAuthConsentOptions } from "@better-auth-ui/core/plugins/oauth-provider"
import { createMutation } from "@tanstack/solid-query"
const deleteConsent = createMutation(() =>
deleteOAuthConsentOptions(authClient)
)
```
## Params [#params]
# useDeleteOrganization (/docs/solid/mutations/delete-organization)
## Usage [#usage]
```tsx
import { useDeleteOrganization } from "@better-auth-ui/solid/plugins/organization"
const mutation = useDeleteOrganization(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { deleteOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => deleteOrganizationOptions(authClient))
```
## Params [#params]
# useDeletePasskey (/docs/solid/mutations/delete-passkey)
Requires the Better Auth passkey plugin.
## Usage [#usage]
```tsx
import { deletePasskeyOptions } from "@better-auth-ui/core/plugins/passkey"
import { useMutation } from "@tanstack/solid-query"
const deletePasskey = useMutation(() => deletePasskeyOptions(authClient))
deletePasskey.mutate({ id: "passkey-id" })
```
## Params [#params]
# useDeleteUser (/docs/solid/mutations/delete-user)
Requires the Better Auth delete-user capability to be enabled on the server.
## Usage [#usage]
```tsx
import { deleteUserOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const deleteUser = useMutation(() => deleteUserOptions(authClient))
deleteUser.mutate()
```
## Params [#params]
# denyDeviceOptions (/docs/solid/mutations/deny-device)
Requires `deviceAuthorizationClient()` from `better-auth/client/plugins`. Call this mutation after the current session has verified and claimed the user code.
## Options factory [#options-factory]
```tsx
import { denyDeviceOptions } from "@better-auth-ui/core/plugins/device-authorization"
import { createMutation } from "@tanstack/solid-query"
const denyDevice = createMutation(() => denyDeviceOptions(authClient))
denyDevice.mutate({
userCode: "ABCD1234"
})
```
## Params [#params]
# disableTwoFactorOptions (/docs/solid/mutations/disable-two-factor)
Requires the Better Auth two-factor plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `TwoFactorAuthClient` from `@better-auth-ui/core/plugins/two-factor`.
Refetches the session so `user.twoFactorEnabled` is up to date.
## Options factory [#options-factory]
```tsx
import { disableTwoFactorOptions } from "@better-auth-ui/core/plugins/two-factor"
import { createMutation } from "@tanstack/solid-query"
const disable = createMutation(() => disableTwoFactorOptions(authClient))
disable.mutate({ password: "current-password" })
```
## Params [#params]
# enableTwoFactorOptions (/docs/solid/mutations/enable-two-factor)
Requires the Better Auth two-factor plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `TwoFactorAuthClient` from `@better-auth-ui/core/plugins/two-factor`.
Resolves with the TOTP URI and the backup codes. Unless the server sets `skipVerificationOnEnable`, two-factor only becomes active once a TOTP code verifies.
## Options factory [#options-factory]
```tsx
import { enableTwoFactorOptions } from "@better-auth-ui/core/plugins/two-factor"
import { createMutation } from "@tanstack/solid-query"
const enable = createMutation(() => enableTwoFactorOptions(authClient))
enable.mutate({ password: "current-password" })
```
## Params [#params]
# generateBackupCodesOptions (/docs/solid/mutations/generate-backup-codes)
Requires the Better Auth two-factor plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `TwoFactorAuthClient` from `@better-auth-ui/core/plugins/two-factor`.
The new codes are returned once. Keep them in component state and let the user copy them: they are never returned again.
## Options factory [#options-factory]
```tsx
import { generateBackupCodesOptions } from "@better-auth-ui/core/plugins/two-factor"
import { createMutation } from "@tanstack/solid-query"
const generateCodes = createMutation(() => generateBackupCodesOptions(authClient))
generateCodes.mutate({ password: "current-password" })
```
## Params [#params]
# getTotpUriOptions (/docs/solid/mutations/get-totp-uri)
Requires the Better Auth two-factor plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `TwoFactorAuthClient` from `@better-auth-ui/core/plugins/two-factor`.
This operation is a mutation because the endpoint uses POST. It accepts the password and returns a secret that the application must not cache.
## Options factory [#options-factory]
```tsx
import { getTotpUriOptions } from "@better-auth-ui/core/plugins/two-factor"
import { createMutation } from "@tanstack/solid-query"
const getTotpUri = createMutation(() => getTotpUriOptions(authClient))
getTotpUri.mutate({ password: "current-password" })
```
## Params [#params]
# Mutations (/docs/solid/mutations)
Solid provides `use*` hooks for components. Shared options factories come from `@better-auth-ui/core`. Plugin factories come from their core plugin entrypoints.
```ts
import { useSignInEmail } from "@better-auth-ui/solid"
import { signInEmailOptions } from "@better-auth-ui/core"
```
Use the hook in components. Use the core factory where Solid Query accepts a `mutationOptions` object.
## Error handling [#error-handling]
Each mutation adds `throw: true` to `fetchOptions`. The promise rejects with `BetterFetchError` instead of resolving with `{ error }`.
You can therefore use the standard `error`, `isError`, `throwOnError`, and `onError` values from `useMutation`.
```tsx
import { signInEmailOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const signIn = useMutation(() => ({
...signInEmailOptions(authClient),
onError: (err) => toast.error(err.message)
}))
```
## Cache side effects [#cache-side-effects]
Factories that change authentication state use the same mutation keys as React. `AuthProvider` installs an invalidator for the standard cache updates.
Common side effects mirror the React docs:
* `signInEmailOptions` / `signInUsernameOptions` / `signInPasskeyOptions` / `signUpEmailOptions`: reset the session query so it retrieves the new session.
* `signInSocialOptions` / `signInMagicLinkOptions`: redirect without a cache update.
* `signOutOptions`: remove every `["auth", ...]` query.
* `useUpdateUser`: update the current user's profile fields and refresh the cached session.
* `setActiveSessionOptions`: refresh the session and device-session queries after the active session changes.
* `changeEmailOptions`: refresh the session.
* `addPasskeyOptions` / `deletePasskeyOptions`: refresh the passkey list.
* `revokeSessionOptions`: refresh the sessions list.
* `revokeMultiSessionOptions`: refresh the device sessions list.
* `unlinkAccountOptions`: refresh the linked accounts list.
* Organization hook mutations expose invalidation metadata consumed by `AuthProvider`, so organization/member/invitation queries are invalidated with the same shared cache keys.
The Solid package does not render toast messages. Add success and error handling in the application, or use Zaidan components with toast integration.
## Tracking mutation state globally [#tracking-mutation-state-globally]
All mutation keys start with `"auth"`. The shared `authMutationKeys` factory in `@better-auth-ui/core` exposes these keys.
Use this factory instead of inline tuples. Call sites and mutation factories will then use the same keys:
```ts
import { authMutationKeys } from "@better-auth-ui/core"
const authPending = queryClient.isMutating({
mutationKey: authMutationKeys.all
})
const signInPending = queryClient.isMutating({
mutationKey: authMutationKeys.signIn.all
})
const emailSignInPending = queryClient.isMutating({
mutationKey: authMutationKeys.signIn.email
})
```
Each grouping (`signIn`, `signUp`, `passkey`, `multiSession`) exposes an `all` prefix so you can match a whole feature at once.
Match inside a `MutationCache` observer for global toasts or analytics:
```ts
import { authMutationKeys } from "@better-auth-ui/core"
import { MutationCache } from "@tanstack/solid-query"
new MutationCache({
onError: (error, _vars, _ctx, mutation) => {
if (mutation.options.mutationKey?.[0] === authMutationKeys.all[0]) {
toast.error(error.message)
}
}
})
```
## Escape hatch [#escape-hatch]
Use `useAuthMutation` for a mutation endpoint that has no specific factory or hook. Use `useAuthQuery` for a read endpoint.
```tsx
import { useAuthMutation } from "@better-auth-ui/solid"
const mutation = useAuthMutation(
authClient.emailOtp.sendVerificationOtp,
["auth", "emailOtp", "sendVerificationOtp"]
)
mutation.mutate({ email: "user@example.com", type: "sign-in" })
```
TypeScript infers variables from the `authFn` parameter. Required parameters prevent an empty `mutate()` call, while optional parameters permit it.
The factory adds `throw: true` to `fetchOptions`. Therefore, `onError` and `error` receive a `BetterFetchError`.
For shared mutation registration, global mutation state checks, a `MutationCache` observer, or manual `useMutation`, use the endpoint's option factory from core directly:
```ts
import { changePasswordOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => changePasswordOptions(authClient))
```
For endpoints that already have a key in `authMutationKeys`, prefer it over an inline tuple so cache observers and mutation state checks line up.
## Available mutations [#available-mutations]
### Auth [#auth]
### Settings [#settings]
### Organization [#organization]
# useInviteMember (/docs/solid/mutations/invite-member)
## Usage [#usage]
```tsx
import { useInviteMember } from "@better-auth-ui/solid/plugins/organization"
const mutation = useInviteMember(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { inviteMemberOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => inviteMemberOptions(authClient))
```
## Params [#params]
# useIsUsernameAvailable (/docs/solid/mutations/is-username-available)
Requires the Better Auth username plugin. Import your configured `authClient`. It already carries the username methods from `createAuthClient`.
This check is modeled as a mutation because callers usually trigger it from user input, not from route-level data loading. Returned data is typed as `{ available: boolean; message: string | null }`.
## Usage [#usage]
```tsx
import { useIsUsernameAvailable } from "@better-auth-ui/solid/plugins/username"
const mutation = useIsUsernameAvailable(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { isUsernameAvailableOptions } from "@better-auth-ui/core/plugins/username"
import { useMutation } from "@tanstack/solid-query"
const checkUsername = useMutation(() => isUsernameAvailableOptions(authClient))
checkUsername.mutate({ username: "alice" })
```
## Params [#params]
# useLeaveOrganization (/docs/solid/mutations/leave-organization)
## Usage [#usage]
```tsx
import { useLeaveOrganization } from "@better-auth-ui/solid/plugins/organization"
const mutation = useLeaveOrganization(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { leaveOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => leaveOrganizationOptions(authClient))
```
## Params [#params]
# useLinkSocial (/docs/solid/mutations/link-social)
## Usage [#usage]
```tsx
import { linkSocialOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const linkSocial = useMutation(() => linkSocialOptions(authClient))
linkSocial.mutate({ provider: "github" })
```
## Params [#params]
# useOAuthConsent (/docs/solid/mutations/oauth-consent)
Requires `oauthProviderClient()` from `@better-auth/oauth-provider/client`. Better Auth reads and validates the signed authorization query from the current browser URL before completing the redirect.
## Usage [#usage]
```tsx
import { useOAuthConsent } from "@better-auth-ui/solid/plugins/oauth-provider"
const consent = useOAuthConsent(authClient)
consent.mutate({ accept: true })
consent.mutate({ accept: false })
```
Pass only the user's decision for the complete requested scope set. The OAuth Provider plugin handles the redirect.
Do not navigate to `redirect_uri` from the application.
## Options factory [#options-factory]
```tsx
import { oauthConsentOptions } from "@better-auth-ui/core/plugins/oauth-provider"
import { createMutation } from "@tanstack/solid-query"
const consent = createMutation(() => oauthConsentOptions(authClient))
consent.mutate({ accept: true })
consent.mutate({ accept: false })
```
## Params [#params]
# oauthContinueOptions (/docs/solid/mutations/oauth-continue)
Requires `oauthProviderClient()` from `@better-auth/oauth-provider/client`.
Better Auth's [redirect screens](https://better-auth.com/docs/plugins/oauth-provider#redirect-screens) hand control to your app so it can create an account, pick an account, or run its own post-login step. When that step finishes, this mutation tells Better Auth to pick the authorization back up.
## Options factory [#options-factory]
```tsx
import { oauthContinueOptions } from "@better-auth-ui/core/plugins/oauth-provider"
import { createMutation } from "@tanstack/solid-query"
const oauthContinue = createMutation(() => oauthContinueOptions(authClient))
// After the account was created during this flow (`prompt=create`)
oauthContinue.mutate({ created: true })
// After the user picked an account (`prompt=select_account`)
oauthContinue.mutate({ selected: true })
// After your own post-login selection screen
await oauthContinue.mutateAsync({ postLogin: true })
```
Set exactly one flag per call: the one matching the screen that just finished.
The signed authorization query has to stay in the browser URL. `oauthProviderClient()` reads it from there and forwards it, so never rebuild the query string or navigate to `redirect_uri` yourself.
Only call `{ created: true }` when the account was created during the current flow and the sign-up left a usable session. An already signed-in user is not a newly created one, and email verification or a social sign-up needs its own resume step.
## Params [#params]
# useRejectInvitation (/docs/solid/mutations/reject-invitation)
## Usage [#usage]
```tsx
import { useRejectInvitation } from "@better-auth-ui/solid/plugins/organization"
const mutation = useRejectInvitation(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { rejectInvitationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => rejectInvitationOptions(authClient))
```
## Params [#params]
# useRemoveMember (/docs/solid/mutations/remove-member)
## Usage [#usage]
```tsx
import { useRemoveMember } from "@better-auth-ui/solid/plugins/organization"
const mutation = useRemoveMember(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { removeMemberOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => removeMemberOptions(authClient))
```
## Params [#params]
# requestEmailChangeOtpOptions (/docs/solid/mutations/request-email-change-otp)
Requires the Better Auth email-OTP plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `EmailOtpAuthClient` from `@better-auth-ui/core/plugins/email-otp`.
Sends a code to the new address. When the server runs with `changeEmail: { verifyCurrentEmail: true }`, pass the `otp` the user received at their current address too.
## Options factory [#options-factory]
```tsx
import { requestEmailChangeOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { createMutation } from "@tanstack/solid-query"
const requestChange = createMutation(() => requestEmailChangeOtpOptions(authClient))
requestChange.mutate({ newEmail: "new@example.com" })
```
## Params [#params]
# requestPasswordResetOtpOptions (/docs/solid/mutations/request-password-reset-otp)
Requires the Better Auth email-OTP plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `EmailOtpAuthClient` from `@better-auth-ui/core/plugins/email-otp`.
Pair with `resetPasswordOtpOptions`, which takes the code and the new password together: no reset link is involved.
## Options factory [#options-factory]
```tsx
import { requestPasswordResetOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { createMutation } from "@tanstack/solid-query"
const requestReset = createMutation(() => requestPasswordResetOtpOptions(authClient))
requestReset.mutate({ email: "alice@example.com" })
```
## Params [#params]
# useRequestPasswordReset (/docs/solid/mutations/request-password-reset)
## Usage [#usage]
```tsx
import { useRequestPasswordReset } from "@better-auth-ui/solid"
const requestReset = useRequestPasswordReset(authClient)
requestReset.mutate({
email: "alice@example.com",
redirectTo: "/auth/reset-password"
})
```
## Options factory [#options-factory]
```tsx
import { requestPasswordResetOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const requestReset = useMutation(() => requestPasswordResetOptions(authClient))
```
## Params [#params]
# useRequestPhoneNumberPasswordReset (/docs/solid/mutations/request-phone-number-password-reset)
Configure Better Auth `sendPasswordResetOTP` before exposing this action.
## Usage [#usage]
```tsx
import type { PhoneNumberAuthClient } from "@better-auth-ui/core/plugins/phone-number"
import { useAuth } from "@better-auth-ui/solid"
import { useRequestPhoneNumberPasswordReset } from "@better-auth-ui/solid/plugins/phone-number"
const { authClient } = useAuth()
const requestReset = useRequestPhoneNumberPasswordReset(
authClient as PhoneNumberAuthClient
)
requestReset.mutate({ phoneNumber: "+12025550123" })
```
## Options factory [#options-factory]
```tsx
import { requestPhoneNumberPasswordResetOptions } from "@better-auth-ui/core/plugins/phone-number"
import { useMutation } from "@tanstack/solid-query"
const requestReset = useMutation(() =>
requestPhoneNumberPasswordResetOptions(authClient)
)
```
## Params [#params]
# resetPasswordOtpOptions (/docs/solid/mutations/reset-password-otp)
Requires the Better Auth email-OTP plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `EmailOtpAuthClient` from `@better-auth-ui/core/plugins/email-otp`.
There is no token in the URL: the code and the new password are submitted in one call.
## Options factory [#options-factory]
```tsx
import { resetPasswordOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { createMutation } from "@tanstack/solid-query"
const resetPassword = createMutation(() => resetPasswordOtpOptions(authClient))
resetPassword.mutate({
email: "alice@example.com",
otp: "123456",
password: "new-password"
})
```
## Params [#params]
# useResetPassword (/docs/solid/mutations/reset-password)
## Usage [#usage]
```tsx
import { useResetPassword } from "@better-auth-ui/solid"
const mutation = useResetPassword(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { resetPasswordOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const resetPassword = useMutation(() => resetPasswordOptions(authClient))
const token = new URLSearchParams(window.location.search).get("token")
const newPassword = formPasswordValue
if (token) {
resetPassword.mutate({ token, newPassword })
}
```
## Params [#params]
# useResetPhoneNumberPassword (/docs/solid/mutations/reset-phone-number-password)
## Usage [#usage]
```tsx
import type { PhoneNumberAuthClient } from "@better-auth-ui/core/plugins/phone-number"
import { useAuth } from "@better-auth-ui/solid"
import { useResetPhoneNumberPassword } from "@better-auth-ui/solid/plugins/phone-number"
const { authClient } = useAuth()
const resetPassword = useResetPhoneNumberPassword(
authClient as PhoneNumberAuthClient
)
resetPassword.mutate({
phoneNumber: "+12025550123",
otp: "123456",
newPassword
})
```
## Options factory [#options-factory]
```tsx
import { resetPhoneNumberPasswordOptions } from "@better-auth-ui/core/plugins/phone-number"
import { useMutation } from "@tanstack/solid-query"
const resetPassword = useMutation(() =>
resetPhoneNumberPasswordOptions(authClient)
)
```
## Params [#params]
# useRevokeMultiSession (/docs/solid/mutations/revoke-multi-session)
Requires the Better Auth multi-session plugin.
## Usage [#usage]
```tsx
import { useRevokeMultiSession } from "@better-auth-ui/solid/plugins/multi-session"
const revokeMultiSession = useRevokeMultiSession(authClient)
revokeMultiSession.mutate({ sessionToken: "session-token" })
```
## Options factory [#options-factory]
```tsx
import { revokeMultiSessionOptions } from "@better-auth-ui/core/plugins/multi-session"
import { useMutation } from "@tanstack/solid-query"
const revokeMultiSession = useMutation(() =>
revokeMultiSessionOptions(authClient, userId)
)
```
## Params [#params]
# useRevokeSession (/docs/solid/mutations/revoke-session)
## Usage [#usage]
```tsx
import { revokeSessionOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const revokeSession = useMutation(() => revokeSessionOptions(authClient))
revokeSession.mutate({ token: "session-token" })
```
## Cache [#cache]
Refetch `listSessions` after revocation.
## Params [#params]
# useSendPhoneNumberOtp (/docs/solid/mutations/send-phone-number-otp)
## Usage [#usage]
```tsx
import type { PhoneNumberAuthClient } from "@better-auth-ui/core/plugins/phone-number"
import { useAuth } from "@better-auth-ui/solid"
import { useSendPhoneNumberOtp } from "@better-auth-ui/solid/plugins/phone-number"
const { authClient } = useAuth()
const sendOtp = useSendPhoneNumberOtp(
authClient as PhoneNumberAuthClient
)
sendOtp.mutate({ phoneNumber: "+12025550123" })
```
## Options factory [#options-factory]
```tsx
import { sendPhoneNumberOtpOptions } from "@better-auth-ui/core/plugins/phone-number"
import { useMutation } from "@tanstack/solid-query"
const sendOtp = useMutation(() => sendPhoneNumberOtpOptions(authClient))
```
## Params [#params]
# sendTwoFactorOtpOptions (/docs/solid/mutations/send-two-factor-otp)
Requires the Better Auth two-factor plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `TwoFactorAuthClient` from `@better-auth-ui/core/plugins/two-factor`.
Authenticated by the two-factor cookie Better Auth set during sign-in, so it only works while a challenge is pending.
## Options factory [#options-factory]
```tsx
import { sendTwoFactorOtpOptions } from "@better-auth-ui/core/plugins/two-factor"
import { createMutation } from "@tanstack/solid-query"
const sendOtp = createMutation(() => sendTwoFactorOtpOptions(authClient))
sendOtp.mutate({})
```
## Params [#params]
# useSendVerificationEmail (/docs/solid/mutations/send-verification-email)
## Usage [#usage]
```tsx
import { useSendVerificationEmail } from "@better-auth-ui/solid"
const sendVerificationEmail = useSendVerificationEmail(authClient)
sendVerificationEmail.mutate({
email: "alice@example.com",
callbackURL: "/dashboard"
})
```
## Options factory [#options-factory]
```tsx
import { sendVerificationEmailOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const sendVerificationEmail = useMutation(() =>
sendVerificationEmailOptions(authClient)
)
```
## Params [#params]
# sendVerificationOtpOptions (/docs/solid/mutations/send-verification-otp)
Requires the Better Auth email-OTP plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `EmailOtpAuthClient` from `@better-auth-ui/core/plugins/email-otp`.
One endpoint backs every email-OTP flow. `type` picks which one: `"sign-in"`, `"email-verification"`, `"forget-password"`, or `"change-email"`.
## Options factory [#options-factory]
```tsx
import { sendVerificationOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { createMutation } from "@tanstack/solid-query"
const sendCode = createMutation(() => sendVerificationOtpOptions(authClient))
sendCode.mutate({ email: "alice@example.com", type: "sign-in" })
```
## Params [#params]
# useSetActiveOrganization (/docs/solid/mutations/set-active-organization)
## Usage [#usage]
```tsx
import { useSetActiveOrganization } from "@better-auth-ui/solid/plugins/organization"
const mutation = useSetActiveOrganization(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { setActiveOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => setActiveOrganizationOptions(authClient))
```
## Params [#params]
# useSetActiveSession (/docs/solid/mutations/set-active-session)
Requires the Better Auth multi-session plugin.
## Usage [#usage]
```tsx
import { useSetActiveSession } from "@better-auth-ui/solid/plugins/multi-session"
const setActiveSession = useSetActiveSession(authClient)
setActiveSession.mutate({ sessionToken: "session-token" })
```
## Options factory [#options-factory]
```tsx
import { setActiveSessionOptions } from "@better-auth-ui/core/plugins/multi-session"
import { useMutation } from "@tanstack/solid-query"
const setActiveSession = useMutation(() =>
setActiveSessionOptions(authClient, userId)
)
```
## Cache [#cache]
The core options metadata refetches the session and device-session queries after switching.
## Params [#params]
# signInEmailOtpOptions (/docs/solid/mutations/sign-in-email-otp)
Requires the Better Auth email-OTP plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `EmailOtpAuthClient` from `@better-auth-ui/core/plugins/email-otp`.
Refetches the session on success. Send the code with `sendVerificationOtpOptions` first.
## Options factory [#options-factory]
```tsx
import { signInEmailOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { createMutation } from "@tanstack/solid-query"
const signIn = createMutation(() => signInEmailOtpOptions(authClient))
signIn.mutate({ email: "alice@example.com", otp: "123456" })
```
## Params [#params]
# useSignInEmail (/docs/solid/mutations/sign-in-email)
The email sign-in mutation calls `authClient.signIn.email` with the supplied credentials.
With `AuthProvider`, a successful sign-in invalidates the session query and waits for active session queries to refetch.
## Usage [#usage]
```tsx
import { useSignInEmail } from "@better-auth-ui/solid"
const mutation = useSignInEmail(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { signInEmailOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const signIn = useMutation(() => signInEmailOptions(authClient))
signIn.mutate({
email: "alice@example.com",
password: passwordInput,
rememberMe: true
})
```
`signInEmailOptions` supplies the mutation key, request function, and session invalidation metadata.
Without `AuthProvider` or `setupMutationInvalidation`, handle session invalidation in the mutation's `onSuccess` callback. This requirement applies to both the hook and the options factory.
## Params [#params]
# useSignInMagicLink (/docs/solid/mutations/sign-in-magic-link)
Requires the Better Auth magic-link plugin. Import your configured `authClient`. It already carries the magic-link methods from `createAuthClient`.
## Usage [#usage]
```tsx
import { useSignInMagicLink } from "@better-auth-ui/solid/plugins/magic-link"
const sendMagicLink = useSignInMagicLink(authClient)
sendMagicLink.mutate({
email: "alice@example.com",
callbackURL: "/dashboard"
})
```
## Options factory [#options-factory]
```tsx
import { signInMagicLinkOptions } from "@better-auth-ui/core/plugins/magic-link"
import { useMutation } from "@tanstack/solid-query"
const sendMagicLink = useMutation(() => signInMagicLinkOptions(authClient))
```
## Params [#params]
# useSignInPasskey (/docs/solid/mutations/sign-in-passkey)
Requires the Better Auth passkey plugin. Import your configured `authClient`. It already carries the passkey methods from `createAuthClient`.
## Usage [#usage]
```tsx
import { useSignInPasskey } from "@better-auth-ui/solid/plugins/passkey"
const mutation = useSignInPasskey(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { signInPasskeyOptions } from "@better-auth-ui/core/plugins/passkey"
import { useMutation } from "@tanstack/solid-query"
const signInWithPasskey = useMutation(() => signInPasskeyOptions(authClient))
signInWithPasskey.mutate()
```
## Params [#params]
# useSignInPhoneNumber (/docs/solid/mutations/sign-in-phone-number)
## Usage [#usage]
```tsx
import type { PhoneNumberAuthClient } from "@better-auth-ui/core/plugins/phone-number"
import { useAuth } from "@better-auth-ui/solid"
import { useSignInPhoneNumber } from "@better-auth-ui/solid/plugins/phone-number"
const { authClient } = useAuth()
const signIn = useSignInPhoneNumber(
authClient as PhoneNumberAuthClient
)
signIn.mutate({
phoneNumber: "+12025550123",
password,
rememberMe: true
})
```
With Better Auth `requireVerification`, an unverified credential returns `PHONE_NUMBER_NOT_VERIFIED` and starts verification.
## Options factory [#options-factory]
```tsx
import { signInPhoneNumberOptions } from "@better-auth-ui/core/plugins/phone-number"
import { useMutation } from "@tanstack/solid-query"
const signIn = useMutation(() => signInPhoneNumberOptions(authClient))
```
## Params [#params]
# useSignInSocial (/docs/solid/mutations/sign-in-social)
Starts the provider OAuth flow and redirects through Better Auth.
## Usage [#usage]
```tsx
import { useSignInSocial } from "@better-auth-ui/solid"
const mutation = useSignInSocial(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { signInSocialOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const signInSocial = useMutation(() => signInSocialOptions(authClient))
signInSocial.mutate({
provider: "github",
callbackURL: "/dashboard"
})
```
## Params [#params]
# useSignInUsername (/docs/solid/mutations/sign-in-username)
Requires the Better Auth username plugin. Import your configured `authClient`. It already carries the username methods from `createAuthClient`.
## Usage [#usage]
```tsx
import { useSignInUsername } from "@better-auth-ui/solid/plugins/username"
const mutation = useSignInUsername(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { signInUsernameOptions } from "@better-auth-ui/core/plugins/username"
import { useMutation } from "@tanstack/solid-query"
const signIn = useMutation(() => signInUsernameOptions(authClient))
signIn.mutate({ username: "alice", password: passwordInput })
```
## Params [#params]
# useSignOut (/docs/solid/mutations/sign-out)
## Usage [#usage]
```tsx
import { useSignOut } from "@better-auth-ui/solid"
const signOut = useSignOut(authClient)
signOut.mutate()
```
## Options factory [#options-factory]
```tsx
import { signOutOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const signOut = useMutation(() => signOutOptions(authClient))
```
The core options metadata removes `authQueryKeys.all` after sign-out when the Better Auth mutation invalidator is installed by `AuthProvider`.
## Params [#params]
# useSignUpEmail (/docs/solid/mutations/sign-up-email)
## Usage [#usage]
```tsx
import { useSignUpEmail } from "@better-auth-ui/solid"
const mutation = useSignUpEmail(authClient)
mutation.mutate(/* params */)
```
## Options factory [#options-factory]
```tsx
import { signUpEmailOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const signUp = useMutation(() => signUpEmailOptions(authClient))
signUp.mutate({
email: "alice@example.com",
password: passwordInput,
name: "Alice"
})
```
Invalidate `authQueryKeys.session` in your `onSuccess` handler when your screen needs the new session immediately after sign-up.
## Params [#params]
# useUnlinkAccount (/docs/solid/mutations/unlink-account)
## Usage [#usage]
```tsx
import { unlinkAccountOptions } from "@better-auth-ui/core"
import { useMutation } from "@tanstack/solid-query"
const unlinkAccount = useMutation(() => unlinkAccountOptions(authClient))
unlinkAccount.mutate({ providerId: "github", accountId: "github-account-id" })
```
## Cache [#cache]
Refetch linked accounts after unlinking.
## Params [#params]
# useUpdateMemberRole (/docs/solid/mutations/update-member-role)
## Usage [#usage]
```tsx
import { useUpdateMemberRole } from "@better-auth-ui/solid/plugins/organization"
const mutation = useUpdateMemberRole(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { updateMemberRoleOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => updateMemberRoleOptions(authClient))
```
## Params [#params]
# useUpdateOrganization (/docs/solid/mutations/update-organization)
## Usage [#usage]
```tsx
import { useUpdateOrganization } from "@better-auth-ui/solid/plugins/organization"
const mutation = useUpdateOrganization(authClient)
mutation.mutate(/* params */)
```
Organization mutations require `organizationClient()` on the Better Auth client. They attach the same `meta.invalidates` / `meta.awaits` cache metadata used by the shared React implementation, and the Solid `AuthProvider` installs a mutation invalidator that refreshes affected organization/session caches after successful auth mutations.
## Options factory [#options-factory]
```tsx
import { updateOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { useMutation } from "@tanstack/solid-query"
const mutation = useMutation(() => updateOrganizationOptions(authClient))
```
## Params [#params]
# useUpdateUser (/docs/solid/mutations/update-user)
## Usage [#usage]
```tsx
import { useUpdateUser } from "@better-auth-ui/solid"
const updateUser = useUpdateUser(authClient)
updateUser.mutate({ name: "Alice" })
```
## Cache [#cache]
The shared mutation invalidator awaits session invalidation after a successful update so cached user data refreshes.
## Params [#params]
# verifyBackupCodeOptions (/docs/solid/mutations/verify-backup-code)
Requires the Better Auth two-factor plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `TwoFactorAuthClient` from `@better-auth-ui/core/plugins/two-factor`.
Each code works once: the server consumes it on success. Refetches the session.
## Options factory [#options-factory]
```tsx
import { verifyBackupCodeOptions } from "@better-auth-ui/core/plugins/two-factor"
import { createMutation } from "@tanstack/solid-query"
const verifyBackupCode = createMutation(() => verifyBackupCodeOptions(authClient))
verifyBackupCode.mutate({ code: "a1b2-c3d4" })
```
## Params [#params]
# verifyDeviceCodeOptions (/docs/solid/mutations/verify-device-code)
Requires `deviceAuthorizationClient()` from `better-auth/client/plugins`. Verification is modeled as a mutation because it claims the code for the signed-in session, even though Better Auth exposes the underlying endpoint through `authClient.device`.
## Options factory [#options-factory]
```tsx
import { verifyDeviceCodeOptions } from "@better-auth-ui/core/plugins/device-authorization"
import { createMutation } from "@tanstack/solid-query"
const verifyDeviceCode = createMutation(() =>
verifyDeviceCodeOptions(authClient)
)
verifyDeviceCode.mutate({
query: {
user_code: "ABCD1234"
}
})
```
The returned `status` indicates whether the request still needs approval or has already been approved or denied.
## Params [#params]
# verifyEmailOtpOptions (/docs/solid/mutations/verify-email-otp)
Requires the Better Auth email-OTP plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `EmailOtpAuthClient` from `@better-auth-ui/core/plugins/email-otp`.
Verification signs the user in, so the session query is refetched on success.
## Options factory [#options-factory]
```tsx
import { verifyEmailOtpOptions } from "@better-auth-ui/core/plugins/email-otp"
import { createMutation } from "@tanstack/solid-query"
const verifyEmail = createMutation(() => verifyEmailOtpOptions(authClient))
verifyEmail.mutate({ email: "alice@example.com", otp: "123456" })
```
## Params [#params]
# useVerifyPhoneNumber (/docs/solid/mutations/verify-phone-number)
## Usage [#usage]
```tsx
import type { PhoneNumberAuthClient } from "@better-auth-ui/core/plugins/phone-number"
import { useAuth } from "@better-auth-ui/solid"
import { useVerifyPhoneNumber } from "@better-auth-ui/solid/plugins/phone-number"
const { authClient } = useAuth()
const verify = useVerifyPhoneNumber(
authClient as PhoneNumberAuthClient
)
verify.mutate({
phoneNumber: "+12025550123",
code: "123456"
})
```
Pass `updatePhoneNumber: true` while authenticated to replace the current phone number. Session data refreshes after success.
## Options factory [#options-factory]
```tsx
import { verifyPhoneNumberOptions } from "@better-auth-ui/core/plugins/phone-number"
import { useMutation } from "@tanstack/solid-query"
const verify = useMutation(() => verifyPhoneNumberOptions(authClient))
```
## Params [#params]
# verifyTotpOptions (/docs/solid/mutations/verify-totp)
Requires the Better Auth two-factor plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `TwoFactorAuthClient` from `@better-auth-ui/core/plugins/two-factor`.
Used both to finish a pending sign-in challenge and to confirm enrollment. Verification is what creates the session, so it refetches the session query.
## Options factory [#options-factory]
```tsx
import { verifyTotpOptions } from "@better-auth-ui/core/plugins/two-factor"
import { createMutation } from "@tanstack/solid-query"
const verifyTotp = createMutation(() => verifyTotpOptions(authClient))
verifyTotp.mutate({ code: "123456", trustDevice: true })
```
## Params [#params]
# verifyTwoFactorOtpOptions (/docs/solid/mutations/verify-two-factor-otp)
Requires the Better Auth two-factor plugin. If TypeScript cannot infer the plugin methods from your client, type or cast `authClient` as `TwoFactorAuthClient` from `@better-auth-ui/core/plugins/two-factor`.
Send the code with `sendTwoFactorOtpOptions` first. Refetches the session on success.
## Options factory [#options-factory]
```tsx
import { verifyTwoFactorOtpOptions } from "@better-auth-ui/core/plugins/two-factor"
import { createMutation } from "@tanstack/solid-query"
const verifyOtp = createMutation(() => verifyTwoFactorOtpOptions(authClient))
verifyOtp.mutate({ code: "123456", trustDevice: true })
```
## Params [#params]
# useAccountInfo (/docs/solid/queries/account-info)
Use `useAccountInfo` when a settings screen needs provider metadata for the current user's linked account. It is user-scoped and waits for the active session.
## Usage [#usage]
```tsx
import { useAccountInfo } from "@better-auth-ui/solid"
const github = useAccountInfo(authClient, () => ({
query: { accountId: "github-account-id" }
}))
```
## Options factory and loader helpers [#options-factory-and-loader-helpers]
```ts
import {
accountInfoOptions,
ensureAccountInfo,
fetchAccountInfo,
prefetchAccountInfo
} from "@better-auth-ui/core"
const options = accountInfoOptions(authClient, userId, {
query: { accountId }
})
await ensureAccountInfo(queryClient, authClient, userId, { query: { accountId } })
await prefetchAccountInfo(queryClient, authClient, userId, { query: { accountId } })
const info = await fetchAccountInfo(queryClient, authClient, userId, { query: { accountId } })
```
## Invalidation [#invalidation]
```ts
import { accountInfoOptions } from "@better-auth-ui/core"
queryClient.invalidateQueries({
queryKey: accountInfoOptions(authClient, userId, {
query: { accountId: "acc_123" }
}).queryKey
})
```
## Params [#params]
# useActiveOrganization (/docs/solid/queries/active-organization)
## Usage [#usage]
```tsx
import { useActiveOrganization } from "@better-auth-ui/solid/plugins/organization"
const result = useActiveOrganization(authClient)
```
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Options factory [#options-factory]
```tsx
import { activeOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { createQuery } from "@tanstack/solid-query"
const query = createQuery(() =>
activeOrganizationOptions(authClient, userId)
)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* An explicit `query.organizationId` or `query.organizationSlug` takes precedence over the plugin slug.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For Solid route loaders, import the router-loader helpers from `@better-auth-ui/core/plugins/organization`. These helpers use the Solid client-shaped `authClient`/`userId` signature so they hydrate into the same cache keys as the component hook. When you want to call your Better Auth server instance directly, matching server-auth Organization helpers are available from `@better-auth-ui/core/plugins/organization/server`.
```ts
import {
ensureActiveOrganization,
fetchActiveOrganization,
prefetchActiveOrganization
} from "@better-auth-ui/core/plugins/organization"
await ensureActiveOrganization(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
await prefetchActiveOrganization(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
const organization = await fetchActiveOrganization(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
```
## Params [#params]
# useAuthenticate (/docs/solid/queries/authenticate)
`useAuthenticate` calls [`useSession`](/docs/solid/queries/session) and, once the query settles, redirects unauthenticated users to the configured sign-in path. The current URL is preserved as a `redirectTo` query parameter so the user lands back where they started after signing in.
Use this as the primary guard inside protected route components.
## Usage [#usage]
```tsx
import { useAuthenticate } from "@better-auth-ui/solid"
export function AccountPage() {
const session = useAuthenticate(authClient)
if (session.isPending) return
if (!session.data) return null // navigating to sign-in
return
}
```
The redirect uses `basePaths.auth` + `viewPaths.auth.signIn` from `AuthProvider`: override those to target a custom sign-in view.
Accepts the same arguments as `useSession`: see its [Params](/docs/solid/queries/session#params).
## First-render caveat [#first-render-caveat]
The redirect runs inside a Solid `createEffect`, so it is browser-only. During the first render, `session.data` is `undefined` and navigation has not started.
The protected interface can appear briefly before the browser effect redirects to sign-in.
You have two ways to handle this:
### Gate on `isPending` (simplest) [#gate-on-ispending-simplest]
Render a skeleton or spinner while the session query is pending, and `null` once it resolves to unauthenticated (the redirect is already in flight). This is what the [Usage](#usage) example does and is enough for most apps:
```tsx
const session = useAuthenticate(authClient)
if (session.isPending) return
if (!session.data) return null // navigating to sign-in
return
```
No flash of protected content, no server work required: just a brief loading state on first mount. Works identically for SSR, client-rendered, and statically prerendered routes.
### Pair with a server-side guard (no loading state) [#pair-with-a-server-side-guard-no-loading-state]
To render the protected interface immediately without a skeleton, prefetch the session on the server. Then hydrate it into the query cache:
* **TanStack Start**: check the session in a server route guard or loader, then hydrate the session query for the protected route.
* **Solid server runtimes**: use the server-side session helpers from [`useSession`](/docs/solid/queries/session#server-side-prefetching) with the request headers for the current request.
The server-side check protects the first render. `useAuthenticate` redirects after token expiration, remote sign-out, or server-side session revocation.
# useFullOrganization (/docs/solid/queries/full-organization)
## Usage [#usage]
```tsx
import { useFullOrganization } from "@better-auth-ui/solid/plugins/organization"
const result = useFullOrganization(authClient)
```
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Options factory [#options-factory]
```tsx
import { fullOrganizationOptions } from "@better-auth-ui/core/plugins/organization"
import { createQuery } from "@tanstack/solid-query"
const query = createQuery(() =>
fullOrganizationOptions(authClient, userId)
)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For Solid route loaders, import the router-loader helpers from `@better-auth-ui/core/plugins/organization`. These helpers use the Solid client-shaped `authClient`/`userId` signature so they hydrate into the same cache keys as the component hook. When you want to call your Better Auth server instance directly, matching server-auth Organization helpers are available from `@better-auth-ui/core/plugins/organization/server`.
```ts
import {
ensureFullOrganization,
fetchFullOrganization,
prefetchFullOrganization
} from "@better-auth-ui/core/plugins/organization"
await ensureFullOrganization(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
await prefetchFullOrganization(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
const organization = await fetchFullOrganization(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
```
## Params [#params]
# useGetApiKey (/docs/solid/queries/get-api-key)
Requires the Better Auth API key plugin. This query calls the browser
`authClient.apiKey.get` endpoint and waits for the current session.
## Usage [#usage]
```tsx
import { useGetApiKey } from "@better-auth-ui/solid/plugins/api-key"
const apiKey = useGetApiKey(authClient, () => ({
query: { id: apiKeyId(), configId: "service" }
}))
```
Omit `configId` when you use the default API key configuration.
## Options factory and loader helpers [#options-factory-and-loader-helpers]
```ts
import {
ensureGetApiKey,
fetchGetApiKey,
getApiKey,
getApiKeyOptions,
prefetchGetApiKey
} from "@better-auth-ui/core/plugins/api-key"
const options = { query: { id: apiKeyId, configId: "service" } }
getApiKeyOptions(authClient, userId, options)
await ensureGetApiKey(queryClient, authClient, userId, options)
await prefetchGetApiKey(queryClient, authClient, userId, options)
const apiKey = await fetchGetApiKey(
queryClient,
authClient,
userId,
options
)
const cachedApiKey = getApiKey(queryClient, authClient, userId, options)
```
These helpers use `authClient`, so use them in client-side loaders. Do not call
them from a trusted server loader.
## Params [#params]
# useHasPermission (/docs/solid/queries/has-permission)
## Usage [#usage]
```tsx
import { useHasPermission } from "@better-auth-ui/solid/plugins/organization"
const result = useHasPermission(authClient, () => ({
permissions: { organization: ["update"] }
}))
```
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Options factory [#options-factory]
```tsx
import { hasPermissionOptions } from "@better-auth-ui/core/plugins/organization"
import { createQuery } from "@tanstack/solid-query"
const params = {
organizationId: "org_123",
permissions: { organization: ["update"] }
}
const query = createQuery(() =>
hasPermissionOptions(authClient, userId, params)
)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For Solid route loaders, import the router-loader helpers from `@better-auth-ui/core/plugins/organization`. These helpers use the Solid client-shaped `authClient`/`userId` signature so they hydrate into the same cache keys as the component hook. When you want to call your Better Auth server instance directly, matching server-auth Organization helpers are available from `@better-auth-ui/core/plugins/organization/server`.
```ts
import {
ensureHasPermission,
fetchHasPermission,
prefetchHasPermission
} from "@better-auth-ui/core/plugins/organization"
await ensureHasPermission(queryClient, authClient, userId, {
organizationId: "org_123",
permissions: { organization: ["update"] }
})
await prefetchHasPermission(queryClient, authClient, userId, {
organizationId: "org_123",
permissions: { organization: ["update"] }
})
const permission = await fetchHasPermission(queryClient, authClient, userId, {
organizationId: "org_123",
permissions: { organization: ["update"] }
})
```
## Params [#params]
# Queries (/docs/solid/queries)
Solid query APIs provide options and hooks for Better Auth read endpoints. They use shared cache keys from `@better-auth-ui/core`.
Each supported query provides an options factory. Queries that are safe for components also provide a `use*` hook.
Session and user queries also provide `ensure*`, `prefetch*`, and `fetch*` helpers for route loaders.
```ts
import { useSession } from "@better-auth-ui/solid"
import { sessionOptions } from "@better-auth-ui/core"
const sessionQuery = sessionOptions(authClient)
const session = useSession(authClient, () => ({ enabled: !import.meta.env.SSR }))
```
## Invalidation [#invalidation]
All keys start with `["auth", ...]`. Queries for one user use the `["auth", "user", userId, ...]` prefix.
Invalidate a prefix to control the related cache entries. Use `authQueryKeys` from `@better-auth-ui/core` to create consistent keys:
```ts
import { authQueryKeys } from "@better-auth-ui/core"
queryClient.invalidateQueries({ queryKey: authQueryKeys.all })
queryClient.invalidateQueries({ queryKey: authQueryKeys.user(userId) })
queryClient.invalidateQueries({ queryKey: authQueryKeys.session })
```
## Per-user queries [#per-user-queries]
Settings queries use a separate cache key for each user. They wait for the session before they start.
Solid hooks remain disabled until their required user values resolve. Consumer `enabled` controls still apply.
This design removes stale data after sign-out. It also isolates data after an account switch.
## Escape hatch [#escape-hatch]
For read-style endpoints without a purpose-built Solid helper, use `useAuthQuery` in components or `authQueryOptions` where Solid Query takes an options object.
```tsx
import { useAuthQuery } from "@better-auth-ui/solid"
const result = useAuthQuery(
authClient.magicLink.list,
["auth", "magicLink", "list"],
{ query: { limit: 20 } }
)
```
## Available queries [#available-queries]
### Auth [#auth]
The current authenticated session.
The current authenticated user, derived from the session cache.
Session query plus Solid-side redirect when unauthenticated.
Public metadata for the application requesting OAuth authorization.
### Settings [#settings]
The current user's linked social accounts.
Provider-specific info for a linked account.
Active sessions for the current user.
Device sessions for multi-session account switching.
Passkeys registered for the current user.
API keys for the current user when the API key plugin is installed.
One API key selected by ID.
### Organization [#organization]
# useListAccounts (/docs/solid/queries/list-accounts)
User-scoped query helpers wait for the active session user before firing. Solid hooks stay disabled until `userId` exists while preserving consumer `enabled` controls, so account data stays isolated per signed-in user.
## Usage [#usage]
```tsx
import { useListAccounts } from "@better-auth-ui/solid"
const accounts = useListAccounts(authClient)
```
## Options factory and loader helpers [#options-factory-and-loader-helpers]
```ts
import {
ensureListAccounts,
fetchListAccounts,
listAccountsOptions,
prefetchListAccounts
} from "@better-auth-ui/core"
const options = listAccountsOptions(authClient, userId)
await ensureListAccounts(queryClient, authClient, userId)
await prefetchListAccounts(queryClient, authClient, userId)
const accounts = await fetchListAccounts(queryClient, authClient, userId)
```
## Invalidation [#invalidation]
```ts
import { listAccountsOptions } from "@better-auth-ui/core"
queryClient.invalidateQueries({
queryKey: listAccountsOptions(authClient, userId).queryKey
})
```
## Params [#params]
# useListApiKeys (/docs/solid/queries/list-api-keys)
This page documents the Solid package API for the Better Auth API key plugin. React now has a matching runtime/docs track, but the Solid helpers remain specific to `@tanstack/solid-query` usage.
## Usage [#usage]
```tsx
import { useListApiKeys } from "@better-auth-ui/solid/plugins/api-key"
const apiKeys = useListApiKeys(authClient)
```
## Options factory and loader helpers [#options-factory-and-loader-helpers]
```ts
import {
ensureListApiKeys,
fetchListApiKeys,
listApiKeysOptions,
prefetchListApiKeys
} from "@better-auth-ui/core/plugins/api-key"
const options = listApiKeysOptions(authClient, userId)
await ensureListApiKeys(queryClient, authClient, userId)
await prefetchListApiKeys(queryClient, authClient, userId)
const keys = await fetchListApiKeys(queryClient, authClient, userId)
```
## Invalidation [#invalidation]
```ts
import { listApiKeysOptions } from "@better-auth-ui/core/plugins/api-key"
queryClient.invalidateQueries({
queryKey: listApiKeysOptions(authClient, userId).queryKey
})
```
## Params [#params]
# useListDeviceSessions (/docs/solid/queries/list-device-sessions)
Requires the Better Auth multi-session plugin on your client/server setup. Import the helper from `@better-auth-ui/core/plugins/multi-session`. The endpoint succeeds only when your Better Auth client exposes multi-session methods.
## Usage [#usage]
```tsx
import { useListDeviceSessions } from "@better-auth-ui/solid/plugins/multi-session"
const deviceSessions = useListDeviceSessions(authClient)
```
## Options factory and loader helpers [#options-factory-and-loader-helpers]
```ts
import {
ensureListDeviceSessions,
fetchListDeviceSessions,
listDeviceSessionsOptions,
prefetchListDeviceSessions
} from "@better-auth-ui/core/plugins/multi-session"
const options = listDeviceSessionsOptions(authClient, userId)
await ensureListDeviceSessions(queryClient, authClient, userId)
await prefetchListDeviceSessions(queryClient, authClient, userId)
const sessions = await fetchListDeviceSessions(queryClient, authClient, userId)
```
## Invalidation [#invalidation]
```ts
import { listDeviceSessionsOptions } from "@better-auth-ui/core/plugins/multi-session"
queryClient.invalidateQueries({
queryKey: listDeviceSessionsOptions(authClient, userId).queryKey
})
```
## Params [#params]
# useListOrganizationInvitations (/docs/solid/queries/list-invitations)
## Usage [#usage]
```tsx
import { useListOrganizationInvitations } from "@better-auth-ui/solid/plugins/organization"
const result = useListOrganizationInvitations(authClient)
```
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Options factory [#options-factory]
```tsx
import { listOrganizationInvitationsOptions } from "@better-auth-ui/core/plugins/organization"
import { createQuery } from "@tanstack/solid-query"
const query = createQuery(() =>
listOrganizationInvitationsOptions(authClient, userId)
)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For Solid route loaders, import the router-loader helpers from `@better-auth-ui/core/plugins/organization`. These helpers use the Solid client-shaped `authClient`/`userId` signature so they hydrate into the same cache keys as the component hook. When you want to call your Better Auth server instance directly, matching server-auth Organization helpers are available from `@better-auth-ui/core/plugins/organization/server`.
```ts
import {
ensureListOrganizationInvitations,
fetchListOrganizationInvitations,
prefetchListOrganizationInvitations
} from "@better-auth-ui/core/plugins/organization"
await ensureListOrganizationInvitations(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
await prefetchListOrganizationInvitations(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
const invitations = await fetchListOrganizationInvitations(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
```
## Params [#params]
# useListOrganizationMembers (/docs/solid/queries/list-members)
## Usage [#usage]
```tsx
import { useListOrganizationMembers } from "@better-auth-ui/solid/plugins/organization"
const result = useListOrganizationMembers(authClient)
```
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Options factory [#options-factory]
```tsx
import { listOrganizationMembersOptions } from "@better-auth-ui/core/plugins/organization"
import { createQuery } from "@tanstack/solid-query"
const query = createQuery(() =>
listOrganizationMembersOptions(authClient, userId)
)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For Solid route loaders, import the router-loader helpers from `@better-auth-ui/core/plugins/organization`. These helpers use the Solid client-shaped `authClient`/`userId` signature so they hydrate into the same cache keys as the component hook. When you want to call your Better Auth server instance directly, matching server-auth Organization helpers are available from `@better-auth-ui/core/plugins/organization/server`.
```ts
import {
ensureListOrganizationMembers,
fetchListOrganizationMembers,
prefetchListOrganizationMembers
} from "@better-auth-ui/core/plugins/organization"
await ensureListOrganizationMembers(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
await prefetchListOrganizationMembers(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
const members = await fetchListOrganizationMembers(queryClient, authClient, userId, { query: { organizationId: "org_123" } })
```
## Params [#params]
# useListOAuthConsents (/docs/solid/queries/list-oauth-consents)
Requires `oauthProviderClient()` from `@better-auth/oauth-provider/client`.
Returns the raw consent records Better Auth stores for the signed-in user. One OAuth client can have several records, so pair this with `groupOAuthConsents` from `@better-auth-ui/core/plugins/oauth-provider` before rendering.
## Usage [#usage]
```tsx
import { groupOAuthConsents } from "@better-auth-ui/core/plugins/oauth-provider"
import { useListOAuthConsents } from "@better-auth-ui/solid/plugins/oauth-provider"
const consents = useListOAuthConsents(authClient)
const applications = () => groupOAuthConsents(consents.data)
```
The query key is scoped to the signed-in user, so one account's authorized applications never surface in another account's view.
## Options factory and loader helpers [#options-factory-and-loader-helpers]
```ts
import {
ensureListOAuthConsents,
fetchListOAuthConsents,
listOAuthConsentsOptions,
prefetchListOAuthConsents
} from "@better-auth-ui/core/plugins/oauth-provider"
const options = listOAuthConsentsOptions(authClient, userId)
await ensureListOAuthConsents(queryClient, authClient, userId)
await prefetchListOAuthConsents(queryClient, authClient, userId)
const consents = await fetchListOAuthConsents(queryClient, authClient, userId)
```
## Invalidation [#invalidation]
```ts
import { listOAuthConsentsOptions } from "@better-auth-ui/core/plugins/oauth-provider"
queryClient.invalidateQueries({
queryKey: listOAuthConsentsOptions(authClient, userId).queryKey
})
```
`deleteOAuthConsentOptions` shares the same user-scoped prefix, so a partially failed removal still ends up reflecting the server's state.
## Params [#params]
# useListOrganizations (/docs/solid/queries/list-organizations)
## Usage [#usage]
```tsx
import { useListOrganizations } from "@better-auth-ui/solid/plugins/organization"
const result = useListOrganizations(authClient)
```
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Options factory [#options-factory]
```tsx
import { listOrganizationsOptions } from "@better-auth-ui/core/plugins/organization"
import { createQuery } from "@tanstack/solid-query"
const query = createQuery(() =>
listOrganizationsOptions(authClient, userId)
)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For Solid route loaders, import the router-loader helpers from `@better-auth-ui/core/plugins/organization`. These helpers use the Solid client-shaped `authClient`/`userId` signature so they hydrate into the same cache keys as the component hook. When you want to call your Better Auth server instance directly, matching server-auth Organization helpers are available from `@better-auth-ui/core/plugins/organization/server`.
```ts
import {
ensureListOrganizations,
fetchListOrganizations,
prefetchListOrganizations
} from "@better-auth-ui/core/plugins/organization"
await ensureListOrganizations(queryClient, authClient, userId)
await prefetchListOrganizations(queryClient, authClient, userId)
const organizations = await fetchListOrganizations(queryClient, authClient, userId)
```
## Params [#params]
# useListPasskeys (/docs/solid/queries/list-passkeys)
Requires the Better Auth passkey plugin. Use this query in passkey settings screens and invalidate it after add/delete passkey mutations.
## Usage [#usage]
```tsx
import { useListPasskeys } from "@better-auth-ui/solid/plugins/passkey"
const passkeys = useListPasskeys(authClient)
```
## Options factory and loader helpers [#options-factory-and-loader-helpers]
```ts
import {
ensureListPasskeys,
fetchListPasskeys,
listPasskeysOptions,
prefetchListPasskeys
} from "@better-auth-ui/core/plugins/passkey"
const options = listPasskeysOptions(authClient, userId)
await ensureListPasskeys(queryClient, authClient, userId)
await prefetchListPasskeys(queryClient, authClient, userId)
const passkeys = await fetchListPasskeys(queryClient, authClient, userId)
```
## Invalidation [#invalidation]
```ts
import { listPasskeysOptions } from "@better-auth-ui/core/plugins/passkey"
queryClient.invalidateQueries({
queryKey: listPasskeysOptions(authClient, userId).queryKey
})
```
## Params [#params]
# useListSessions (/docs/solid/queries/list-sessions)
`useListSessions` powers account security screens that show active sessions/devices for the signed-in user.
## Usage [#usage]
```tsx
import { useListSessions } from "@better-auth-ui/solid"
const sessions = useListSessions(authClient)
```
## Options factory and loader helpers [#options-factory-and-loader-helpers]
```ts
import {
ensureListSessions,
fetchListSessions,
listSessionsOptions,
prefetchListSessions
} from "@better-auth-ui/core"
const options = listSessionsOptions(authClient, userId)
await ensureListSessions(queryClient, authClient, userId)
await prefetchListSessions(queryClient, authClient, userId)
const sessions = await fetchListSessions(queryClient, authClient, userId)
```
## Invalidation [#invalidation]
```ts
import { listSessionsOptions } from "@better-auth-ui/core"
queryClient.invalidateQueries({
queryKey: listSessionsOptions(authClient, userId).queryKey
})
```
## Params [#params]
# useListUserInvitations (/docs/solid/queries/list-user-invitations)
## Usage [#usage]
```tsx
import { useListUserInvitations } from "@better-auth-ui/solid/plugins/organization"
const result = useListUserInvitations(authClient)
```
Organization queries require `organizationClient()` on your Better Auth client. Queries that depend on a signed-in user wait for session data before calling Better Auth.
## Options factory [#options-factory]
```tsx
import { listUserInvitationsOptions } from "@better-auth-ui/core/plugins/organization"
import { createQuery } from "@tanstack/solid-query"
const query = createQuery(() =>
listUserInvitationsOptions(authClient, userId)
)
```
## Notes [#notes]
* `useActiveOrganization` treats `slug: undefined` as the session active organization, a string slug as a URL-selected organization, and `slug: null` as intentionally no active organization.
* Member, invitation, and permission helpers follow upstream active-organization fallback behavior when an organization id is not provided.
* `useHasPermission` preserves Better Auth's flat permission parameter shape.
## Server-side prefetching [#server-side-prefetching]
For Solid route loaders, import the router-loader helpers from `@better-auth-ui/core/plugins/organization`. These helpers use the Solid client-shaped `authClient`/`userId` signature so they hydrate into the same cache keys as the component hook. When you want to call your Better Auth server instance directly, matching server-auth Organization helpers are available from `@better-auth-ui/core/plugins/organization/server`.
```ts
import {
ensureListUserInvitations,
fetchListUserInvitations,
prefetchListUserInvitations
} from "@better-auth-ui/core/plugins/organization"
await ensureListUserInvitations(queryClient, authClient, userId)
await prefetchListUserInvitations(queryClient, authClient, userId)
const invitations = await fetchListUserInvitations(queryClient, authClient, userId)
```
## Params [#params]
# usePublicOAuthClient (/docs/solid/queries/public-oauth-client)
Requires `oauthProviderClient()` from `@better-auth/oauth-provider/client`. Read the client ID from the signed authorization query and leave the complete query string in the browser URL.
## Usage [#usage]
```tsx
import { parseOAuthAuthorizationRequest } from "@better-auth-ui/core/plugins/oauth-provider"
import { usePublicOAuthClient } from "@better-auth-ui/solid/plugins/oauth-provider"
const client = usePublicOAuthClient(
authClient,
() => parseOAuthAuthorizationRequest(window.location.search).clientId
)
```
The query remains disabled until the client ID accessor returns a value.
## Options factory and loader helpers [#options-factory-and-loader-helpers]
```ts
import {
ensurePublicOAuthClient,
fetchPublicOAuthClient,
prefetchPublicOAuthClient,
publicOAuthClientOptions
} from "@better-auth-ui/core/plugins/oauth-provider"
const options = publicOAuthClientOptions(authClient, clientId)
await ensurePublicOAuthClient(queryClient, authClient, clientId)
await prefetchPublicOAuthClient(queryClient, authClient, clientId)
const client = await fetchPublicOAuthClient(queryClient, authClient, clientId)
```
## Params [#params]
# useSession (/docs/solid/queries/session)
## Usage [#usage]
```tsx
import { useSession } from "@better-auth-ui/solid"
const session = useSession(authClient)
const user = () => session.data?.user
```
Pass Better Auth params and Solid Query options through the same object.
```tsx
const session = useSession(authClient, () => ({
query: { disableCookieCache: true },
staleTime: 30_000
}))
```
## Options factory [#options-factory]
```tsx
import { sessionOptions } from "@better-auth-ui/core"
import { createQuery } from "@tanstack/solid-query"
const session = createQuery(() => sessionOptions(authClient))
```
## Server-side prefetching [#server-side-prefetching]
For client and router loaders that already have an `authClient`, use the helpers from `@better-auth-ui/core`.
```ts
import { ensureSession, fetchSession, prefetchSession } from "@better-auth-ui/core"
await ensureSession(queryClient, authClient)
await prefetchSession(queryClient, authClient)
const session = await fetchSession(queryClient, authClient)
```
For server loaders, use `@better-auth-ui/core/server`. These helpers call your Better Auth server instance directly. Pass the request headers required by `auth.api.getSession`.
```ts
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { auth } from "~/lib/auth"
const session = await ensureSessionServer(queryClient, auth, {
headers: request.headers
})
```
`@better-auth-ui/core/server` is the canonical session server-auth entrypoint. Plugin non-session server helpers live under their plugin `/server` entrypoints, such as `@better-auth-ui/core/plugins/organization/server`.
## Invalidation [#invalidation]
```ts
import { sessionOptions } from "@better-auth-ui/core"
queryClient.invalidateQueries({
queryKey: sessionOptions(authClient).queryKey
})
```
The shared key factory provides the same query key:
```ts
import { authQueryKeys } from "@better-auth-ui/core"
queryClient.invalidateQueries({ queryKey: authQueryKeys.session })
```
## Client params [#client-params]
## Server params [#server-params]
# useUser (/docs/solid/queries/user)
Thin wrapper over [`useSession`](/docs/solid/queries/session) that returns `session.data?.user` as `data`. It shares the session cache entry and does not make an extra request.
## Usage [#usage]
```tsx
import { useUser } from "@better-auth-ui/solid"
const user = useUser(authClient)
const email = () => user.data?.email
```
Accepts the same options as [`useSession`](/docs/solid/queries/session#params).
# SSR (/docs/solid/ssr)
Every Better Auth UI Solid hook uses [TanStack Solid Query](https://tanstack.com/query). A shared `QueryClient` lets the application prefetch sessions, protect routes, and hydrate the browser cache.
The examples use [TanStack Start](https://tanstack.com/start) with Solid Router. They match [`examples/start-solid-zaidan-example`](https://github.com/better-auth-ui/better-auth-ui/tree/main/examples/start-solid-zaidan-example).
Other Solid applications can use the same `QueryClient` pattern with different router configuration.
## Install the SSR integration [#install-the-ssr-integration]
npm
pnpm
yarn
bun
```bash
npm install @tanstack/solid-query @tanstack/solid-router @tanstack/solid-start solid-js
```
```bash
pnpm add @tanstack/solid-query @tanstack/solid-router @tanstack/solid-start solid-js
```
```bash
yarn add @tanstack/solid-query @tanstack/solid-router @tanstack/solid-start solid-js
```
```bash
bun add @tanstack/solid-query @tanstack/solid-router @tanstack/solid-start solid-js
```
TanStack Start connects Solid Router to server and browser rendering. Solid Query provides the shared `QueryClient`.
Pass this client through the route context and `AuthProvider`. Better Auth UI hooks can then read data that route loaders prefetched.
## Customize the QueryClient [#customize-the-queryclient]
Create the `QueryClient` inside the router factory. Each SSR request then receives a separate cache.
Apply `defaultOptions` in this factory. The example uses a `staleTime` of 5 seconds to reuse recent results during navigation.
```tsx title="src/router.tsx"
import { QueryClient } from "@tanstack/solid-query"
import { createRouter } from "@tanstack/solid-router"
import { routeTree } from "./routeTree.gen"
export const getRouter = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5000
}
}
})
return createRouter({
context: { queryClient },
defaultPreloadStaleTime: 0,
routeTree,
scrollRestoration: true
})
}
```
The options have these effects:
* `defaultPreloadStaleTime: 0` tells TanStack Router to start loaders during each preload. Query `staleTime` can still reuse cached data.
* `context: { queryClient }` exposes the client to every loader/`beforeLoad` hook via `context.queryClient`.
* Configure `defaultOptions` as you do in other TanStack Query applications.
## Type the root route context [#type-the-root-route-context]
Use `createRootRouteWithContext` so child routes can read the typed `context.queryClient`. Then pass this client to the application providers.
```tsx title="src/routes/__root.tsx"
import type { QueryClient } from "@tanstack/solid-query"
import { createRootRouteWithContext, Outlet } from "@tanstack/solid-router"
import { Providers } from "@/components/providers"
export const Route = createRootRouteWithContext<{
queryClient: QueryClient
}>()({
component: RootComponent
})
function RootComponent() {
const routeContext = Route.useRouteContext()
return (
)
}
```
The tree of routes now has `{ queryClient }` available in every loader, `beforeLoad`, and component via `Route.useRouteContext()`.
## Prefetch the session in `beforeLoad` [#prefetch-the-session-in-beforeload]
Each Solid query provides matching `ensure*`, `prefetch*`, and `fetch*` helpers. Base helpers come from `@better-auth-ui/solid` or `@better-auth-ui/core`.
Optional plugin helpers come from `@better-auth-ui/core/plugins/`. These helpers accept `QueryClient`, `authClient`, and the query parameters.
| Helper | When to use |
| ----------------- | ---------------------------------------------------------------------------------------------- |
| `ensureSession` | Read the session, resolving from cache if fresh. Most common in loaders. |
| `prefetchSession` | Kick off a background fetch without awaiting. Good for soft preloads. |
| `fetchSession` | Fetch through the query client, reusing fresh cached data according to Query stale-time rules. |
Use `ensureSession` in `beforeLoad` to protect a route. Connect the browser and server helpers with `createIsomorphicFn`.
The server path receives the request headers. The browser path reuses `authClient`.
```tsx title="src/routes/settings/$path.tsx"
import { viewPaths } from "@better-auth-ui/core"
import { ensureSession } from "@better-auth-ui/core"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { createFileRoute, notFound, redirect } from "@tanstack/solid-router"
import { createIsomorphicFn } from "@tanstack/solid-start"
import { getRequestHeaders } from "@tanstack/solid-start/server"
import { Settings } from "@/components/auth/settings/settings"
import { auth } from "@/lib/auth"
import { authClient } from "@/lib/auth-client"
export const Route = createFileRoute("/settings/$path")({
async beforeLoad({ params: { path }, context: { queryClient }, location }) {
if (!Object.values(viewPaths.settings).includes(path)) {
throw notFound()
}
const ensureSessionIso = createIsomorphicFn()
.server(() =>
ensureSessionServer(queryClient, auth, {
headers: getRequestHeaders()
})
)
.client(() => ensureSession(queryClient, authClient))
const session = await ensureSessionIso()
if (!session) {
throw redirect({
to: "/auth/$path",
params: { path: "sign-in" },
search: { redirectTo: location.href }
})
}
return { session }
},
component: SettingsPage
})
function SettingsPage() {
const path = () => Route.useParams()().path
return
}
```
The loader adds the session to the query cache before the protected route renders. `Settings` and its `useSession` calls can read it immediately.
TanStack Router adds the value from `beforeLoad` to the route context. Read this value in a component with `Route.useRouteContext()`.
This pattern avoids another query for the same session data.
## Prefetch without blocking [#prefetch-without-blocking]
Use `prefetchSession` to fill the cache without blocking navigation. This pattern can prepare a public route that later shows an authenticated widget:
```ts
import { prefetchSession } from "@better-auth-ui/core"
export const Route = createFileRoute("/")({
loader: ({ context: { queryClient } }) => {
void prefetchSession(queryClient, authClient)
}
})
```
The same pattern works for other Solid query helpers. Base helpers such as `prefetchListAccounts` come from the Solid or core packages.
Plugin helpers come from their core plugin packages. To call the Better Auth server directly, use the core or plugin `/server` entrypoint.
## Server-only helpers [#server-only-helpers]
To avoid an HTTP request, import the session helpers from `@better-auth-ui/core/server`. Call them from a server function or another server runtime.
These helpers accept the Better Auth server instance instead of `authClient`:
```ts title="src/lib/session.ts"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import type { QueryClient } from "@tanstack/solid-query"
import { createServerFn } from "@tanstack/solid-start"
import { getRequestHeaders } from "@tanstack/solid-start/server"
import { auth } from "@/lib/auth"
const getSession = createServerFn().handler(() =>
auth.api.getSession({ headers: getRequestHeaders() })
)
export const ensureServerSession = (queryClient: QueryClient) =>
ensureSessionServer(queryClient, auth, { headers: getRequestHeaders() })
```
Session server-auth helpers are canonical from `@better-auth-ui/core/server` as `sessionOptionsServer`, `ensureSessionServer`, `prefetchSessionServer`, and `fetchSessionServer`.
Available `@better-auth-ui/core/server` exports include:
* Session helpers: `sessionOptionsServer`, `ensureSessionServer`, `prefetchSessionServer`, `fetchSessionServer`
* Base server auth type: `AuthServer`
* Settings helpers: `listAccountsOptions`, `ensureListAccounts`, `prefetchListAccounts`, `fetchListAccounts`, `accountInfoOptions`, `ensureAccountInfo`, `listSessionsOptions`, and `ensureListSessions` with matching prefetch/fetch helpers
Plugin server query helper families live under their plugin server entrypoints:
* API-key (`@better-auth-ui/core/plugins/api-key/server`): `listApiKeysOptions`, `ensureListApiKeys`, `prefetchListApiKeys`, `fetchListApiKeys`
* Multi-session (`@better-auth-ui/core/plugins/multi-session/server`): `listDeviceSessionsOptions`, `ensureListDeviceSessions`, `prefetchListDeviceSessions`, `fetchListDeviceSessions`
* Passkey (`@better-auth-ui/core/plugins/passkey/server`): `listPasskeysOptions`, `ensureListPasskeys`, `prefetchListPasskeys`, `fetchListPasskeys`
* Organization (`@better-auth-ui/core/plugins/organization/server`): `activeOrganizationOptions`, `ensureActiveOrganization`, `prefetchActiveOrganization`, `fetchActiveOrganization`, `fullOrganizationOptions`, `ensureFullOrganization`, `listOrganizationsOptions`, `ensureListOrganizations`, `listOrganizationMembersOptions`, `ensureListOrganizationMembers`, `listOrganizationInvitationsOptions`, `ensureListOrganizationInvitations`, `listUserInvitationsOptions`, `ensureListUserInvitations`, `hasPermissionOptions`, and `ensureHasPermission` with matching prefetch/fetch helpers
Server helpers, loader helpers, and component hooks use the same cache keys. Browser hooks can therefore read data that route loaders prefetched.
These helpers do not own secrets, routes, copied interface code, or registry installation. The Solid package also does not create routes.
For TanStack Start installation and component customization, use the [Zaidan documentation](/docs/zaidan).
# (/docs/zaidan/components/account-settings)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/settings/account/account-settings.tsx
import { AccountSettings } from "@/components/auth/settings/account/account-settings"
export function AccountSettingsDemo() {
return
}
```
`AccountSettings` renders profile, email, and plugin-contributed `accountCards` in the account tab.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/account-settings.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/account-settings.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/account-settings.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/account-settings.json
```
After install, `src/components/auth/settings/account/account-settings.tsx` is app-owned. Customize it inside your Solid app as needed.
## Props [#props]
# (/docs/zaidan/components/active-sessions)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/settings/security/active-sessions.tsx
import { ActiveSessionsSettings } from "@/components/auth/settings/security/active-sessions"
export function ActiveSessionsDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/active-sessions.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/active-sessions.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/active-sessions.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/active-sessions.json
```
After install, `src/components/auth/settings/security/active-sessions.tsx` is app-owned. Customize it inside your Solid app as needed.
## Props [#props]
# (/docs/zaidan/components/auth-provider)
## Usage [#usage]
Install the provider registry payload with `npx shadcn@latest add https://better-auth-ui.com/r/solid/auth-provider.json`. It copies `src/components/auth/auth-provider.tsx` and related provider support files. After install, these files are app-owned. Then wrap your application shell as shown in the Solid/Zaidan example.
```tsx file=/../../examples/start-solid-zaidan-example/src/components/providers.tsx
import { deleteUserPlugin } from "@better-auth-ui/core/plugins/delete-user"
import type { AuthLinkProps } from "@better-auth-ui/solid"
import type { QueryClient } from "@tanstack/solid-query"
import {
Link as RouterLink,
useNavigate,
useParams
} from "@tanstack/solid-router"
import type { JSX } from "solid-js"
import { onCleanup, onMount, Show, splitProps } from "solid-js"
import { apiKeyPlugin } from "@/lib/auth/api-key-plugin"
import { emailOtpPlugin } from "@/lib/auth/email-otp-plugin"
import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin"
import { multiSessionPlugin } from "@/lib/auth/multi-session-plugin"
import { organizationPlugin } from "@/lib/auth/organization-plugin"
import { passkeyPlugin } from "@/lib/auth/passkey-plugin"
import { themePlugin } from "@/lib/auth/theme-plugin"
import { twoFactorPlugin } from "@/lib/auth/two-factor-plugin"
import { usernamePlugin } from "@/lib/auth/username-plugin"
import { authClient } from "@/lib/auth-client"
import { syncDocumentThemePreference } from "@/lib/theme"
import { AuthProvider } from "./auth/auth-provider"
import { Toaster } from "./ui/sonner"
export type ProvidersProps = {
children?: JSX.Element | (() => JSX.Element)
queryClient?: QueryClient
}
const resolveProviderChildren = (children: ProvidersProps["children"]) =>
typeof children === "function" ? children() : children
function AuthLink(props: AuthLinkProps) {
const [local, linkProps] = splitProps(props, ["href"])
return
}
export function Providers(props: ProvidersProps) {
const navigate = useNavigate()
const params = useParams({ strict: false })
const organizationSlug = () => {
const slug = params()?.slug
if (typeof slug === "string" && slug.length > 0) return slug
return null
}
onMount(() => {
const cleanup = syncDocumentThemePreference()
onCleanup(cleanup)
})
return (
{() => (
<>
{resolveProviderChildren(props.children)}
>
)}
)
}
```
## Localization [#localization]
Install the locale package:
```bash
bun add @better-auth-ui/locales
```
Import one locale and pass it to `AuthProvider`:
```tsx title="components/providers.tsx"
import { deDE } from "@better-auth-ui/locales/de-DE"
{children}
```
Locale bundles include the core messages and all built-in plugin messages.
Use `localization` for product-specific text. These values take priority over the selected locale:
```tsx
{children}
```
### Match the browser language [#match-the-browser-language]
In a client-only application, import the supported locales and match `navigator.languages` against that list:
```tsx
import { matchAuthLocale } from "@better-auth-ui/locales"
import { deDE } from "@better-auth-ui/locales/de-DE"
import { enUS } from "@better-auth-ui/locales/en-US"
const locale = matchAuthLocale({
requested: navigator.languages,
supported: [enUS, deDE],
fallback: enUS
})
```
For server rendering, resolve the same locale from a user preference or the `Accept-Language` header. Pass that locale during the first render to prevent a hydration mismatch.
Changing the `locale` prop updates mounted auth components. Email components do not read `AuthProvider`; pass their localization on the server.
## Popup social sign-in [#popup-social-sign-in]
Set `socialSignInMode="popup"` to keep the current page open during social sign-in. Redirect mode remains the default.
Better Auth marks this API as experimental. Configure the server and client plugins before you enable it:
```ts title="lib/auth.ts"
import { betterAuth } from "better-auth"
import { bearer, oauthPopup } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [bearer(), oauthPopup()]
})
```
```ts title="lib/auth-client.ts"
import { createAuthClient } from "better-auth/solid"
import { oauthPopupClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [oauthPopupClient()]
})
```
Then select popup mode on the provider:
```tsx
{children}
```
Popup mode uses the same provider buttons and redirect target. It returns control to the current page, refreshes the session, and then runs the configured navigation.
## Props [#props]
# (/docs/zaidan/components/auth-redirect)
`` powers the Solid `/auth/redirect` view. It checks the
current session, then continues to the `redirectTo` query parameter.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/auth-redirect.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/auth-redirect.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/auth-redirect.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/auth-redirect.json
```
After install, `src/components/auth/auth-redirect.tsx` is app-owned. Route it
through the unified auth component:
```tsx
import { Auth } from "@/components/auth/auth"
export default function AuthPage(props: { path: string }) {
return
}
```
Open the view with an encoded, same-origin destination:
```text
/auth/redirect?redirectTo=%2Fsettings%2Faccount
```
Authenticated users continue immediately. Signed-out users go to sign in and
return to the redirect view after authentication. The final redirect uses a
full-page request, so the destination can be an API callback.
Only root-relative paths and same-origin HTTP(S) URLs are accepted. Unsafe,
cross-origin, malformed, or self-referencing targets fall back to `/`.
## Use with account deletion emails [#use-with-account-deletion-emails]
Better Auth requires the user who follows a deletion link to have a matching
session. Wrap the generated deletion URL with the redirect view before sending
the email:
```ts
import { betterAuth } from "better-auth"
export const auth = betterAuth({
user: {
deleteUser: {
enabled: true,
sendDeleteAccountVerification: async ({ user, url }) => {
const appURL = new URL(process.env.BETTER_AUTH_URL!)
const deleteURL = new URL(url)
const redirectURL = new URL("/auth/redirect", appURL)
redirectURL.searchParams.set(
"redirectTo",
`${deleteURL.pathname}${deleteURL.search}${deleteURL.hash}`,
)
await sendDeleteAccountEmail({
to: user.email,
url: redirectURL.toString(),
})
},
},
},
})
```
The Better Auth callback and the redirect view must share an origin.
## Props [#props]
# (/docs/zaidan/components/auth)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/auth/auth.tsx
import { Auth } from "@/components/auth/auth"
export function AuthDemo() {
return
}
```
### Built-in views [#built-in-views]
| `view` | Default path |
| ---------------- | ----------------------- |
| `callback` | `/auth/callback` |
| `error` | `/auth/error` |
| `redirect` | `/auth/redirect` |
| `signIn` | `/auth/sign-in` |
| `signUp` | `/auth/sign-up` |
| `signOut` | `/auth/sign-out` |
| `forgotPassword` | `/auth/forgot-password` |
| `resetPassword` | `/auth/reset-password` |
| `resetLinkSent` | `/auth/reset-link-sent` |
| `verifyEmail` | `/auth/verify-email` |
Registered plugins can contribute more views through their own `viewPaths.auth`, such as `magicLink` and `magicLinkSent`.
### Callback results [#callback-results]
Set Better Auth `onAPIError.errorURL` and each social sign-in `errorCallbackURL` to `/auth/error`. The view reads Better Auth's `error` query parameter and gives the user a suitable recovery action.
Use `/auth/callback?result=email_verified` as an email verification callback. The result view also supports `account_linked`, `password_reset`, `signup_complete`, and `cancelled`. Add `flow=email-verification`, `account-linking`, `password-reset`, or `oauth` when `result=success` needs more context. A local `redirectTo` path adds a Continue action.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/auth.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/auth.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/auth.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/auth.json
```
After install, `src/components/auth/auth.tsx` and the copied auth view files are app-owned. Wire it to your TanStack Router auth route or pass an explicit `view`.
## Props [#props]
# (/docs/zaidan/components/change-email)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/settings/account/change-email.tsx
import { ChangeEmail } from "@/components/auth/settings/account/change-email"
export function ChangeEmailDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/change-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/change-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/change-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/change-email.json
```
After install, `src/components/auth/settings/account/change-email.tsx` is app-owned. Customize it inside your Solid app as needed.
## Props [#props]
# (/docs/zaidan/components/change-password)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/settings/security/change-password.tsx
import { ChangePasswordSettings } from "@/components/auth/settings/security/change-password"
export function ChangePasswordDemo() {
return
}
```
Current, new, and confirmation password fields provide independent localized show/hide controls.
When a user without a credential account requests a set-password email, the email-provider button appears after the request succeeds. Hover or focus it to show a QR code for opening the same provider URL on another device.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/change-password.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/change-password.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/change-password.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/change-password.json
```
After install, `src/components/auth/settings/security/change-password.tsx` is app-owned. Customize it inside your Solid app as needed.
## Props [#props]
# (/docs/zaidan/components/email/change-email-confirmation-email)
## Usage [#usage]
```tsx file=/src/demos/zaidan/email/change-email-confirmation-email.tsx#L13-
import { ChangeEmailConfirmationEmail } from "@better-auth-ui/solid/email"
import { render } from "@solidjs-email/main"
const html = await render(() =>
ChangeEmailConfirmationEmail({
url: "https://better-auth-ui.com/api/auth/change-email/verify?token=example-token",
appName: "Better Auth UI",
logoURL: {
light: "/favicon-96x96.png",
dark: "/favicon-96x96-inverted.png"
},
currentEmail: "current@example.com",
newEmail: "new@example.com",
expirationMinutes: 60,
darkMode: true,
poweredBy: true
})
)
```
## Better Auth setup [#better-auth-setup]
Render this template from `user.changeEmail.sendChangeEmailConfirmation` and send it to `user.email`. Better Auth supplies the approval URL and requested address.
```tsx
sendChangeEmailConfirmation: async ({ user, newEmail, url }) => {
const html = await render(() =>
ChangeEmailConfirmationEmail({
url,
currentEmail: user.email,
newEmail,
appName: "My App"
})
)
await sendEmail({
to: user.email,
subject: "Approve your email change",
html
})
}
```
Use `EmailChangedEmail` separately if you also send a notification after the address has changed.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/change-email-confirmation-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/change-email-confirmation-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/change-email-confirmation-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/change-email-confirmation-email.json
```
## Props [#props]
## Features [#features]
* Shows the current and requested email addresses
* Includes an approval button and fallback URL
* Explains that ignoring the message leaves the address unchanged
* Supports expiration details, theming, branding, and localization
# (/docs/zaidan/components/email/delete-account-verification-email)
## Usage [#usage]
```tsx file=/src/demos/zaidan/email/delete-account-verification-email.tsx#L13-
import { DeleteAccountVerificationEmail } from "@better-auth-ui/solid/email"
import { render } from "@solidjs-email/main"
const html = await render(() =>
DeleteAccountVerificationEmail({
url: "https://better-auth-ui.com/api/auth/delete-user/callback?token=example-token",
appName: "Better Auth UI",
logoURL: {
light: "/favicon-96x96.png",
dark: "/favicon-96x96-inverted.png"
},
email: "user@example.com",
expirationHours: 24,
darkMode: true,
poweredBy: true
})
)
```
## Better Auth setup [#better-auth-setup]
Render this template from `user.deleteUser.sendDeleteAccountVerification`. Better Auth supplies the verification URL and user whose account is being deleted.
```tsx
sendDeleteAccountVerification: async ({ user, url }) => {
const html = await render(() =>
DeleteAccountVerificationEmail({
url,
email: user.email,
appName: "My App"
})
)
await sendEmail({
to: user.email,
subject: "Confirm account deletion",
html
})
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/delete-account-verification-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/delete-account-verification-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/delete-account-verification-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/delete-account-verification-email.json
```
## Props [#props]
## Features [#features]
* Clearly states that account deletion is permanent
* Includes a verification button and fallback URL
* Explains that ignoring the message keeps the account active
* Supports expiration details, theming, branding, and localization
# (/docs/zaidan/components/email/email-changed-email)
## Usage [#usage]
```tsx file=/src/demos/zaidan/email/email-changed-email.tsx#L13-
import { EmailChangedEmail } from "@better-auth-ui/solid/email"
import { render } from "@solidjs-email/main"
const html = await render(() =>
EmailChangedEmail({
oldEmail: "old@example.com",
newEmail: "new@example.com",
appName: "Better Auth UI",
logoURL: {
light: "/favicon-96x96.png",
dark: "/favicon-96x96-inverted.png"
},
supportEmail: "support@example.com",
revertURL: "https://better-auth-ui.com/revert-email",
darkMode: true,
poweredBy: true
})
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/email-changed-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/email-changed-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/email-changed-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/email-changed-email.json
```
## Props [#props]
## Features [#features]
* Email change notification
* Shows previous and new email addresses
* Revert action button if unauthorized
* Support contact information
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/zaidan/components/email/email-verification-email)
## Usage [#usage]
```tsx file=/src/demos/zaidan/email/email-verification-email.tsx#L13-
import { EmailVerificationEmail } from "@better-auth-ui/solid/email"
import { render } from "@solidjs-email/main"
const html = await render(() =>
EmailVerificationEmail({
url: "https://better-auth-ui.com/auth/verify-email?token=example-token",
appName: "Better Auth UI",
logoURL: {
light: "/favicon-96x96.png",
dark: "/favicon-96x96-inverted.png"
},
email: "user@example.com",
expirationMinutes: 60,
darkMode: true,
poweredBy: true
})
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/email-verification-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/email-verification-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/email-verification-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/email-verification-email.json
```
## Props [#props]
## Features [#features]
* Verification button and fallback URL
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/zaidan/components/email/magic-link-email)
## Usage [#usage]
```tsx file=/src/demos/zaidan/email/magic-link-email.tsx#L13-
import { MagicLinkEmail } from "@better-auth-ui/solid/email"
import { render } from "@solidjs-email/main"
const html = await render(() =>
MagicLinkEmail({
url: "https://better-auth-ui.com/auth/verify?token=example-token",
appName: "Better Auth UI",
logoURL: {
light: "/favicon-96x96.png",
dark: "/favicon-96x96-inverted.png"
},
email: "user@example.com",
expirationMinutes: 5,
darkMode: true,
poweredBy: true
})
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/magic-link-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/magic-link-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/magic-link-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/magic-link-email.json
```
## Props [#props]
## Features [#features]
* Sign-in button with magic link
* Fallback URL for manual copy/paste
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/zaidan/components/email/new-device-email)
## Usage [#usage]
```tsx file=/src/demos/zaidan/email/new-device-email.tsx#L13-
import { NewDeviceEmail } from "@better-auth-ui/solid/email"
import { render } from "@solidjs-email/main"
const html = await render(() =>
NewDeviceEmail({
userEmail: "user@example.com",
deviceInfo: {
browser: "Chrome 120.0",
os: "macOS 14.2",
location: "San Francisco, CA",
ipAddress: "192.168.1.1",
timestamp: "January 15, 2024 at 3:30 PM"
},
appName: "Better Auth UI",
logoURL: {
light: "/favicon-96x96.png",
dark: "/favicon-96x96-inverted.png"
},
supportEmail: "support@example.com",
secureAccountLink: "https://better-auth-ui.com/secure-account",
darkMode: true,
poweredBy: true
})
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/new-device-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/new-device-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/new-device-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/new-device-email.json
```
## Props [#props]
## Features [#features]
* Device information display (browser, OS, location, IP)
* Timestamp of the sign-in
* Security action button if unauthorized
* Support contact information
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/zaidan/components/email/organization-invitation-email)
## Usage [#usage]
```tsx file=/src/demos/zaidan/email/organization-invitation-email.tsx#L13-
import { OrganizationInvitationEmail } from "@better-auth-ui/solid/email"
import { render } from "@solidjs-email/main"
const html = await render(() =>
OrganizationInvitationEmail({
url: "https://better-auth-ui.com/auth/accept-invitation?invitationId=example",
email: "invitee@example.com",
inviterName: "Jane Doe",
inviterEmail: "jane@example.com",
organizationName: "Acme Inc.",
role: "member",
appName: "Better Auth UI",
logoURL: {
light: "/favicon-96x96.png",
dark: "/favicon-96x96-inverted.png"
},
expirationHours: 48,
darkMode: true,
poweredBy: true
})
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/organization-invitation-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/organization-invitation-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/organization-invitation-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/organization-invitation-email.json
```
## Better Auth setup [#better-auth-setup]
Wire the email into the Better Auth `organization` plugin via `sendInvitationEmail`. Point `url` at the direct invitation view registered by `organizationPlugin()`.
```tsx title="auth.ts"
import { OrganizationInvitationEmail } from "@better-auth-ui/solid/email"
import { render } from "@solidjs-email/main"
import { betterAuth } from "better-auth"
import { organization } from "better-auth/plugins"
const baseUrl = process.env.BETTER_AUTH_URL!
export const auth = betterAuth({
plugins: [
organization({
async sendInvitationEmail(data) {
const html = await render(() =>
OrganizationInvitationEmail({
url: `${baseUrl}/auth/accept-invitation?invitationId=${data.id}`,
email: data.email,
inviterName: data.inviter.user.name,
inviterEmail: data.inviter.user.email,
organizationName: data.organization.name,
organizationLogoURL: data.organization.logo ?? undefined,
role: data.role,
appName: "My App",
poweredBy: true
})
)
await sendEmail({
to: data.email,
subject: `You're invited to ${data.organization.name}`,
html
})
}
})
]
})
```
Add `organizationPlugin().viewPaths.auth.acceptInvitation` to your auth
route allow-list. Use `{baseUrl}/settings/organizations` only when you want
the invitation email to open the full pending-invitations list instead.
## Props [#props]
## Features [#features]
* Inviter name and email display
* Organization name and optional organization logo
* Role being offered (for example member, admin, owner)
* Accept invitation button linking to the direct invitation view
* Fallback URL for manual copy/paste
* Optional expiration time
* Security notice for unexpected invitations
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/zaidan/components/email/otp-email)
## Usage [#usage]
```tsx file=/src/demos/zaidan/email/otp-email.tsx#L13-
import { OtpEmail } from "@better-auth-ui/solid/email"
import { render } from "@solidjs-email/main"
const html = await render(() =>
OtpEmail({
verificationCode: "123456",
email: "user@example.com",
appName: "Better Auth UI",
logoURL: {
light: "/favicon-96x96.png",
dark: "/favicon-96x96-inverted.png"
},
expirationMinutes: 10,
darkMode: true,
poweredBy: true
})
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/otp-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/otp-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/otp-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/otp-email.json
```
## Props [#props]
## Features [#features]
* Large, prominently displayed verification code
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/zaidan/components/email/password-changed-email)
## Usage [#usage]
```tsx file=/src/demos/zaidan/email/password-changed-email.tsx#L13-
import { PasswordChangedEmail } from "@better-auth-ui/solid/email"
import { render } from "@solidjs-email/main"
const html = await render(() =>
PasswordChangedEmail({
email: "user@example.com",
timestamp: "January 15, 2024 at 3:30 PM",
appName: "Better Auth UI",
logoURL: {
light: "/favicon-96x96.png",
dark: "/favicon-96x96-inverted.png"
},
supportEmail: "support@example.com",
secureAccountURL: "https://better-auth-ui.com/secure-account",
darkMode: true,
poweredBy: true
})
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/password-changed-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/password-changed-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/password-changed-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/password-changed-email.json
```
## Props [#props]
## Features [#features]
* Password change notification
* Timestamp of the change
* Security action button if unauthorized
* Support contact information
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/zaidan/components/email/reset-password-email)
## Usage [#usage]
```tsx file=/src/demos/zaidan/email/reset-password-email.tsx#L13-
import { ResetPasswordEmail } from "@better-auth-ui/solid/email"
import { render } from "@solidjs-email/main"
const html = await render(() =>
ResetPasswordEmail({
url: "https://better-auth-ui.com/auth/reset-password?token=example-token",
appName: "Better Auth UI",
logoURL: {
light: "/favicon-96x96.png",
dark: "/favicon-96x96-inverted.png"
},
email: "user@example.com",
expirationMinutes: 60,
darkMode: true,
poweredBy: true
})
)
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/reset-password-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/reset-password-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/reset-password-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/reset-password-email.json
```
## Props [#props]
## Features [#features]
* Password reset button and fallback URL
* Expiration time information
* Security notice for unauthorized requests
* Customizable branding and styling
* Support for light/dark mode themes
* Localization support
# (/docs/zaidan/components/forgot-password)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/auth/forgot-password.tsx
import { ForgotPassword } from "@/components/auth/forgot-password"
export function ForgotPasswordDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/forgot-password.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/forgot-password.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/forgot-password.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/forgot-password.json
```
After install, `src/components/auth/forgot-password.tsx` and the copied forgot-password files are app-owned. Set `redirectTo` when your reset route differs from the default auth path.
## Props [#props]
# (/docs/zaidan/components/linked-accounts)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/settings/security/linked-accounts.tsx
import { LinkedAccountsSettings } from "@/components/auth/settings/security/linked-accounts"
export function LinkedAccountsDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/linked-accounts.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/linked-accounts.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/linked-accounts.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/linked-accounts.json
```
After install, `src/components/auth/settings/security/linked-accounts.tsx` is app-owned. Customize it inside your Solid app as needed.
## Props [#props]
# (/docs/zaidan/components/reset-link-sent)
`` renders this view at `/auth/reset-link-sent` after `` successfully requests a reset link. It reads the submitted email from session storage, redirects back to forgot-password when that state is missing, and shows an email-provider shortcut when one is available.
Hover or focus the email-provider button to show a QR code for the same provider URL. Users can scan it when their email account is available on another device.
## Installation [#installation]
The view is included with the Solid forgot-password registry entry.
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/forgot-password.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/forgot-password.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/forgot-password.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/forgot-password.json
```
## Usage [#usage]
```tsx
import { ResetLinkSent } from "@/components/auth/reset-link-sent"
```
## Props [#props]
# (/docs/zaidan/components/reset-password)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/auth/reset-password.tsx
import { ResetPassword } from "@/components/auth/reset-password"
export function ResetPasswordDemo() {
return
}
```
New-password and confirmation fields each provide a localized show/hide control while preserving their current values.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/reset-password.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/reset-password.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/reset-password.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/reset-password.json
```
After install, `src/components/auth/reset-password.tsx` and the copied reset-password files are app-owned. Pass `token` from your Solid router when you do not want the component to read it from the URL query string.
## Props [#props]
# (/docs/zaidan/components/security-settings)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/settings/security/security-settings.tsx
import { SecuritySettings } from "@/components/auth/settings/security/security-settings"
export function SecuritySettingsDemo() {
return
}
```
`SecuritySettings` renders password, linked-account, active-session, API key, passkey, delete-user, and plugin-contributed security cards.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/security-settings.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/security-settings.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/security-settings.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/security-settings.json
```
After install, `src/components/auth/settings/security/security-settings.tsx` is app-owned. Customize it inside your Solid app as needed.
## Props [#props]
## Fresh-session checks [#fresh-session-checks]
Sensitive session operations can return `SESSION_NOT_FRESH`. The active sessions card shows an inline password prompt for this response. After the user signs in again, it retries the session query. If password sign-in is disabled, the prompt links to the configured sign-in route.
# (/docs/zaidan/components/settings)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/settings/settings.tsx
import { Settings } from "@/components/auth/settings/settings"
export function SettingsDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/settings.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/settings.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/settings.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/settings.json
```
After install, `src/components/auth/settings/settings.tsx` is app-owned. Customize it inside your Solid app as needed.
## Props [#props]
# (/docs/zaidan/components/sign-in)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/auth/sign-in.tsx
import { SignIn } from "@/components/auth/sign-in"
export function SignInDemo() {
return
}
```
The password starts masked. Its localized show/hide button changes only the field presentation, so the submitted password value stays unchanged.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/sign-in.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/sign-in.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/sign-in.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/sign-in.json
```
After install, `src/components/auth/sign-in.tsx` and the copied sign-in files are app-owned. Register plugins such as Username or Magic Link in `AuthProvider` when you want them to alter the sign-in flow.
## Props [#props]
# (/docs/zaidan/components/sign-out)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/auth/sign-out.tsx
import { SignOut } from "@/components/auth/sign-out"
export function SignOutDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/sign-out.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/sign-out.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/sign-out.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/sign-out.json
```
After install, `src/components/auth/sign-out.tsx` is app-owned. Route it through `` so the user returns to sign in after sign-out.
## Props [#props]
# (/docs/zaidan/components/sign-up)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/auth/sign-up.tsx
import { SignUp } from "@/components/auth/sign-up"
export function SignUpDemo() {
return
}
```
Password and confirmation fields each provide a localized show/hide control while preserving their current values.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/sign-up.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/sign-up.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/sign-up.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/sign-up.json
```
After install, `src/components/auth/sign-up.tsx` and the copied sign-up files are app-owned. Configure additional fields and plugins in `AuthProvider` to extend the form.
## Props [#props]
# (/docs/zaidan/components/user-avatar)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/user/user-avatar.tsx
import { UserAvatar } from "@/components/auth/user/user-avatar"
export function UserAvatarDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/user-avatar.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/user-avatar.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/user-avatar.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/user-avatar.json
```
After install, `src/components/auth/user/user-avatar.tsx` and the copied avatar files are app-owned. Customize fallback initials, icon behavior, or avatar sizing in your copied component.
## Props [#props]
# (/docs/zaidan/components/user-button)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/user/user-button.tsx
import { UserButton } from "@/components/auth/user/user-button"
export function UserButtonDemo() {
return
}
```
## Icon [#icon]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/user/user-button-icon.tsx
import { UserButton } from "@/components/auth/user/user-button"
export function UserButtonIconDemo() {
return
}
```
## Custom links [#custom-links]
Use the `links` prop to add entries above the built-in items. Each entry is either a `{ label, href, icon?, variant?, visibility? }` descriptor or a fully rendered Solid element. `visibility` defaults to `"always"` and accepts `"authenticated" | "unauthenticated" | "always"`. Pass `hideSettings` to remove the built-in Settings link.
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/user/user-button-links.tsx
import { LayoutDashboard, Users } from "lucide-solid"
import { UserButton } from "@/components/auth/user/user-button"
export function UserButtonLinksDemo() {
return (
Dashboard,
href: "/dashboard",
icon: ,
visibility: "authenticated"
},
{
label: Team,
href: "/team",
icon:
}
]}
/>
)
}
```
For interactive items shared across the app, prefer a plugin's `userMenuItems` slot over `links`.
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/user-button.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/user-button.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/user-button.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/user-button.json
```
After installation, the application owns `src/components/auth/user-button.tsx` and the other copied files. Point authentication and settings links to the TanStack Router paths.
Then select the plugins that add authenticated user menu items.
## Props [#props]
# (/docs/zaidan/components/user-profile)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/settings/account/user-profile.tsx
import { UserProfile } from "@/components/auth/settings/account/user-profile"
export function UserProfileDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/user-profile.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/user-profile.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/user-profile.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/user-profile.json
```
After install, `src/components/auth/settings/account/user-profile.tsx` is app-owned. Customize it inside your Solid app as needed.
## Props [#props]
# (/docs/zaidan/components/user-view)
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/user/user-view.tsx
import { UserView } from "@/components/auth/user/user-view"
export function UserViewDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/user-view.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/user-view.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/user-view.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/user-view.json
```
After install, `src/components/auth/user/user-view.tsx` and the copied user view files are app-owned. Tune the label and secondary label mapping to match your account model.
## Props [#props]
# (/docs/zaidan/components/verify-email)
Hover or focus the email-provider button to show a QR code for the same provider URL. Users can scan it when their email account is available on another device.
## Usage [#usage]
```tsx file=/../../examples/start-solid-zaidan-example/src/demos/auth/verify-email.tsx
import { VerifyEmail } from "@/components/auth/verify-email"
export function VerifyEmailDemo() {
return
}
```
## Installation [#installation]
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/verify-email.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/verify-email.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/verify-email.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/verify-email.json
```
After install, `src/components/auth/verify-email.tsx` and the copied verify-email files are app-owned. The component reads the target email from `sessionStorage` (set on sign-up or sign-in redirect) and redirects back to sign-in when no email is stored.
## Props [#props]
# Additional Fields (/docs/zaidan/concepts/additional-fields)
`additionalFields` is an `AuthProvider` config option that declares extra user fields to render on the sign-up form and user profile. Each field describes its data type, label, and optional UI rendering. Better Auth UI then handles rendering, parsing, and submitting the value through `signUp.email` (sign-up) and `updateUser` (profile).
Zaidan copies the Solid `` renderer into your app, so the happy path matches the shadcn API while the UI remains app-owned and customizable.
Define the same fields in your Better Auth server config under `user.additionalFields`. The UI's `additionalFields` only controls rendering and form submission: the server still owns persistence and validation.
## Usage [#usage]
Install the composed auth registry entry and the profile registry entry that render additional fields:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/auth.json https://better-auth-ui.com/r/solid/user-profile.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/auth.json https://better-auth-ui.com/r/solid/user-profile.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/auth.json https://better-auth-ui.com/r/solid/user-profile.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/auth.json https://better-auth-ui.com/r/solid/user-profile.json
```
Pass an array of field configurations to the copied `` shell:
```tsx
import { AuthProvider } from "@/components/auth/auth-provider"
{children}
```
Fields default to rendering on the user profile only. Set `signUp: true` to also render the field on the sign-up form.
Additional sign-up fields without `required: true` include an ` (optional)` suffix in their label. Override the complete suffix with `localization.auth.optional`, or set it to an empty string to remove the indicator.
## Install model [#install-model]
The composed auth/profile registry entries include the additional field renderer when those forms need it. If you are building a smaller custom install, you can install the renderer by itself:
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://better-auth-ui.com/r/solid/additional-field.json
```
```bash
pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/additional-field.json
```
```bash
yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/additional-field.json
```
```bash
bun x shadcn@latest add https://better-auth-ui.com/r/solid/additional-field.json
```
The registry entry copies:
* `src/components/auth/additional-field.tsx`
* local button, input, and label primitives used by the renderer
After install, adapt validation messages, Solid `class` styling, and field layouts in your app-owned copy.
## Field types [#field-types]
The `type` controls the data type of the field. The default `inputType` is inferred from `type`, but you can override it for a different look.
| `type` | Default `inputType` | Submitted as |
| ----------- | ------------------- | ------------ |
| `"string"` | `"input"` | `string` |
| `"number"` | `"number"` | `number` |
| `"boolean"` | `"switch"` | `boolean` |
| `"date"` | `"date"` | `Date` |
## Input types [#input-types]
Override the visual rendering with `inputType`. The current Zaidan renderer favors semantic native controls instead of inventing shadcn-only primitives that the Solid registry entry does not ship yet.
| `inputType` | Zaidan rendering behavior |
| ------------ | ------------------------------------------------------------------ |
| `"input"` | Single-line native input using the copied Zaidan `Input` |
| `"textarea"` | Native textarea with Zaidan classes |
| `"number"` | Native number input with `min`, `max`, and `step` |
| `"slider"` | Native number fallback. Customize the copied renderer for a slider |
| `"switch"` | Native checkbox fallback |
| `"checkbox"` | Native checkbox |
| `"select"` | Native select from `options` |
| `"combobox"` | Native select fallback from `options`. Customize for search UI |
| `"date"` | Native date input |
| `"datetime"` | Native `datetime-local` input |
| `"hidden"` | Hidden input (submitted but not rendered) |
## Examples [#examples]
### Numeric formatting [#numeric-formatting]
`number` fields can declare `Intl.NumberFormatOptions` via `formatOptions`, matching the shared field config. The current Zaidan renderer keeps the control native, so use `formatOptions` as metadata or customize the copied renderer if you need formatted display text.
```ts
{
name: "hourlyRate",
type: "number",
label: "Hourly rate",
formatOptions: { style: "currency", currency: "USD" }
}
{
name: "commissionRate",
type: "number",
label: "Commission rate",
formatOptions: { style: "percent", maximumFractionDigits: 2 }
}
```
Use `min`, `max`, and `step` to bound the native value:
```ts
{
name: "yearsExperience",
type: "number",
label: "Years of experience",
min: 0,
max: 50,
step: 1
}
```
### Slider [#slider]
The shared config supports `inputType: "slider"`, but the current Zaidan registry entry does not ship a Solid slider primitive. The copied renderer falls back to a native number control for number fields. Replace that branch in `src/components/auth/additional-field.tsx` when your app has a Solid slider component.
```ts
{
name: "budget",
type: "number",
label: "Budget",
inputType: "slider",
min: 0,
max: 5000,
step: 50,
defaultValue: 1000,
formatOptions: { style: "currency", currency: "USD" }
}
```
### Select / Combobox [#select--combobox]
Both accept an `options` array of `{ label, value }` objects. Zaidan currently renders both as a native `