# 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 (
) } ``` ## Props [#props] # (/docs/heroui/components/settings/account/change-email) ## Usage [#usage] ```tsx file=/src/demos/heroui/settings/account/change-email.tsx import { ChangeEmail } from "@better-auth-ui/heroui" export function ChangeEmailDemo() { return (
) } ``` ## Props [#props] # (/docs/heroui/components/settings/account/user-profile) ## Usage [#usage] ```tsx file=/src/demos/heroui/settings/account/user-profile.tsx import { UserProfile } from "@better-auth-ui/heroui" export function UserProfileDemo() { return (
) } ``` ## Props [#props] # (/docs/heroui/components/settings/security/active-sessions) ## Usage [#usage] ```tsx file=/src/demos/heroui/settings/security/active-sessions.tsx import { ActiveSessions } from "@better-auth-ui/heroui" export function ActiveSessionsDemo() { return (
) } ``` ## Props [#props] # (/docs/heroui/components/settings/security/change-password) ## Usage [#usage] ```tsx file=/src/demos/heroui/settings/security/change-password.tsx import { ChangePassword } from "@better-auth-ui/heroui" 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. ## 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] # (/docs/heroui/components/settings/security/security-settings) ## Usage [#usage] ```tsx file=/src/demos/heroui/settings/security/security-settings.tsx import { SecuritySettings } from "@better-auth-ui/heroui" export function SecuritySettingsDemo() { 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 (
) } ```
```tsx title="app/settings/[path]/page.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 { 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 { auth } from "@/lib/auth" import { getQueryClient } from "@/lib/query-client" const validSettingsPaths = [ ...Object.values(viewPaths.settings), ...Object.values(organizationPlugin().viewPaths.settings) // [!code highlight] ] export default async function SettingsPage({ params }: { params: Promise<{ path: string }> }) { const { path } = await params if (!validSettingsPaths.includes(path)) { notFound() } const queryClient = getQueryClient() const session = await ensureSessionServer(queryClient, auth, { headers: await headers() }) if (!session) { redirect( `/auth/sign-in?redirectTo=${encodeURIComponent(`/settings/${path}`)}` ) } return (
) } ```
`/settings/organizations` now renders ``: the list of organizations the user belongs to plus pending invitations.
### Create the organization page [#create-the-organization-page] Mount a dynamic route at `/organization/` that renders `` for the matching tab. `` is the shell: it shows the `settings` (profile + danger zone) and `people` (members + invitations) tabs for the **active** organization. The active organization is whatever the user picked in `` (persisted on the server session via `setActive`). TanStack Start Next.js ```tsx title="routes/organization/$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/$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 (
) } ```
```tsx title="app/organization/[path]/page.tsx" import { Organization, organizationPlugin } from "@better-auth-ui/heroui/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 { auth } from "@/lib/auth" import { getQueryClient } from "@/lib/query-client" const validOrganizationPaths = Object.values( organizationPlugin().viewPaths.organization ) export default async function OrganizationPage({ params }: { params: Promise<{ path: string }> }) { const { path } = await params if (!validOrganizationPaths.includes(path)) { notFound() } const queryClient = getQueryClient() const session = await ensureSessionServer(queryClient, auth, { headers: await headers() }) if (!session) { redirect( `/auth/sign-in?redirectTo=${encodeURIComponent(`/organization/${path}`)}` ) } 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 (
) } ```
```tsx title="app/organization/[slug]/[path]/page.tsx" import { Organization, organizationPlugin } from "@better-auth-ui/heroui/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 { auth } from "@/lib/auth" import { getQueryClient } from "@/lib/query-client" const validOrganizationPaths = Object.values( organizationPlugin().viewPaths.organization ) export default async function OrganizationPage({ params }: { params: Promise<{ slug: string; path: string }> }) { const { slug, path } = await params if (!validOrganizationPaths.includes(path)) { notFound() } const queryClient = getQueryClient() const session = await ensureSessionServer(queryClient, auth, { headers: await headers() }) if (!session) { redirect( `/auth/sign-in?redirectTo=${encodeURIComponent(`/organization/${slug}/${path}`)}` ) } 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. [![Join the Better Auth UI Discord](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/social/discord-plural_vector.svg)](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 (
) } ``` ## Installation [#installation] npm pnpm yarn bun ```bash npx shadcn@latest add @better-auth-ui/account-settings ``` ```bash pnpm dlx shadcn@latest add @better-auth-ui/account-settings ``` ```bash yarn dlx shadcn@latest add @better-auth-ui/account-settings ``` ```bash bun x shadcn@latest add @better-auth-ui/account-settings ``` ## Props [#props] # (/docs/shadcn/components/settings/account/change-email) ## Usage [#usage] ```tsx file=/src/demos/shadcn/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 @better-auth-ui/change-email ``` ```bash pnpm dlx shadcn@latest add @better-auth-ui/change-email ``` ```bash yarn dlx shadcn@latest add @better-auth-ui/change-email ``` ```bash bun x shadcn@latest add @better-auth-ui/change-email ``` ## Props [#props] # (/docs/shadcn/components/settings/account/user-profile) ## Usage [#usage] ```tsx file=/src/demos/shadcn/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 @better-auth-ui/user-profile ``` ```bash pnpm dlx shadcn@latest add @better-auth-ui/user-profile ``` ```bash yarn dlx shadcn@latest add @better-auth-ui/user-profile ``` ```bash bun x shadcn@latest add @better-auth-ui/user-profile ``` ## Props [#props] # (/docs/shadcn/components/settings/security/active-sessions) ## Usage [#usage] ```tsx file=/src/demos/shadcn/settings/security/active-sessions.tsx import { ActiveSessions } from "@/components/auth/settings/security/active-sessions" export function ActiveSessionsDemo() { return (
) } ``` ## Props [#props] # (/docs/shadcn/components/settings/security/change-password) ## Usage [#usage] ```tsx file=/src/demos/shadcn/settings/security/change-password.tsx import { ChangePassword } 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 @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/linked-accounts ``` ```bash pnpm dlx shadcn@latest add @better-auth-ui/linked-accounts ``` ```bash yarn dlx shadcn@latest add @better-auth-ui/linked-accounts ``` ```bash bun x shadcn@latest add @better-auth-ui/linked-accounts ``` ## Props [#props] # (/docs/shadcn/components/settings/security/security-settings) ## Usage [#usage] ```tsx file=/src/demos/shadcn/settings/security/security-settings.tsx import { SecuritySettings } from "@/components/auth/settings/security/security-settings" export function SecuritySettingsDemo() { 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 (
) } ```
```tsx title="app/settings/[path]/page.tsx" import { viewPaths } from "@better-auth-ui/core" 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" import { organizationPlugin } from "@/lib/auth/organization-plugin" // [!code highlight] const validSettingsPaths = [ ...Object.values(viewPaths.settings), ...Object.values(organizationPlugin().viewPaths.settings) // [!code highlight] ] export default async function SettingsPage({ params }: { params: Promise<{ path: string }> }) { const { path } = await params if (!validSettingsPaths.includes(path)) { notFound() } const queryClient = getQueryClient() const session = await ensureSessionServer(queryClient, auth, { headers: await headers() }) if (!session) { redirect( `/auth/sign-in?redirectTo=${encodeURIComponent(`/settings/${path}`)}` ) } return (
) } ```
`/settings/organizations` now renders ``: the list of organizations the user belongs to plus pending invitations.
### Create the organization page [#create-the-organization-page] Mount a dynamic route at `/organization/` that renders `` for the matching tab. `` is the shell: it shows the `settings` (profile + danger zone) and `people` (members + invitations) tabs for the **active** organization. The active organization is whatever the user picked in `` (persisted on the server session via `setActive`). TanStack Start Next.js ```tsx title="routes/organization/$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/$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 (
) } ```
```tsx title="app/organization/[path]/page.tsx" 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 { Organization } from "@/components/auth/organization/organization" import { auth } from "@/lib/auth" import { getQueryClient } from "@/lib/query-client" import { organizationPlugin } from "@/lib/auth/organization-plugin" const validOrganizationPaths = Object.values( organizationPlugin().viewPaths.organization ) export default async function OrganizationPage({ params }: { params: Promise<{ path: string }> }) { const { path } = await params if (!validOrganizationPaths.includes(path)) { notFound() } const queryClient = getQueryClient() const session = await ensureSessionServer(queryClient, auth, { headers: await headers() }) if (!session) { redirect( `/auth/sign-in?redirectTo=${encodeURIComponent(`/organization/${path}`)}` ) } 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 (
) } ```
```tsx title="app/organization/[slug]/[path]/page.tsx" 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 { Organization } from "@/components/auth/organization/organization" import { auth } from "@/lib/auth" import { getQueryClient } from "@/lib/query-client" import { organizationPlugin } from "@/lib/auth/organization-plugin" const validOrganizationPaths = Object.values( organizationPlugin().viewPaths.organization ) export default async function OrganizationPage({ params }: { params: Promise<{ slug: string; path: string }> }) { const { slug, path } = await params if (!validOrganizationPaths.includes(path)) { notFound() } const queryClient = getQueryClient() const session = await ensureSessionServer(queryClient, auth, { headers: await headers() }) if (!session) { redirect( `/auth/sign-in?redirectTo=${encodeURIComponent(`/organization/${slug}/${path}`)}` ) } 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 ` ) } ``` ### 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/zaidan/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. Zaidan copies `` into your app alongside those forms, so you own the markup and can restyle the bars or drop the label entirely. 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/zaidan) Zaidan installs Better Auth UI components that your Solid application owns. This guide prepares TanStack Start and installs the authentication registry entry. You can then add settings, user controls, and plugin components. Use [Solid](/docs/solid) for `@better-auth-ui/solid` package/runtime APIs. ## Prerequisites [#prerequisites] Before you install registry entries, add these prerequisites: * [Better Auth](https://www.better-auth.com/docs/installation) * [TanStack Start for Solid](/docs/zaidan/integrations/tanstack-start) * `@better-auth-ui/solid` and `@tanstack/solid-query` * Tailwind v4 and the local UI primitives copied by Solid/Zaidan components ## Installation [#installation] ### Install the auth components [#install-the-auth-components] Install the composed auth route surface with the shadcn CLI from the Better Auth UI Solid registry. 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 ``` 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 https://better-auth-ui.com/r/solid/settings.json https://better-auth-ui.com/r/solid/user-button.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/settings.json https://better-auth-ui.com/r/solid/user-button.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/settings.json https://better-auth-ui.com/r/solid/user-button.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/settings.json https://better-auth-ui.com/r/solid/user-button.json ``` ### Install everything (optional) [#install-everything-optional] Install all Solid/Zaidan components, plugin surfaces, and email templates with one command. npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/all.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/all.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/all.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/all.json ``` ### Install plugin surfaces as needed [#install-plugin-surfaces-as-needed] Plugin registry entries are optional. If the application enables a Better Auth plugin, add its matching registry entry. npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/passkey.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/passkey.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/passkey.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/passkey.json ``` The registry entries are generated from `examples/start-solid-zaidan-example/registry.manifest.ts` and hosted under `apps/docs/public/r/solid/**`. The generated registry index is available at `https://better-auth-ui.com/r/solid/registry.json`. ## Next Steps [#next-steps] Complete the framework setup first. Then use the component and plugin pages while you review the copied files. Prepare a Solid app for Solid/Zaidan components. Render custom Better Auth user fields in copied Solid forms. ## Solid Reference [#solid-reference] Each Zaidan registry entry uses the shared `@better-auth-ui/solid` data layer. Read the Solid reference to access or change authentication state directly. Queries, mutations, and server helpers for Solid apps. ## What Zaidan includes [#what-zaidan-includes] * Installable auth, user, settings, and plugin component files. * Zaidan/Tailwind/Kobalte setup needed by those registry entries. * Links back to Solid runtime docs when an installed component needs package APIs. * Social/GitHub provider buttons are included by auth registry entry when your Better Auth client enables those providers. # TanStack Start (/docs/zaidan/integrations/tanstack-start) ## Prerequisites [#prerequisites] Complete the [Quick Start](/docs/zaidan) guide first. Solid/Zaidan components require a Solid app with Better Auth UI runtime configuration. Registry entries such as `solid/auth.json` install copied components. Your application still owns `createAuthClient`, `QueryClient`, router navigation, and the `AuthProvider` configuration. ## Integration [#integration] ### Configure AuthProvider [#configure-authprovider] Configure `AuthProvider` with TanStack Router navigation. Pass the Solid Query client from the route context. ```tsx title="components/providers.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)} )} ) } ``` The `navigate` prop connects Better Auth UI to TanStack Router. Zaidan installs Solid components, but your application owns the provider configuration. ### Update the Root Route [#update-the-root-route] Wrap your application with `Providers` in the root route. Pass the route `queryClient` to the provider. ```tsx title="routes/__root.tsx" file=/../../examples/start-solid-zaidan-example/src/routes/__root.tsx import type { QueryClient } from "@tanstack/solid-query" import { createRootRouteWithContext, HeadContent, Outlet, Scripts } from "@tanstack/solid-router" import type { JSX } from "solid-js" import { HydrationScript } from "solid-js/web" import { Header } from "@/components/header" import { Providers } from "@/components/providers" import { themeScript } from "@/lib/theme" import "../styles/globals.css" export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ component: RootComponent, head: () => ({ meta: [ { charset: "utf-8" }, { name: "viewport", content: "width=device-width, initial-scale=1" }, { title: "Start Solid Zaidan Example" } ] }), shellComponent: RootDocument }) function RootComponent() { return } function RootDocument(props: { children: JSX.Element }) { const routeContext = Route.useRouteContext() return ( {() => ( <>
{props.children}
)} ) } ``` The root route also imports the global Tailwind v4 stylesheet. It renders shared interface elements such as the header. ### Create the Auth Page [#create-the-auth-page] Install the composed authentication registry entry. Then create a dynamic page that selects the authentication view from the URL segment. 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 ``` ```tsx title="routes/auth/$path.tsx" file=/../../examples/start-solid-zaidan-example/src/routes/auth/$path.tsx import { viewPaths } from "@better-auth-ui/core" import { createFileRoute, redirect } from "@tanstack/solid-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), ...Object.values(magicLinkPlugin().viewPaths.auth), ...Object.values(organizationPlugin().viewPaths.auth), ...Object.values(emailOtpPlugin().viewPaths.auth), ...Object.values(twoFactorPlugin().viewPaths.auth) ]) 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 registry entry, 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 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 ``` ```tsx title="routes/settings/$path.tsx" file=/../../examples/start-solid-zaidan-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/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 { 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()().path return (
) } ``` The `viewPaths.settings` object contains the base segments `account` and `security`. Plugin registry entries can add segments through their local configuration.
### Add the User Button (optional) [#add-the-user-button-optional] If the application shell needs a signed-in user menu, install the composed user button registry entry. 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 ``` The example application renders the button in the header. The copied Solid component supports SSR. It renders a lightweight shell first. Then it creates session queries after the component mounts. ## 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, A, redirect } from "@tanstack/solid-router" import { createIsomorphicFn } from "@tanstack/solid-start" import { getRequestHeaders } from "@tanstack/solid-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 can reuse the hydrated session. ### 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 { useAuthenticate } from "@better-auth-ui/solid" import { createFileRoute, A } from "@tanstack/solid-router" import { Show } from "solid-js" import { authClient } from "@/lib/auth-client" export const Route = createFileRoute("/dashboard")({ component: Dashboard }) function Dashboard() { const session = useAuthenticate(authClient) return (
} > {(currentSession) => (

Hello, {currentSession().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-solid-zaidan-example](https://github.com/better-auth-ui/better-auth-ui/tree/main/examples/start-solid-zaidan-example) in the repository. ## Next Steps [#next-steps] Read about the shared Solid queries and mutations that power each Zaidan registry entry. Queries, mutations, and server helpers for Solid apps. Every auth read, with usage and server-side recipes. Every auth write, with mutation keys and cache side effects. # Admin (/docs/zaidan/plugins/admin) The Admin plugin adds a static `/admin/users` page and a user-detail dialog. It also adds a "Stop impersonating" action to the copied ``. ## Setup [#setup] ### Enable the Better Auth admin plugin [#enable-the-better-auth-admin-plugin] ```ts title="src/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 Solid client plugin [#add-the-matching-solid-client-plugin] ```ts title="src/lib/auth-client.ts" import { createAuthClient } from "@better-auth-ui/solid" import { adminClient } from "better-auth/client/plugins" // [!code highlight] export const authClient = createAuthClient({ plugins: [adminClient()] // [!code highlight] }) ``` ### Install the Solid and Zaidan integration [#install-the-solid-and-zaidan-integration] npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/admin.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/admin.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/admin.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/admin.json ``` This copies: * `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 copied UI plugin [#register-the-copied-ui-plugin] ```tsx title="src/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 TanStack Start route: ```tsx title="src/routes/admin/users.tsx" import { createFileRoute } from "@tanstack/solid-router" import { Admin } from "@/components/auth/admin/admin" export const Route = createFileRoute("/admin/users")({ component: () => }) ``` Use `` when a parent route supplies the final static path segment. The user-detail dialog keeps user IDs out of the route contract. Applications can control the selected user through ``: ```tsx import { AdminUsers } from "@/components/auth/admin/admin-users" ``` ## 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 dialog 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 either `email` or `name` in each request. Passwords stay in local form state. The forms clear each password after the request or when the user closes the form. Session IP addresses are hidden unless `showIpAddress` is `true`. ```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] The copied `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/solid/plugins/admin" const stopImpersonating = useStopImpersonating(authClient) ``` Use the hook when you need the same behavior outside the copied user button. It restores the admin session and awaits invalidation of the shared session query. See the [Solid package reference](/docs/solid) for the shared query and mutation APIs. # Agent Auth (/docs/zaidan/plugins/agent-auth) The Agent Auth plugin adds the application-owned UI that the protocol itself does not render. The approval page shows the requesting agent, its host, its mode, the capabilities it wants, and the approval strength each one needs. Users allow a subset or deny outright. A security settings card then lists agents and revokes individual active grants. ## Setup [#setup] ### Configure the Better Auth server [#configure-the-better-auth-server] Point the approval page at the BAUI route and describe each capability. ```ts title="src/lib/auth.ts" import { agentAuth } from "@better-auth/agent-auth" // [!code highlight] import { betterAuth } from "better-auth" export const auth = betterAuth({ // ... plugins: [ agentAuth({ // [!code highlight] 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" } ] }) ] }) ``` ### Install the UI plugin [#install-the-ui-plugin] npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/agent-auth.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/agent-auth.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/agent-auth.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/agent-auth.json ``` ### Register the plugin [#register-the-plugin] ```tsx title="src/components/providers.tsx" import { AuthProvider } from "@/components/auth/auth-provider" import { agentAuthPlugin } from "@/lib/auth/agent-auth-plugin" // [!code highlight] import { authClient } from "@/lib/auth-client" export function Providers(props: { children?: JSX.Element }) { return ( {props.children} ) } ``` `grants: true` adds the `` security card. Leave it off if you only need the approval page. Approval strength is the server's decision, not the UI's. The badge tells the user what a capability requires. The server still enforces it. ## The approval page [#the-approval-page] The page reads `agent_id`, and optionally `approval_id` and `code`, from the query string. Without `agent_id` it renders an invalid-request state rather than guessing. Signed-out users are sent to sign-in with a `redirectTo` back to the full approval URL, so the request survives the round trip. Capabilities start fully selected. Clearing one removes it from the approval, and clearing all of them disables the allow button. ## Runtime prerequisites [#runtime-prerequisites] * Server: `agentAuth()` from `@better-auth/agent-auth`, with `deviceAuthorizationPage` pointing at your route. * App: an `AgentAuthAdapter` instance. * Runtime API: agent query and mutation options from [Solid runtime APIs](/docs/solid). ## Copied files [#copied-files] * `src/lib/auth/agent-auth-plugin.ts` * `src/components/auth/agent-auth/agent-approval.tsx` * `src/components/auth/agent-auth/agent-authorizations.tsx` After install both views are yours. Group capabilities differently, add your own risk copy, or render the constraints payload instead of the raw JSON. ## Options [#options] ## Localization [#localization] # Anonymous (/docs/zaidan/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="src/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 Solid client: ```ts title="src/lib/auth-client.ts" import { createAuthClient } from "better-auth/solid" 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 Solid button and local plugin factory: ```bash bunx --bun shadcn@latest add https://better-auth-ui.com/r/solid/anonymous.json ``` 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="src/components/providers.tsx" import { AuthProvider } from "@/components/auth/auth-provider" import { anonymousPlugin } from "@/lib/auth/anonymous-plugin" // [!code highlight] {props.children} ``` ## Change the label [#change-the-label] ```tsx anonymousPlugin({ localization: { continueAsGuest: "Explore as a guest" } }) ``` For custom Solid surfaces, use `useSignInAnonymous` from `@better-auth-ui/solid/plugins/anonymous`. See the [Solid package reference](/docs/solid) for the shared runtime APIs. 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/zaidan/plugins/api-key) The API key plugin adds programmatic API key management to your Solid/Zaidan authentication settings. Users can create, reveal once, copy, list, and revoke API keys from security and organization settings. New keys can use a configurable expiration interval, and each listed key shows when it expires. It contributes: * An `` card rendered by the copied `SecuritySettings` component when the API Key plugin is enabled * An `` card for organization-owned keys when `apiKeyPlugin({ organization: true })` is registered * Solid query and mutation wiring through `listApiKeysOptions`, `createApiKeyOptions`, and `deleteApiKeyOptions` * App-owned API key cards, empty state, loading placeholder, create dialog, new-key reveal dialog, and delete confirmation dialog ## 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="src/lib/auth.ts" import { apiKey } from "@better-auth/api-key" // [!code highlight] import { betterAuth } from "better-auth" export const auth = betterAuth({ // ... plugins: [ apiKey() // [!code highlight] ] }) ``` ### Install the matching client plugin [#install-the-matching-client-plugin] Add `apiKeyClient()` to your Solid auth client so `authClient.apiKey.*` methods are available: ```ts title="src/lib/auth-client.ts" import { apiKeyClient } from "@better-auth/api-key/client" // [!code highlight] import { createAuthClient } from "better-auth/solid" export const authClient = createAuthClient({ plugins: [ apiKeyClient() // [!code highlight] ] }) ``` ### Install the Solid/Zaidan components [#install-the-solidzaidan-components] Run the shadcn CLI to install the Solid API key components and the `apiKeyPlugin()` factory into your project: npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/api-key.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/api-key.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/api-key.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/api-key.json ``` This drops the following into your codebase: * `src/lib/auth/api-key-plugin.ts`: local `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`: loading placeholder 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] For user-owned keys, the copied `SecuritySettings` component renders `` whenever the API Key plugin is enabled. For organization-owned keys, register the copied UI plugin with `{ organization: true }`: ```tsx title="src/components/providers.tsx" import { apiKeyPlugin } from "@/lib/auth/api-key-plugin" // [!code highlight] {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 the copied `apiKeyPlugin()`: ```tsx title="src/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="src/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="src/components/providers.tsx" apiKeyPlugin({ keyExpiration: false }) ``` ## Components [#components] ### `` [#apikeys-] The security settings page shows `` when its layout includes `SecuritySettings` and enables the API Key plugin. The component uses the [Solid runtime APIs](/docs/solid) to list, create, and revoke keys. **Usage** ```tsx import { ApiKeys } from "@/components/auth/api-key/api-keys" ``` This component is normally mounted by `SecuritySettings`. Render it manually only if you are building a custom settings layout. **Props** ### `` [#organizationapikeys-] This component wraps ``. It resolves the organization and checks the current user's membership. It renders the API keys only for an organization owner. To enable, opt in on the copied UI plugin and add a matching API key configuration to your Better Auth server config. The organization API key config uses a fixed `configId` of `"organization"`, so the server entry **must** be `{ configId: "organization", references: "organization" }`: ```tsx title="src/components/providers.tsx" import { apiKeyPlugin } from "@/lib/auth/api-key-plugin" {children} ``` ```ts title="src/lib/auth.ts" import { apiKey } from "@better-auth/api-key" import { betterAuth } from "better-auth" 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] ## Runtime API references [#runtime-api-references] ### List API keys [#list-api-keys] ### Create API key [#create-api-key] ### Delete API key [#delete-api-key] ## 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 Solid rename surface. # Billing (/docs/zaidan/plugins/billing) The billing plugin adds a billing tab to personal or organization settings. Every view reads a provider-neutral `BillingAdapter`. BAUI includes adapters for Stripe, Polar, Autumn, Creem, Dodo Payments, and Commet. The UI covers: * Pricing plans with monthly, yearly, and one-time prices * Current subscription status and renewal or end date * Checkout and plan changes * Billing portal access * Cancellation and restoration * Seat management * Metered usage ## 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 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 UI plugin [#install-the-ui-plugin] npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/billing.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/billing.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/billing.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/billing.json ``` ### Register the plugin [#register-the-plugin] Build an adapter and hand it to the plugin. ```tsx title="src/components/providers.tsx" import { createStripeBillingAdapter } from "@better-auth-ui/core/plugins/billing" // [!code highlight] import { AuthProvider } from "@/components/auth/auth-provider" import { billingPlugin } from "@/lib/auth/billing-plugin" // [!code highlight] import { authClient } from "@/lib/auth-client" const billingAdapter = createStripeBillingAdapter(authClient, { plans, successUrl: "/settings/billing?checkout=success", cancelUrl: "/settings/billing?checkout=canceled", returnUrl: "/settings/billing" }) // [!code highlight] export function Providers(props: { children?: JSX.Element }) { return ( {props.children} ) } ``` `user` defaults to `true` and adds the tab to personal settings. `organization` defaults to `false`. Turn it on to add the same tab to organization settings. ## Bundled adapters [#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 | The factories are `createStripeBillingAdapter`, `createPolarBillingAdapter`, `createAutumnBillingAdapter`, `createCreemBillingAdapter`, `createDodoPaymentsBillingAdapter`, and `createCommetBillingAdapter`. They use the same `BillingPlan` list and work with both React and Solid billing hooks. 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. The adapter's `supports` flags decide which controls render. A provider that cannot restore a canceled subscription does not show the restore button. ## Writing your own adapter [#writing-your-own-adapter] `BillingAdapter` is a plain interface: list plans, read state, start checkout, open a portal, cancel, restore, and update seats. Implement it against any billing service and the views work unchanged. ## Runtime prerequisites [#runtime-prerequisites] * Server and client: a Better Auth billing plugin, or your own billing endpoints. * App: a `BillingAdapter` instance. * Runtime API: billing query and mutation options from [Solid runtime APIs](/docs/solid). ## Copied files [#copied-files] * `src/lib/auth/billing-plugin.tsx` * `src/components/auth/billing/billing-settings.tsx` After install the plan cards, usage bars, and dialogs are yours to restyle. The usage bar is rendered inline with `role="progressbar"` rather than a Zaidan primitive, since Zaidan does not ship one. ## Options [#options] ## Localization [#localization] # Captcha (/docs/zaidan/plugins/captcha) The captcha plugin adds a widget to the copied Solid/Zaidan authentication forms. It sends the resolved token in the `x-captcha-response` header. The plugin supports Cloudflare Turnstile, hCaptcha, CaptchaFox, and reCAPTCHA. Provide a Solid `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 copied authentication forms already contain the captcha widget slot. Configure the Better Auth server plugin and register the Solid UI 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 choose a provider: ```ts title="src/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, Better Auth protects `/sign-up/email`, `/sign-in/email`, and `/request-password-reset`. 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 title="src/lib/auth.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 Solid UI plugin [#register-the-solid-ui-plugin] Pass `captchaPlugin({ render })` to ``. `render` is a Solid component that receives `setToken`, `clearToken`, and `setReset` and is responsible for mounting your provider widget. ```tsx title="src/components/providers.tsx" import { captchaPlugin } from "@better-auth-ui/solid/plugins/captcha" // [!code highlight] import { AuthProvider } from "@/components/auth/auth-provider" import { authClient } from "@/lib/auth-client" import { CaptchaWidget } from "@/components/captcha-widget" // [!code highlight] {children} ``` The `render` component is mounted as a real Solid component, so Solid primitives and context work inside it. See the [Providers](#providers) section below for ready-to-use widget patterns. ## Providers [#providers] The examples use [`@better-captcha/solidjs`](https://www.better-captcha.dev/docs/frameworks/solidjs). This library supports the providers that Better Auth can verify. You can use another Solid widget. It must call `setToken` after success, call `clearToken` after an error, and register `reset` with `setReset`. The server-side Better Auth provider names are the same as shadcn. Only the client-side widget package changes from React to Solid: | Provider | Better Auth `provider` | shadcn React widget | Solid/Zaidan widget | | -------------------- | ------------------------ | --------------------------- | ---------------------------------------------- | | Cloudflare Turnstile | `"cloudflare-turnstile"` | `@marsidev/react-turnstile` | `@better-captcha/solidjs/provider/turnstile` | | hCaptcha | `"hcaptcha"` | `@hcaptcha/react-hcaptcha` | `@better-captcha/solidjs/provider/hcaptcha` | | CaptchaFox | `"captchafox"` | `@captchafox/react` | `@better-captcha/solidjs/provider/captcha-fox` | ### Cloudflare Turnstile [#cloudflare-turnstile] npm pnpm yarn bun ```bash npm install @better-captcha/solidjs ``` ```bash pnpm add @better-captcha/solidjs ``` ```bash yarn add @better-captcha/solidjs ``` ```bash bun add @better-captcha/solidjs ``` ```tsx title="src/components/turnstile-widget.tsx" import type { CaptchaRenderProps } from "@better-auth-ui/solid/plugins/captcha" import { createCaptchaController, Turnstile, type TurnstileHandle } from "@better-captcha/solidjs/provider/turnstile" import { onCleanup, onMount } from "solid-js" export function TurnstileWidget({ setToken, clearToken, setReset }: CaptchaRenderProps) { const controller = createCaptchaController() onMount(() => { setReset(() => controller.handle()?.reset()) }) onCleanup(() => setReset(null)) return ( ) } ``` ```tsx title="src/components/providers.tsx" import { captchaPlugin } from "@better-auth-ui/solid/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 @better-captcha/solidjs ``` ```bash pnpm add @better-captcha/solidjs ``` ```bash yarn add @better-captcha/solidjs ``` ```bash bun add @better-captcha/solidjs ``` ```tsx title="src/components/hcaptcha-widget.tsx" import type { CaptchaRenderProps } from "@better-auth-ui/solid/plugins/captcha" import { createCaptchaController, HCaptcha, type HCaptchaHandle } from "@better-captcha/solidjs/provider/hcaptcha" import { onCleanup, onMount } from "solid-js" export function HCaptchaWidget({ setToken, clearToken, setReset }: CaptchaRenderProps) { const controller = createCaptchaController() onMount(() => { setReset(() => controller.handle()?.reset()) }) onCleanup(() => setReset(null)) return ( ) } ``` ```tsx title="src/components/providers.tsx" import { captchaPlugin } from "@better-auth-ui/solid/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 @better-captcha/solidjs ``` ```bash pnpm add @better-captcha/solidjs ``` ```bash yarn add @better-captcha/solidjs ``` ```bash bun add @better-captcha/solidjs ``` ```tsx title="src/components/captchafox-widget.tsx" import type { CaptchaRenderProps } from "@better-auth-ui/solid/plugins/captcha" import { CaptchaFox, type CaptchaFoxHandle, createCaptchaController } from "@better-captcha/solidjs/provider/captcha-fox" import { onCleanup, onMount } from "solid-js" export function CaptchaFoxWidget({ setToken, clearToken, setReset }: CaptchaRenderProps) { const controller = createCaptchaController() onMount(() => { setReset(() => controller.handle()?.reset()) }) onCleanup(() => setReset(null)) return ( ) } ``` ```tsx title="src/components/providers.tsx" import { captchaPlugin } from "@better-auth-ui/solid/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/zaidan/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 UI plugin [#install-the-ui-plugin] ```bash bunx shadcn@latest add https://better-auth-ui.com/r/solid/dash.json ``` ### Register the UI plugin [#register-the-ui-plugin] ```tsx title="src/components/providers.tsx" import { AuthProvider } from "@/components/auth/auth-provider" import { dashPlugin } from "@/lib/auth/dash-plugin" import { organizationPlugin } from "@/lib/auth/organization-plugin" export function Providers(props: { children?: JSX.Element }) { return ( {props.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/solid/plugins/dash" ``` The core package also exports query option factories and `ensure`, `prefetch`, and `fetch` helpers from `@better-auth-ui/core/plugins/dash`. ## Runtime prerequisites [#runtime-prerequisites] * Server: `dash()` from `@better-auth/infra`. * Client: `dashClient()` from `@better-auth/infra/client`. * Organization activity: the Better Auth organization server and client plugins. * Runtime API: Dash hooks and query helpers from the [Solid package](/docs/solid). ## Copied files [#copied-files] * `src/lib/auth/dash-plugin.tsx` * `src/components/auth/dash/activity.tsx` ## Options [#options] ## Localization [#localization] # Delete User (/docs/zaidan/plugins/delete-user) The copied Solid/Zaidan delete-user components render the danger-zone account deletion UI for Better Auth's built-in [account deletion](https://www.better-auth.com/docs/concepts/users-accounts#delete-user) capability. Register the UI plugin directly from `@better-auth-ui/core/plugins/delete-user` to show `` in the copied security settings surface. ## Setup [#setup] ### Enable account deletion in Better Auth [#enable-account-deletion-in-better-auth] Enable Better Auth account deletion in your server config: ```ts title="src/lib/auth.ts" import { betterAuth } from "better-auth" export const auth = betterAuth({ // ... user: { deleteUser: { enabled: true } } }) ``` For OAuth users or flows that confirm deletion by email, configure the related Better Auth options. See the [delete-user documentation](https://www.better-auth.com/docs/concepts/users-accounts#delete-user). ### Install the Solid/Zaidan components [#install-the-solidzaidan-components] npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/delete-user.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/delete-user.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/delete-user.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/delete-user.json ``` This copies the following files into your project: * `src/components/auth/delete-user/danger-zone.tsx` * `src/components/auth/delete-user/delete-account.tsx` * local dialog, button, card, input, and label primitives under `src/components/ui/**` ### Register the UI plugin [#register-the-ui-plugin] Import `deleteUserPlugin()` directly from `@better-auth-ui/core/plugins/delete-user` and register it with your copied Solid auth provider: ```tsx title="src/components/providers.tsx" import { deleteUserPlugin } from "@better-auth-ui/core/plugins/delete-user" import { AuthProvider } from "@/components/auth/auth-provider" {children} ``` When the plugin is registered, the copied `` surface renders `` automatically. ## Components [#components] ### `` [#dangerzone-] `` is the primary security card wrapper. It renders the localized danger-zone heading and composes `` below it. ```tsx import { DangerZone } from "@/components/auth/delete-user/danger-zone" ``` ### `` [#deleteaccount-] `` is the lower-level delete account card used inside ``. You can also import it directly for custom settings layouts. When credential confirmation is required, the password starts masked and includes a localized show/hide control. ```tsx import { DeleteAccount } from "@/components/auth/delete-user/delete-account" ``` ## Options [#options] ## Localization [#localization] # Device Authorization (/docs/zaidan/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 copied `` 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 * Solid Query mutation options for verifying, approving, and denying 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="src/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 Solid client [#configure-the-solid-client] Add `deviceAuthorizationClient()` to the auth client: ```ts title="src/lib/auth-client.ts" import { createAuthClient } from "@better-auth-ui/solid" import { deviceAuthorizationClient } from "better-auth/client/plugins" // [!code highlight] export const authClient = createAuthClient({ plugins: [deviceAuthorizationClient()] // [!code highlight] }) ``` ### Install the Solid and Zaidan view [#install-the-solid-and-zaidan-view] ```bash bunx --bun shadcn@latest add https://better-auth-ui.com/r/solid/device-authorization.json ``` This copies the following files into your project: * `src/lib/auth/device-authorization-plugin.ts` * `src/components/auth/device-authorization/device-authorization.tsx` * Local Zaidan UI primitives under `src/components/ui/**` After install, the view and plugin factory belong to your application. You can adapt their layout or behavior without waiting for a package release. ### Register the UI plugin [#register-the-ui-plugin] Pass `deviceAuthorizationPlugin()` to the copied ``: ```tsx title="src/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="src/routes/auth/$path.tsx" import { viewPaths } from "@better-auth-ui/core" import { createFileRoute, redirect } from "@tanstack/solid-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 redirect({ to: "/" }) } }, component: AuthPage }) function AuthPage() { const { path } = Route.useParams()() return } ``` ## Component [#component] The copied 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] ## Solid APIs [#solid-apis] The copied view uses these Solid Query option factories: * [`verifyDeviceCodeOptions`](/docs/solid/mutations/verify-device-code) verifies and claims the submitted code for the current session. * [`approveDeviceOptions`](/docs/solid/mutations/approve-device) grants the requesting device access. * [`denyDeviceOptions`](/docs/solid/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/zaidan/components/active-sessions) to list sessions and revoke access for an approved device. # Email OTP (/docs/zaidan/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 keep the link-based views you like and replace only the ones you do not. It contributes: * A copied `` sign-in view rendered at `/auth/email-otp`, plus a toggle button * Code-based replacements for the verify-email, forgot-password, reset-password, and change-email surfaces * Solid runtime wiring through [`signInEmailOtpOptions`](/docs/solid/mutations/sign-in-email-otp) and the other email-OTP mutation factories 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/zaidan/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 `emailOTP({ sendVerificationOTP })` to your Better Auth server config. One callback serves all four flows: `type` tells you which one: ```ts title="src/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] ] }) ``` Replace example code logging with a real email provider before production. ### Install the matching Solid client plugin [#install-the-matching-solid-client-plugin] ```ts title="src/lib/auth-client.ts" import { emailOTPClient } from "better-auth/client/plugins" // [!code highlight] import { createAuthClient } from "@better-auth-ui/solid" export const authClient = createAuthClient({ plugins: [ emailOTPClient() // [!code highlight] ] }) ``` ### Add the copied components [#add-the-copied-components] npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/email-otp.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/email-otp.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/email-otp.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/email-otp.json ``` 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/*.tsx`: sign-in, toggle button, verification, password reset, and change-email views * Provider-button and last-used badge components needed by the passwordless sign-in view The registry also refreshes ``, so enabling the change-email override does not require a separate registry command. ### Register the plugin [#register-the-plugin] Turn on the flows you want: ```tsx title="src/components/providers.tsx" import { emailOtpPlugin } from "@/lib/auth/email-otp-plugin" // [!code highlight] {props.children} ``` ### Allow the new view path [#allow-the-new-view-path] The plugin contributes an `email-otp` segment to `viewPaths.auth`. Spread it into your auth route's allowed-path set: ```tsx title="src/routes/auth/$path.tsx" import { viewPaths } from "@better-auth-ui/core" import { createFileRoute, redirect } from "@tanstack/solid-router" import { Auth } from "@/components/auth/auth" import { emailOtpPlugin } from "@/lib/auth/email-otp-plugin" // [!code highlight] const validAuthPathSegments = new Set([ ...Object.values(viewPaths.auth), ...Object.values(emailOtpPlugin().viewPaths.auth) // [!code highlight] ]) export const Route = createFileRoute("/auth/$path")({ beforeLoad({ params: { path } }) { if (!validAuthPathSegments.has(path)) { throw redirect({ to: "/" }) } }, component: AuthPage }) ``` ## 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 copied `` 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 plugin 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. ### `` [#verifyemailotp-] Rendered at `/auth/verify-email` when `emailVerification` is on. Reads the pending address from session storage 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. ### `` [#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. # Last Login Method (/docs/zaidan/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 Solid client passed to ``: ```ts title="lib/auth-client.ts" import { createAuthClient } from "@better-auth-ui/solid" 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 Solid registry item: ```bash bunx --bun shadcn@latest add https://better-auth-ui.com/r/solid/last-login-method.json ``` ### Register the UI plugin [#register-the-ui-plugin] Register the copied UI plugin 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/zaidan/plugins/magic-link) The Magic Link plugin adds a passwordless email sign-in flow to the copied Solid/Zaidan auth UI. Users enter an email address, receive a one-time link, and sign in when they open it. It contributes: * A copied `` view rendered at `/auth/magic-link` * A copied `` confirmation view rendered at `/auth/magic-link-sent` * A Magic Link toggle button that links between password sign-in and the magic-link view * Solid runtime wiring through [`signInMagicLinkOptions`](/docs/solid/mutations/sign-in-magic-link) ## Setup [#setup] ### Install the Better Auth plugin [#install-the-better-auth-plugin] Add `magicLink({ sendMagicLink })` to your Better Auth server config and connect `sendMagicLink` to your email provider: ```ts title="src/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` with your provider. // [!code highlight] } // [!code highlight] }) // [!code highlight] ] }) ``` Replace example email logging with a real email provider before production. ### Install the matching Solid client plugin [#install-the-matching-solid-client-plugin] Add `magicLinkClient()` to your Solid auth client so `authClient.signIn.magicLink` is available: ```ts title="src/lib/auth-client.ts" import { magicLinkClient } from "better-auth/client/plugins" // [!code highlight] import { createAuthClient } from "better-auth/solid" // [!code highlight] export const authClient = createAuthClient({ plugins: [ magicLinkClient() // [!code highlight] ] }) ``` ### Install the Solid/Zaidan components [#install-the-solidzaidan-components] npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/magic-link.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/magic-link.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/magic-link.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/magic-link.json ``` This copies the following files into your project: * `src/lib/auth/magic-link-plugin.ts` * `src/components/auth/magic-link.tsx` * `src/components/auth/magic-link-sent.tsx` * `src/components/auth/magic-link-button.tsx` * `src/components/auth/open-email-button.tsx` * local form UI primitives under `src/components/ui/**` ### Register the UI plugin [#register-the-ui-plugin] Register `magicLinkPlugin()` in your copied Solid auth provider: ```tsx title="src/components/providers.tsx" import { AuthProvider } from "@/components/auth/auth-provider" import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin" // [!code highlight] {children} ``` ### Allow the plugin auth route [#allow-the-plugin-auth-route] The copied route must accept the plugin-contributed auth path. Keep provider registration and route validation aligned, especially if you customize `magicLinkPlugin({ path })`: ```tsx title="src/routes/auth/$path.tsx" import { viewPaths } from "@better-auth-ui/core" import { createFileRoute, redirect } from "@tanstack/solid-router" import { Auth } from "@/components/auth/auth" import { magicLinkPlugin } from "@/lib/auth/magic-link-plugin" // [!code highlight] const validAuthPathSegments = new Set([ ...Object.values(viewPaths.auth), ...Object.values(magicLinkPlugin().viewPaths.auth) // [!code highlight] ]) 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 } ``` `/auth/magic-link` and `/auth/magic-link-sent` work after both the provider registration step and the route-wiring step are in place. ## Components [#components] ### `` [#magiclink-] The copied `` view is rendered when the Magic Link plugin is registered and the auth route accepts the plugin path. ```tsx import { MagicLink } from "@/components/auth/magic-link" ``` ### `` [#magiclinksent-] After a magic-link request succeeds, the form stores the submitted email in session storage and navigates to this confirmation view. It redirects back to the magic-link form when that state is missing and 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] Override the route segment or localization by configuring the copied `magicLinkPlugin()` factory: ```ts title="src/lib/auth/magic-link-plugin.ts" magicLinkPlugin({ path: "email-link", sentPath: "email-link-sent", localization: { sendMagicLink: "Email me a link" } }) ``` If you override `path` or `sentPath`, use the same plugin options in both your provider registration and your auth-route validation. ## Localization [#localization] In a custom Solid component, read Magic Link labels from the registered plugin metadata. This keeps the form and toggle button consistent. ## Email template [#email-template] You can pair `sendMagicLink` with the existing [``](/docs/shadcn/components/email/magic-link-email) example, or render your own email template on the server. ## Passwordless-only [#passwordless-only] If your app disables email and password auth, you can keep the Magic Link flow as the primary sign-in surface: ```tsx {children} ``` The copied plugin already provides `fallbackViews.auth.signIn`. The `signUp`, `forgotPassword`, `resetPassword`, and `resetLinkSent` views redirect there when password auth is disabled. Route behavior still depends on the setup steps above: register `magicLinkPlugin()` and allow both plugin auth paths in `/auth/$path`. # Multi Session (/docs/zaidan/plugins/multi-session) The Multi Session plugin lets users stay signed in to multiple accounts at once, switch the active account from the user menu, and manage device sessions from account settings. It contributes: * A plugin-injected switch-account submenu inside `` * A plugin-injected `` card inside `` * Solid runtime helpers for listing, switching, and revoking device sessions ## Setup [#setup] ### Install the Better Auth plugin [#install-the-better-auth-plugin] Add `multiSession()` to your Better Auth server config: ```ts title="src/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 Solid client plugin [#install-the-matching-solid-client-plugin] Add `multiSessionClient()` so `authClient.multiSession.*` methods are available to the copied Solid components: ```ts title="src/lib/auth-client.ts" import { createAuthClient } from "better-auth/solid" // [!code highlight] import { multiSessionClient } from "better-auth/client/plugins" // [!code highlight] export const authClient = createAuthClient({ plugins: [multiSessionClient()] // [!code highlight] }) ``` ### Install the Solid/Zaidan components [#install-the-solidzaidan-components] npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/multi-session.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/multi-session.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/multi-session.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/multi-session.json ``` This copies the following files into your project: * `src/lib/auth/multi-session-plugin.ts` * `src/components/auth/multi-session/manage-account.tsx` * `src/components/auth/multi-session/manage-accounts.tsx` * `src/components/auth/multi-session/switch-account-submenu.tsx` * `src/components/auth/multi-session/switch-account-submenu-content.tsx` * `src/components/auth/multi-session/switch-account-submenu-item.tsx` * `src/components/auth/settings/shared/helpers.ts` * `src/components/auth/settings/shared/types.ts` * copied UI primitives under `src/components/ui/**` ### Register the copied UI plugin [#register-the-copied-ui-plugin] Register the local `multiSessionPlugin()` wrapper in your copied provider: ```tsx title="src/components/providers.tsx" import { AuthProvider } from "@/components/auth/auth-provider" import { multiSessionPlugin } from "@/lib/auth/multi-session-plugin" // [!code highlight] {children} ``` The copied wrapper composes the core Better Auth UI metadata and injects Solid UI slots for account settings and the user menu. ## Components [#components] ### `` [#userbutton-] When `multiSessionPlugin()` is registered, `` renders plugin-contributed user menu items, including the switch-account submenu. ```tsx import { UserButton } from "@/components/auth/user/user-button" ``` ### `` [#switchaccountsubmenu-] ```tsx import { SwitchAccountSubmenu } from "@/components/auth/multi-session/switch-account-submenu" ``` ### `` [#manageaccounts-] `` is rendered through plugin-contributed `accountCards`, so it appears inside `` when the plugin is registered. ```tsx import { ManageAccounts } from "@/components/auth/multi-session/manage-accounts" ``` ## Options [#options] The copied local wrapper accepts the same core plugin options: ```ts title="src/lib/auth/multi-session-plugin.ts" multiSessionPlugin({ localization: { switchAccount: "Switch Account", addAccount: "Add account", manageAccounts: "Manage accounts" } }) ``` ## Localization [#localization] The copied submenu and manage-accounts components read localization from the registered plugin first, then fall back to the core defaults. ## Session management [#session-management] The copied Solid components use the Solid runtime helpers directly: * `listDeviceSessionsOptions` * `setActiveSessionOptions` * `revokeMultiSessionOptions` References: * [listDeviceSessionsOptions](/docs/solid/queries/list-device-sessions) * [setActiveSessionOptions](/docs/solid/mutations/set-active-session) * [revokeMultiSessionOptions](/docs/solid/mutations/revoke-multi-session) These APIs power the switch-account submenu, current-session indicator, add-account navigation, and device-session revoke/switch actions. # OAuth Provider (/docs/zaidan/plugins/oauth-provider) The OAuth Provider plugin provides the pages used by the [Better Auth OAuth 2.1 Provider](https://better-auth.com/docs/plugins/oauth-provider). It also provides a security card for authorized applications. It contributes: * A copied `` view at `/auth/oauth-consent` * A copied `` view at `/auth/oauth-sign-up`, for `prompt=create` * A copied `` view at `/auth/select-account`, for `prompt=select_account` * A copied `` card in security settings * Public OAuth client metadata loading * Scope labels as a map, a list, or a resolver * Headless continuation through `oauthContinueOptions`, 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="src/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 Solid client [#configure-the-solid-client] Add `oauthProviderClient()` to the auth client. It preserves Better Auth's signed authorization query when the user responds: ```ts title="src/lib/auth-client.ts" import { oauthProviderClient } from "@better-auth/oauth-provider/client" import { createAuthClient } from "@better-auth-ui/solid" 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 Solid and Zaidan views [#install-the-solid-and-zaidan-views] ```bash bunx shadcn@latest add https://better-auth-ui.com/r/solid/oauth-provider.json ``` 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 * `src/components/auth/user/user-avatar.tsx` ### Register the UI plugin [#register-the-ui-plugin] ```tsx title="src/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="src/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`. ## 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="src/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="src/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="src/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 [`oauthContinueOptions`](/docs/solid/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` using the Zaidan `Item` primitives. 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="src/routes/auth/select-workspace.tsx" import { oauthContinueOptions } from "@better-auth-ui/core/plugins/oauth-provider" import { useAuth } from "@better-auth-ui/solid" import { createMutation } from "@tanstack/solid-query" import { For } from "solid-js" type Workspace = { slug: string; name: string } function SelectWorkspace(props: { workspaces: Workspace[] }) { const auth = useAuth() const oauthContinue = createMutation(() => oauthContinueOptions(auth.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 ( {(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] ## Solid APIs [#solid-apis] * [`usePublicOAuthClient`](/docs/solid/queries/public-oauth-client) loads application metadata * [`useOAuthConsent`](/docs/solid/mutations/oauth-consent) submits the user's decision * [`oauthContinueOptions`](/docs/solid/mutations/oauth-continue) resumes the request after a redirect screen * [`useListOAuthConsents`](/docs/solid/queries/list-oauth-consents) lists authorized applications * [`useDeleteOAuthConsent`](/docs/solid/mutations/delete-oauth-consent) removes a stored consent # One Tap (/docs/zaidan/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="src/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 Solid client: ```ts title="src/lib/auth-client.ts" import { createAuthClient } from "@better-auth-ui/solid" 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 Solid registry item so the sign-in and sign-up surfaces include the headless prompt slot: ```bash bunx --bun shadcn@latest add https://better-auth-ui.com/r/solid/one-tap.json ``` ### Register the Solid UI plugin [#register-the-solid-ui-plugin] Pass `oneTapPlugin()` to your copied ``. The prompt opens on sign-in by default. ```tsx title="src/components/providers.tsx" import { oneTapPlugin } from "@better-auth-ui/solid/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="src/lib/auth.ts" lastLoginMethod({ customResolveMethod: (context) => context.path === "/one-tap/callback" ? "google" : null }) ``` # Organization (/docs/zaidan/plugins/organization) The organization plugin adds multi-tenant organization management to your Solid/Zaidan 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/zaidan/plugins/api-key)) can attach org-scoped cards * Solid hooks and mutations for organization endpoints such as `useActiveOrganization`, `useListOrganizations`, `useInviteMember`, and `useUpdateMemberRole` ## 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="src/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="src/lib/auth-client.ts" import { createAuthClient } from "better-auth/solid" 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 https://better-auth-ui.com/r/solid/organization.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/organization.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/organization.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/organization.json ``` This drops the following into your codebase: * `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 prefixed organization route shell * `src/components/auth/organization/organization-switcher.tsx`: header dropdown for switching organizations * `src/components/auth/organization/organization-roles.tsx`: dynamic role and permission editor * `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, remove, and leave 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/delete-organization.tsx`: delete danger-zone row * `src/components/auth/organization/leave-organization.tsx`: leave danger-zone row * `src/components/auth/organization/change-organization-logo.tsx`: logo upload control * `src/components/auth/organization/organization-logo.tsx`, `slug-field.tsx`, plus matching loading and empty states ### Register the UI plugin [#register-the-ui-plugin] Pass `organizationPlugin()` to `` so the organizations settings tab, `` shell, and `` can read plugin localization and view paths. ```tsx title="src/components/providers.tsx" import type { QueryClient } from "@tanstack/solid-query" import { useNavigate, useParams } from "@tanstack/solid-router" import type { JSX } from "solid-js" import { Show } from "solid-js" import { AuthProvider } from "@/components/auth/auth-provider" import { organizationPlugin } from "@/lib/auth/organization-plugin" // [!code highlight] import { authClient } from "@/lib/auth-client" export type ProvidersProps = { children?: JSX.Element | (() => JSX.Element) queryClient?: QueryClient } 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 } return ( {props.children} ) } ``` `Show keyed` remounts the provider when the URL slug changes, so plugin state and organization-scoped queries cannot keep using a previous slug. ### 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="src/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] Drop `` into your app shell: typically in the header next to ``. It shows the active organization, lets users switch between organizations, and exposes a "Create organization" entry. ```tsx title="src/components/header.tsx" {2,7} import { OrganizationSwitcher } from "@/components/auth/organization/organization-switcher" import { UserButton } from "@/components/auth/user/user-button" 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. ```tsx title="src/routes/settings/$path.tsx" import { ensureSession } from "@better-auth-ui/core" import { ensureSessionServer } from "@better-auth-ui/core/server" import { viewPaths } from "@better-auth-ui/core" 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" 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 params = Route.useParams() return (
) } ``` `/settings/organizations` now renders ``: the list of organizations the user belongs to plus pending invitations addressed to them.
### Create the organization page [#create-the-organization-page] Mount a dynamic route at `/organization/@{$slug}/$path` that renders `` for the matching tab. The literal `@` stays in the URL while the router captures the unprefixed slug. `` shows the `settings` and `people` tabs for that organization. ```tsx title="src/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/solid-router" import { createIsomorphicFn } from "@tanstack/solid-start" import { getRequestHeaders } from "@tanstack/solid-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 params = Route.useParams() return (
) } ``` `/organization/@acme/settings` and `/organization/@acme/people` now render the organization management UI. Internal links from `` and `` wire up automatically.
Users can now create organizations from `/settings/organizations`, switch between them from the header, and manage members and invitations at prefixed organization URLs. ## Slug-based routes [#slug-based-routes] The Solid/Zaidan organization UI uses slug-based routes for organization pages. This avoids relying on stale session-active organization state when users open multiple organizations or navigate directly to a saved link. This unlocks: * Shareable, bookmarkable per-org URLs that do not depend on session state * Fetching the organization identified by the URL 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 2. Rewrites links from ``, ``, and the `` tabs to include `/$slug/` 3. Swaps the switcher's behavior from `setActive` to `navigate`: clicking an organization takes the user to its slug-prefixed route The example also passes `slugPrefix: "@"`. Solid fields and organization labels display `@acme`, generated links use `/organization/@acme/...`, and the router still passes the raw `acme` slug to Better Auth. ### 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`. ```tsx title="src/components/providers.tsx" import { useNavigate, useParams } from "@tanstack/solid-router" import { Show } from "solid-js" import { AuthProvider } from "@/components/auth/auth-provider" import { organizationPlugin } from "@/lib/auth/organization-plugin" import { authClient } from "@/lib/auth-client" export function Providers(props: ProvidersProps) { const navigate = useNavigate() const params = useParams({ strict: false }) // [!code highlight] const organizationSlug = () => { const slug = params().slug if (typeof slug === "string" && slug.length > 0) return slug return null } return ( {props.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` to `/organization/@{$slug}/$path`. Validate the path segment and gate on the signed-in session as before. ```tsx title="src/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/solid-router" import { createIsomorphicFn } from "@tanstack/solid-start" import { getRequestHeaders } from "@tanstack/solid-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 params = Route.useParams() return (
) } ```
The switcher, organization rows, and tabs automatically include `/@/` once `organizationPlugin({ slug, slugPrefix: "@" })` is wired 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. Clicking the personal account navigates to `auth.basePaths.settings`. To use a custom destination, pass the `setActive` prop. This prop replaces the plugin's default navigation. The callback receives the selected organization, or `null` for the personal account: ```tsx title="src/components/header.tsx" import { useNavigate } from "@tanstack/solid-router" import { OrganizationSwitcher } from "@/components/auth/organization/organization-switcher" export function Header() { const navigate = useNavigate() return ( { navigate({ to: organization ? `/${organization.slug}/dashboard` : "/dashboard" }) }} /> ) } ``` Outside slug-aware routes, the default behavior is `authClient.organization.setActive`. Pass `setActive` to intercept the selection. ## 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({ // Pass a string slug for org routes, null for non-org routes, or omit it to use session fallback. slug: "acme", // 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`. ## Solid Hooks [#solid-hooks] Copied Zaidan components are wired through ``. If you build custom organization UI with the low-level `@better-auth-ui/solid` hooks, pass your configured `authClient` as shown in the linked Solid API docs. ### Queries [#queries] * `useActiveOrganization()`: Full organization for the URL slug when `organizationPlugin({ slug })` is set, or the active session organization when slug is omitted * `useListOrganizations()`: All organizations the signed-in user belongs to * `useListOrganizationMembers()`: Members of the active organization * `useListOrganizationInvitations()`: Invitations for the active organization * `useListUserInvitations()`: Pending invitations addressed to the signed-in user * `useHasPermission()`: 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 outside slug-aware routing * `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] These previews use seeded Storybook fixtures and do not call live Better Auth endpoints. ### `` [#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=/../../examples/start-solid-zaidan-example/src/demos/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/@/`. It renders `settings` and `people` tabs for the organization identified by the raw slug. ```tsx file=/../../examples/start-solid-zaidan-example/src/demos/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/zaidan/plugins/api-key)), then ``. Drop it into a custom layout if you do not want the tabbed shell. ```tsx file=/../../examples/start-solid-zaidan-example/src/demos/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=/../../examples/start-solid-zaidan-example/src/demos/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=/../../examples/start-solid-zaidan-example/src/demos/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=/../../examples/start-solid-zaidan-example/src/demos/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=/../../examples/start-solid-zaidan-example/src/demos/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=/../../examples/start-solid-zaidan-example/src/demos/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=/../../examples/start-solid-zaidan-example/src/demos/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 ``. ### `` [#userinvitations-] Invitations addressed to the signed-in user across every organization, with Accept / Reject actions. Embedded inside ``. ```tsx file=/../../examples/start-solid-zaidan-example/src/demos/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). ## Related APIs [#related-apis] * [Solid organization queries](/docs/solid/queries/active-organization) * [Solid organization mutations](/docs/solid/mutations/create-organization) ## 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="src/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="src/lib/auth.ts" import { organization } from "better-auth/plugins" import { organizationAccess } from "./organization-access" organization({ ac: organizationAccess, dynamicAccessControl: { enabled: true } }) ``` ```ts title="src/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="src/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 Solid 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 { createSignal } from "solid-js" import { TeamSwitcher } from "@/components/auth/organization/team-switcher" export function ProjectTeamFilter(props: { organizationId: string }) { const [teamId, setTeamId] = createSignal(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/zaidan/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 * Solid query and mutation options such as `signInPasskeyOptions`, `listPasskeysOptions`, `addPasskeyOptions`, `updatePasskeyOptions`, and `deletePasskeyOptions` ## 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="src/lib/auth.ts" import { betterAuth } from "better-auth" import { passkey } from "@better-auth/passkey" // [!code highlight] export const auth = betterAuth({ // ... plugins: [ passkey() // [!code highlight] ] }) ``` Configure the WebAuthn origin and trusted origins before production so browser authenticators can complete ceremonies on your real domain. ### 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="src/lib/auth-client.ts" import { createAuthClient } from "better-auth/solid" 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 local `passkeyPlugin()` factory into your project: npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/passkey.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/passkey.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/passkey.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/passkey.json ``` This drops the following into your codebase: * `src/lib/auth/passkey-plugin.ts`: `passkeyPlugin()` factory * `src/components/auth/passkey/passkey-localization.ts`: local localization resolver for copied Passkey UI * `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`: loading placeholder 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 deleting a passkey * `src/components/ui/spinner.tsx`: pending indicator reused by Passkey actions ### Register the plugin [#register-the-plugin] Pass `passkeyPlugin()` to ``. The local plugin registers the auth-flow button and the security settings card. ```tsx title="src/components/providers.tsx" import { AuthProvider } from "@/components/auth/auth-provider" import { passkeyPlugin } from "@/lib/auth/passkey-plugin" // [!code highlight] import { authClient } from "@/lib/auth-client" export function Providers(props: { children?: JSX.Element }) { return ( {props.children} ) } ``` When the plugin is registered, `` can render ``, and `` can render `` through plugin-contributed `securityCards`. ## Runtime prerequisites [#runtime-prerequisites] * Server: `passkey()` from `@better-auth/passkey`. * Client: `passkeyClient()` from `@better-auth/passkey/client`. * Production: set the WebAuthn origin and trusted origins correctly. * Runtime API: passkey options and queries from [Solid runtime APIs](/docs/solid). ## Copied files [#copied-files] * `src/lib/auth/passkey-plugin.ts` * `src/components/auth/passkey/passkey-localization.ts` * `src/components/auth/passkey/passkey-button.tsx` * `src/components/auth/passkey/passkeys.tsx` * `src/components/auth/passkey/passkey.tsx` * `src/components/auth/passkey/passkey-skeleton.tsx` * `src/components/auth/passkey/passkeys-empty.tsx` * `src/components/auth/passkey/add-passkey-dialog.tsx` * `src/components/auth/passkey/delete-passkey-dialog.tsx` * `src/components/ui/spinner.tsx` ## Components [#components] These previews use seeded Storybook fixtures and mocked Passkey methods. They do not call `navigator.credentials` or start real WebAuthn ceremonies. ### `` [#signin-] A "Continue with Passkey" button is automatically rendered on the `` and magic-link views when the plugin is registered. It is hidden on sign-up. **Usage** ```tsx file=/../../examples/start-solid-zaidan-example/src/demos/passkey/passkey-sign-in.tsx import { PasskeyButton } from "@/components/auth/passkey/passkey-button" export function PasskeySignInDemo() { return } ``` **Props** ### `` [#passkeyssettings-] The `` security card is rendered on the security settings page when your layout renders each plugin's `securityCards` and `passkeyPlugin()` is registered. It lists registered passkeys and provides add / delete dialogs. **Usage** ```tsx file=/../../examples/start-solid-zaidan-example/src/demos/passkey/passkeys.tsx import { PasskeysSettings } from "@/components/auth/passkey/passkeys" export function PasskeysDemo() { return } ``` **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 addPasskey = useAddPasskey(auth.authClient) addPasskey.mutate({ 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/solid/plugins/passkey" const auth = useAuth() const passkeyAutoFill = isPasskeyAutoFillEnabled(auth.plugins) usePasskeyAutoFill(auth.authClient) ``` `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] The copied Zaidan Passkey components resolve labels from `passkeyPlugin({ localization })` and fall back to the default Passkey localization. ## Solid runtime APIs [#solid-runtime-apis] Use the low-level Solid runtime helpers when building custom Passkey UI: * [`signInPasskeyOptions`](/docs/solid/mutations/sign-in-passkey) * [`addPasskeyOptions`](/docs/solid/mutations/add-passkey) * [`deletePasskeyOptions`](/docs/solid/mutations/delete-passkey) * [`listPasskeysOptions`](/docs/solid/queries/list-passkeys) # Phone Number (/docs/zaidan/plugins/phone-number) The Solid/Zaidan registry item adds phone verification-code and password sign-in, phone password recovery, and verified phone-number management. 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 [#configure-better-auth] ```ts title="src/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. ### Update the schema and client [#update-the-schema-and-client] Generate or migrate the Better Auth schema so the user model has nullable `phoneNumber` and `phoneNumberVerified` fields. Keep `phoneNumber` unique. ```ts title="src/lib/auth-client.ts" import { createAuthClient } from "@better-auth-ui/solid" import { phoneNumberClient } from "better-auth/client/plugins" export const authClient = createAuthClient({ plugins: [phoneNumberClient()] }) ``` ### Add the registry item [#add-the-registry-item] ```bash bunx --bun shadcn@latest add https://better-auth-ui.com/r/solid/phone-number.json ``` ### Register the UI plugin [#register-the-ui-plugin] ```tsx title="src/components/providers.tsx" import { phoneNumberPlugin } from "@/lib/auth/phone-number-plugin" {props.children} ``` ### Allow the plugin route segments [#allow-the-plugin-route-segments] ```ts title="src/routes/auth/$path.tsx" import { viewPaths } from "@better-auth-ui/core" import { phoneNumberPlugin } from "@/lib/auth/phone-number-plugin" const validAuthPathSegments = 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. Passwordless phone verification bypasses Better Auth 2FA. Phone number and password sign-in still follows the configured second-factor flow. Set Better Auth `signUpOnVerification` to create an account after it verifies an unknown number. If verification requires more user fields, build a custom view. ## Options and localization [#options-and-localization] The copied components use [`useSendPhoneNumberOtp`](/docs/solid/mutations/send-phone-number-otp) and the other Solid phone-number mutation hooks. See the [Better Auth phone-number plugin](https://better-auth.com/docs/plugins/phone-number) for server behavior and advanced options. # Sign In With Ethereum (/docs/zaidan/plugins/siwe) The SIWE plugin adds wallet sign-in backed by Better Auth's `siwe()` plugin, with optional email collection and a security settings card for connected wallets. It contributes: * A "Continue with Ethereum" button on sign-in views * An optional email dialog when the plugin's `email` mode is `"optional"` or `"required"` * A `` security card for connecting, promoting, and removing wallets ## Setup [#setup] ### Configure the Better Auth server [#configure-the-better-auth-server] Add `siwe()` with a secure nonce generator and an ERC-4361 verifier, then apply the plugin schema. ```ts title="src/lib/auth.ts" import { betterAuth } from "better-auth" import { siwe } from "better-auth/plugins" // [!code highlight] import { verifyMessage } from "viem" import { generateSiweNonce } from "viem/siwe" export const auth = betterAuth({ // ... plugins: [ siwe({ // [!code highlight] domain: "app.example.com", getNonce: async () => generateSiweNonce(), verifyMessage: async ({ message, signature, address }) => verifyMessage({ address: address as `0x${string}`, message, signature: signature as `0x${string}` }) }) ] }) ``` ### Add the matching client plugin [#add-the-matching-client-plugin] ```ts title="src/lib/auth-client.ts" import { createAuthClient } from "better-auth/solid" import { siweClient } from "better-auth/client/plugins" // [!code highlight] export const authClient = createAuthClient({ plugins: [siweClient()] // [!code highlight] }) ``` ### Install the UI plugin [#install-the-ui-plugin] npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/siwe.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/siwe.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/siwe.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/siwe.json ``` ### Register the plugin [#register-the-plugin] `connector` is yours to supply. It bridges the plugin to whichever wallet library you use, so BAUI never depends on one. ```tsx title="src/components/providers.tsx" import { AuthProvider } from "@/components/auth/auth-provider" import { siwePlugin } from "@/lib/auth/siwe-plugin" // [!code highlight] import { authClient } from "@/lib/auth-client" export function Providers(props: { children?: JSX.Element }) { return ( {props.children} ) } ``` Pass `walletManager` as well to render the `` security card. Without it the plugin only contributes the sign-in button. `domain` and `uri` must match what the server verifies. A mismatch makes every signature fail verification. ## Runtime prerequisites [#runtime-prerequisites] * Server: `siwe()` from `better-auth/plugins`, with `getNonce` and `verifyMessage`. * Client: `siweClient()` from `better-auth/client/plugins`. * App: a wallet connector, and a wallet manager if you want the settings card. * Runtime API: SIWE query and mutation options from [Solid runtime APIs](/docs/solid). ## Copied files [#copied-files] * `src/lib/auth/siwe-plugin.ts` * `src/components/auth/siwe/sign-in-ethereum-button.tsx` * `src/components/auth/siwe/wallet-accounts.tsx` After install these files are yours. Restyle the wallet rows, change how addresses are shortened, or swap the dialog for your wallet library's own modal. ## Options [#options] ## Localization [#localization] # SSO (/docs/zaidan/plugins/sso) The SSO plugin replaces the standard sign-in form with an email-first flow. The user types an email, BAUI asks Better Auth whether an organization claims that domain, and redirects to the identity provider when one does. When none does, the same view reveals password, magic link, email OTP, and every other registered sign-in method. ## Setup [#setup] ### Configure the Better Auth server [#configure-the-better-auth-server] Install `@better-auth/sso`, add `sso()` to the server, and apply the plugin schema. See the [Better Auth SSO guide](https://www.better-auth.com/docs/plugins/sso) for provider setup. ```ts title="src/lib/auth.ts" import { betterAuth } from "better-auth" import { sso } from "@better-auth/sso" // [!code highlight] export const auth = betterAuth({ // ... plugins: [sso()] // [!code highlight] }) ``` ### Add the matching client plugin [#add-the-matching-client-plugin] ```ts title="src/lib/auth-client.ts" import { ssoClient } from "@better-auth/sso/client" // [!code highlight] import { createAuthClient } from "better-auth/solid" export const authClient = createAuthClient({ plugins: [ssoClient()] // [!code highlight] }) ``` ### Install the UI plugin [#install-the-ui-plugin] npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/sso.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/sso.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/sso.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/sso.json ``` ### Register the plugin [#register-the-plugin] ```tsx title="src/components/providers.tsx" import { AuthProvider } from "@/components/auth/auth-provider" import { ssoPlugin } from "@/lib/auth/sso-plugin" // [!code highlight] import { authClient } from "@/lib/auth-client" export function Providers(props: { children?: JSX.Element }) { return ( {props.children} ) } ``` `ssoPlugin()` overrides the `signIn` view by default. Pass `ssoPlugin({ emailFirst: false })` to keep the standard form and use SSO only through your own UI. ## How discovery resolves [#how-discovery-resolves] * A provider match redirects the browser straight to the identity provider. * A 404 means no organization claims the domain. The email is remembered, the view moves to the fallback step, and the user continues with whatever methods you registered. * Any other failure shows an error and leaves the user on the email step so they can retry. ## Runtime prerequisites [#runtime-prerequisites] * Server: `sso()` from `@better-auth/sso`, with at least one registered provider. * Client: `ssoClient()` from `@better-auth/sso/client`. * Runtime API: `signInSsoOptions` from [Solid runtime APIs](/docs/solid). ## Copied files [#copied-files] * `src/lib/auth/sso-plugin.ts` * `src/lib/auth/use-sign-in-continuation.ts` * `src/lib/auth/two-factor-methods.ts` * `src/components/auth/sso/email-first-sign-in.tsx` * `src/components/auth/provider-button.tsx` * `src/components/auth/provider-buttons.tsx` After install the flow is yours. Reorder the fallback methods, change the copy on the discovery step, or skip the fallback entirely for a tenant that must use SSO. ## Options [#options] ## Localization [#localization] # Theme (/docs/zaidan/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 factory, preference helpers, and copied Theme UI components: npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/theme.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/theme.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/theme.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/theme.json ``` ### Register the UI plugin [#register-the-ui-plugin] The plugin is theme-library agnostic. The recommended Zaidan setup is to use the copied theme helper from `src/lib/theme.ts`: run `themeScript` before Solid hydrates, sync the document preference in your provider shell, then register `themePlugin()` so the plugin's slot components read the persisted theme state. ```tsx title="src/routes/__root.tsx" import { themeScript } from "@/lib/theme" // [!code highlight] export const Route = createRootRoute({ head: () => ({ scripts: [{ children: themeScript }] // [!code highlight] }) }) ``` ```tsx title="src/components/providers.tsx" import { onCleanup, onMount } from "solid-js" import { AuthProvider } from "@/components/auth/auth-provider" import { themePlugin } from "@/lib/auth/theme-plugin" // [!code highlight] import { authClient } from "@/lib/auth-client" import { syncDocumentThemePreference } from "@/lib/theme" // [!code highlight] export function Providers(props: { children?: JSX.Element }) { onMount(() => { const cleanup = syncDocumentThemePreference() // [!code highlight] onCleanup(cleanup) }) return ( {props.children} ) } ``` When the plugin is registered, `` renders the Theme menu item through `userMenuItems`, and account settings render `` through `accountCards`. ### Or pass static theme state [#or-pass-static-theme-state] If you manage the theme with a custom Solid signal or controller, pass both `theme` **and** `setTheme` from that stateful source. The plugin receives the current value from the parent and the slot components call your setter when users change themes. ```tsx title="src/components/providers.tsx" import { createSignal } from "solid-js" import type { ThemeMode } from "@/lib/theme" const [theme, setTheme] = createSignal("system") // [!code highlight] setTheme(nextTheme as ThemeMode), // [!code highlight] themes: ["system", "light", "dark"] // [!code highlight] }) // [!code highlight] ]} > {props.children} ``` Use either the copied helper defaults **or** an explicit `theme`/`setTheme` pair when you need a custom controller. ## Components [#components] These previews use local theme state only. They do not call Better Auth endpoints or require a live session. ### `` [#userbutton-] The Theme menu item is automatically rendered in `` when `themePlugin()` is registered. **Usage** ```tsx file=/../../examples/start-solid-zaidan-example/src/demos/theme/user-button.tsx import { AuthProvider } from "@/components/auth/auth-provider" import { UserButton } from "@/components/auth/user/user-button" import { themePlugin } from "@/lib/auth/theme-plugin" import { authClient } from "@/lib/auth-client" export function ThemeUserButtonDemo() { return ( {() => } ) } ``` ### `` [#appearance-] The `` card is automatically rendered in account settings when `themePlugin()` is registered and the account page renders plugin-contributed `accountCards`. **Usage** ```tsx file=/../../examples/start-solid-zaidan-example/src/demos/theme/appearance.tsx import { Appearance } from "@/components/auth/theme/appearance" export function AppearanceDemo() { return } ``` **Props** ## Options [#options] ## Localization [#localization] The copied Zaidan Theme components resolve labels from `themePlugin({ localization })` and fall back to the default Theme localization. # Two Factor (/docs/zaidan/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 copied `` view at `/auth/two-factor` covering authenticator codes, emailed codes, backup codes, and "trust this device" * A copied `` card in security settings for enrolling, showing the QR code, and managing backup codes * Solid runtime wiring through [`verifyTotpOptions`](/docs/solid/mutations/verify-totp) and the other two-factor mutation factories The copied sign-in form detects `twoFactorRedirect` for you and routes 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 `twoFactor()` to your Better Auth server config. Wire `otpOptions.sendOTP` if you want to offer emailed codes as a second factor: ```ts title="src/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 Solid client plugin [#install-the-matching-solid-client-plugin] ```ts title="src/lib/auth-client.ts" import { twoFactorClient } from "better-auth/client/plugins" // [!code highlight] import { createAuthClient } from "@better-auth-ui/solid" 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. ### Add the copied components [#add-the-copied-components] npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/two-factor.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/two-factor.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/two-factor.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/two-factor.json ``` 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/*.tsx`: the challenge view, settings card, dialogs, and backup-code display The registry also refreshes the copied sign-in form so password and username sign-in both detect the two-factor redirect. ### Register the plugin [#register-the-plugin] ```tsx title="src/components/providers.tsx" import { twoFactorPlugin } from "@/lib/auth/two-factor-plugin" // [!code highlight] {props.children} ``` ### Allow the new view path [#allow-the-new-view-path] ```tsx title="src/routes/auth/$path.tsx" import { viewPaths } from "@better-auth-ui/core" import { createFileRoute, redirect } from "@tanstack/solid-router" import { Auth } from "@/components/auth/auth" import { twoFactorPlugin } from "@/lib/auth/two-factor-plugin" // [!code highlight] const validAuthPathSegments = new Set([ ...Object.values(viewPaths.auth), ...Object.values(twoFactorPlugin().viewPaths.auth) // [!code highlight] ]) export const Route = createFileRoute("/auth/$path")({ beforeLoad({ params: { path } }) { if (!validAuthPathSegments.has(path)) { throw redirect({ to: "/" }) } }, component: AuthPage }) ``` ## 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 signIn = createMutation(() => ({ ...signInEmailOptions(auth.authClient), onSuccess: (data) => { if (isTwoFactorRedirect(data)) { storeTwoFactorMethods(data.twoFactorMethods) auth.navigate({ to: "/auth/two-factor" }) return } auth.navigate({ to: auth.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. # Username (/docs/zaidan/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 * Solid username availability and username sign-in mutations through the Better Auth client * Automatic username field injection into sign-up and user profile forms ## Setup [#setup] The username plugin requires the Better Auth server plugin, the matching client plugin, and the copied Zaidan UI plugin wrapper. ### Install the Better Auth plugin [#install-the-better-auth-plugin] Install the [`better-auth`](https://www.better-auth.com/docs/plugins/username) server plugin and add it to your Better Auth config: ```ts title="src/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="src/lib/auth-client.ts" import { createAuthClient } from "better-auth/solid" 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] Run the shadcn CLI to install the Username UI plugin wrapper and copied Solid components: npm pnpm yarn bun ```bash npx shadcn@latest add https://better-auth-ui.com/r/solid/username.json ``` ```bash pnpm dlx shadcn@latest add https://better-auth-ui.com/r/solid/username.json ``` ```bash yarn dlx shadcn@latest add https://better-auth-ui.com/r/solid/username.json ``` ```bash bun x shadcn@latest add https://better-auth-ui.com/r/solid/username.json ``` Then pass `usernamePlugin()` to ``: ```tsx title="src/components/providers.tsx" import { AuthProvider } from "@/components/auth/auth-provider" import { usernamePlugin } from "@/lib/auth/username-plugin" // [!code highlight] import { authClient } from "@/lib/auth-client" export function Providers(props: { children?: JSX.Element }) { return ( {props.children} ) } ``` ## Components [#components] These previews use mocked auth clients and do not call live Better Auth endpoints. ### `` [#signin-] ### `` [#signup-] ### `` [#userprofile-] ## Options [#options] Use `usernamePrefix` when usernames are displayed with a marker such as `@`. The prefix appears inside copied Solid username fields but is not included in the value sent to Better Auth. ```tsx usernamePlugin({ usernamePrefix: "@", localization: { usernamePlaceholder: "username" } }) ``` ## Localization [#localization]