Better Auth UI Plugin (Beta)
Beautiful shadcn/ui authentication components for better-auth
The Better Auth UI plugin provides beautiful, plug-and-play authentication UI components built with shadcn/ui for better-auth. This is a fork of the popular better-auth-ui library, adapted for seamless integration with BTST.
Features
- Sign In / Sign Up – Complete authentication flows with email, password, social login, and magic links
- Account Management – User profile settings, security settings, API keys, and team/organization memberships
- Two-Factor Authentication – TOTP and OTP support for enhanced security
- Social Login – GitHub, Google, Discord, and more OAuth providers
- Passkeys – WebAuthn/Passkey authentication support
- Organizations – Team and organization management with invitations and roles
- Email OTP / Magic Link – Passwordless authentication options
- Email Verification – Enforce email verification before access
- Generic OAuth – Bring your own OAuth provider
- Fully Customizable – Built with TailwindCSS and shadcn/ui; per-page
className,classNames, andlocalizationoverrides
Installation
Before starting, ensure you have:
- A Next.js project with
@btst/stackalready set up (see Installation) better-authconfigured (server-side auth)- A
better-authclient (lib/auth-client.ts) set up - A database adapter (e.g., Drizzle with
@btst/adapter-drizzle)
1. Install the Package
pnpm add @btst/better-auth-uiOr with npm/yarn:
npm install @btst/better-auth-ui
# or
yarn add @btst/better-auth-ui2. Configure the Stack Client
Import and register the auth plugins in your stack-client.tsx file:
import { createStackClient } from "@btst/stack/client"
import {
authClientPlugin,
accountClientPlugin,
organizationClientPlugin,
} from "@btst/better-auth-ui/client"
import { QueryClient } from "@tanstack/react-query"
const getBaseURL = () =>
typeof window !== "undefined"
? window.location.origin
: process.env.BASE_URL || "http://localhost:3000"
export function getStackClient(queryClient: QueryClient) {
const baseURL = getBaseURL()
return createStackClient({
plugins: {
// Auth plugin — sign-in, sign-up, forgot-password, magic-link, etc.
auth: authClientPlugin({
siteBaseURL: baseURL,
siteBasePath: "/p", // prefix used in your catch-all route
}),
// Account plugin — settings, security, API keys, teams, organizations
account: accountClientPlugin({
siteBaseURL: baseURL,
siteBasePath: "/p",
}),
// Organization plugin — org settings, members, teams
organization: organizationClientPlugin({
siteBaseURL: baseURL,
siteBasePath: "/p",
}),
// ... other BTST plugins (blog, cms, etc.)
},
})
}3. Configure the StackProvider (Client-Side Layout)
Configure the plugin overrides in your catch-all layout file. The auth overrides are shared across all three plugins using ...authConfig.
"use client"
import { StackProvider } from "@btst/stack/context"
import type {
AuthPluginOverrides,
AccountPluginOverrides,
OrganizationPluginOverrides,
} from "@btst/better-auth-ui/client"
import { authClient } from "@/lib/auth-client"
import Link from "next/link"
import { useRouter } from "next/navigation"
import type { ReactNode } from "react"
type PluginOverrides = {
auth: AuthPluginOverrides
account: AccountPluginOverrides
organization: OrganizationPluginOverrides
}
export default function PagesLayout({ children }: { children: ReactNode }) {
const router = useRouter()
// Shared auth configuration — spread into each plugin override
const authConfig = {
authClient,
navigate: router.push,
replace: router.replace,
onSessionChange: () => router.refresh(),
Link,
}
return (
<StackProvider<PluginOverrides>
basePath="/p"
overrides={{
auth: {
...authConfig,
basePath: "/p/auth", // auth routes prefix
redirectTo: "/p/account/settings", // redirect after login
// social: { providers: ["github", "google"] },
// magicLink: true,
// emailOTP: true,
// passkey: true,
// twoFactor: ["otp", "totp"],
// emailVerification: true,
// credentials: { forgotPassword: true },
},
account: {
...authConfig,
basePath: "/p/account", // account routes prefix
account: {
fields: ["image", "name"], // editable profile fields
},
// deleteUser: true,
// teams: true,
// apiKey: true,
// avatar: {
// upload: async (file) => myUploader(file),
// size: 128,
// extension: "png",
// },
},
organization: {
...authConfig,
basePath: "/p/org",
organization: {
basePath: "/p/org",
// logo: true,
// customRoles: [{ role: "editor", label: "Editor" }],
// apiKey: true,
// pathMode: "slug",
},
// teams: true,
},
}}
>
{children}
</StackProvider>
)
}4. Import Required CSS
Add the better-auth-ui styles to your global stylesheet:
@import "@btst/better-auth-ui/css";Available Routes
Once configured, the following routes become available under your configured basePath.
Auth Routes (/p/auth/...)
| Route | Description |
|---|---|
/p/auth/sign-in | Sign in page |
/p/auth/sign-up | Sign up page |
/p/auth/forgot-password | Password reset request |
/p/auth/reset-password | Password reset form |
/p/auth/magic-link | Magic link landing page |
/p/auth/email-otp | Email OTP landing page |
/p/auth/two-factor | Two-factor verification |
/p/auth/recover-account | Backup code recovery |
/p/auth/callback | OAuth callback handler |
/p/auth/sign-out | Sign out page |
/p/auth/accept-invitation | Organization invitation acceptance |
/p/auth/email-verification | Email verification page |
Account Routes (/p/account/...)
| Route | Description |
|---|---|
/p/account/settings | Profile & account settings |
/p/account/security | Password, 2FA, passkeys, sessions |
/p/account/api-keys | API key management (apiKey: true) |
/p/account/organizations | User's organization memberships |
/p/account/teams | User's team memberships (teams: true) |
Organization Routes (/p/org/...)
| Route | Description |
|---|---|
/p/org/settings | Organization name, logo, danger zone |
/p/org/members | Member management, invitations, roles |
/p/org/api-keys | Organization API keys (apiKey: true) |
/p/org/teams | Team management (teams: true) |
Routes are prefixed with your configured basePath. The examples above use /p as the base path.
The exact sub-paths come from the view paths constants in the library and match the route keys above.
Configuration Options
Auth Plugin (AuthPluginOverrides)
| Option | Type | Default | Description |
|---|---|---|---|
authClient | AnyAuthClient | Required | Better Auth client |
basePath | string | "/auth" | Base path for auth routes |
baseURL | string | — | Front-end base URL for OAuth callbacks |
redirectTo | string | "/" | Redirect URL after login |
credentials | boolean | CredentialsOptions | true | Email/password login |
signUp | boolean | SignUpOptions | true | Sign-up flow |
social | SocialOptions | — | Social provider config |
genericOAuth | GenericOAuthOptions | — | Custom OAuth providers |
magicLink | boolean | false | Passwordless magic link |
emailOTP | boolean | false | Passwordless email OTP |
passkey | boolean | false | WebAuthn passkeys |
oneTap | boolean | false | Google One Tap |
twoFactor | ("otp" | "totp")[] | — | Two-factor authentication |
multiSession | boolean | false | Multiple session support |
emailVerification | boolean | — | Require email verification |
changeEmail | boolean | true | Allow email changes |
nameRequired | boolean | true | Name field required on sign-up |
apiKey | boolean | { prefix?, metadata? } | — | API key plugin support |
gravatar | boolean | GravatarOptions | — | Gravatar avatars |
avatar | boolean | AvatarOptions | — | Avatar upload |
additionalFields | AdditionalFields | — | Extra user fields |
captcha | CaptchaOptions | — | CAPTCHA integration |
localization | AuthLocalization | — | Override all UI strings |
viewPaths | Partial<AuthViewPaths> | — | Custom route sub-paths |
freshAge | number | 86400 | Session freshness in seconds |
persistClient | boolean | false | Force session refresh on callback |
optimistic | boolean | false | Optimistic user updates |
hooks | Partial<AuthHooks> | — | Custom data fetching hooks |
mutators | Partial<AuthMutators> | — | Custom mutation handlers |
Link | Link | <a> | Custom link component |
navigate | (href: string) => void | location.href | Navigation function |
replace | (href: string) => void | navigate | Replace navigation |
toast | RenderToast | Sonner | Custom toast renderer |
onSessionChange | () => void | — | Session change callback |
onRouteError | (name, error, ctx) => void | — | Route error callback |
pageProps | See Per-Page Props | — | Per-page className/classNames/localization |
Account Plugin (AccountPluginOverrides)
Extends Partial<AuthPluginOverrides> with:
| Option | Type | Default | Description |
|---|---|---|---|
account | boolean | Partial<AccountOptions> | { fields: ["image", "name"] } | Account view config |
deleteUser | boolean | DeleteUserOptions | — | Account deletion |
teams | boolean | TeamOptions | — | Teams support |
pageProps | See Per-Page Props | — | Per-page className/classNames/localization |
Organization Plugin (OrganizationPluginOverrides)
Extends Partial<AuthPluginOverrides> with:
| Option | Type | Default | Description |
|---|---|---|---|
organization | boolean | OrganizationOptions | — | Organization config |
teams | boolean | TeamOptions | — | Teams within organizations |
pageProps | See Per-Page Props | — | Per-page className/classNames/localization |
OrganizationOptions:
| Option | Type | Default | Description |
|---|---|---|---|
basePath | string | "/organization" | Base path for org routes |
logo | boolean | Partial<OrganizationLogoOptions> | — | Logo upload |
customRoles | { role: string; label: string }[] | [] | Extra roles beyond owner/admin/member |
apiKey | boolean | false | API keys for organizations |
pathMode | "default" | "slug" | "default" | Route mode |
slug | string | — | Active organization slug (when pathMode: "slug") |
personalPath | string | — | Redirect path when Personal Account is selected |
viewPaths | Partial<OrganizationViewPaths> | — | Custom route sub-paths |
Per-Page Props
You can customize each page individually with className, classNames, localization, and other view-specific props — without replacing the entire component.
These are set via pageProps in the relevant plugin override:
overrides={{
auth: {
...authConfig,
basePath: "/p/auth",
pageProps: {
signIn: {
className: "my-wrapper",
classNames: {
title: "text-3xl font-bold",
description: "text-muted-foreground",
footer: "border-t pt-4",
},
localization: { SIGN_IN: "Log in" }, // only override what you need
socialLayout: "grid",
redirectTo: "/dashboard",
},
signUp: {
cardHeader: <MyCustomHeader />, // replace the card header entirely
callbackURL: "/welcome",
},
callback: {
redirectTo: "/onboarding", // override the post-OAuth redirect
},
signOut: {
redirectTo: "/p/auth/sign-in",
},
},
},
account: {
...authConfig,
basePath: "/p/account",
account: { fields: ["image", "name"] },
pageProps: {
accountSettings: {
className: "max-w-2xl mx-auto",
hideNav: true,
},
accountSecurity: {
localization: { SECURITY: "Privacy & Security" },
},
},
},
organization: {
...authConfig,
basePath: "/p/org",
organization: { basePath: "/p/org" },
pageProps: {
organizationSettings: {
className: "p-8",
classNames: { sidebar: { base: "w-64" } },
},
},
},
}}AuthPageProps (auth pages)
| Prop | Type | Description |
|---|---|---|
className | string | Wrapper class |
classNames | AuthViewClassNames | Fine-grained class overrides |
localization | Partial<AuthLocalization> | Override specific strings |
socialLayout | "auto" | "horizontal" | "grid" | "vertical" | Social provider button layout |
callbackURL | string | URL sent to OAuth providers as callback |
redirectTo | string | Override the post-auth redirect |
cardHeader | ReactNode | Replace the card header |
cardFooter | ReactNode | Replace the card footer |
otpSeparators | 0 | 1 | 2 | OTP input separator count |
callbackandsignOutonly accept{ redirectTo }.acceptInvitationonly accepts{ className }.
AccountPageProps (account pages)
| Prop | Type | Description |
|---|---|---|
className | string | Wrapper class |
classNames | { base?, cards?, drawer?, sidebar?, card? } | Fine-grained class overrides |
localization | Partial<AuthLocalization> | Override specific strings |
hideNav | boolean | Hide the sidebar/drawer navigation |
showTeams | boolean | Show teams tab on the page |
OrganizationPageProps (organization pages)
| Prop | Type | Description |
|---|---|---|
className | string | Wrapper class |
classNames | { base?, cards?, drawer?, sidebar?, card? } | Fine-grained class overrides |
localization | Partial<AuthLocalization> | Override specific strings |
hideNav | boolean | Hide the sidebar/drawer navigation |
slug | string | Override the active organization slug |
Common Recipes
Social Login
auth: {
...authConfig,
social: {
providers: ["github", "google", "discord"],
},
}Passwordless (Magic Link + Email OTP)
auth: {
...authConfig,
magicLink: true,
emailOTP: true,
}Two-Factor Authentication
auth: {
...authConfig,
twoFactor: ["otp", "totp"],
}Passkeys
auth: {
...authConfig,
passkey: true,
}Avatar Upload
account: {
...authConfig,
avatar: {
upload: async (file) => {
const result = await myStorage.upload(file)
return result.url
},
size: 128,
extension: "png",
},
}Account Deletion
account: {
...authConfig,
deleteUser: true,
// or with a confirmation requirement:
// deleteUser: { requirePassword: true },
}Organizations with Logo Upload
organization: {
...authConfig,
basePath: "/p/org",
organization: {
basePath: "/p/org",
logo: {
upload: async (file) => {
const result = await myStorage.upload(file)
return result.url
},
size: 256,
extension: "png",
},
customRoles: [
{ role: "editor", label: "Editor" },
{ role: "viewer", label: "Viewer" },
],
apiKey: true,
},
teams: true,
}API Keys
Enable API key management in account settings:
auth: {
...authConfig,
apiKey: {
prefix: "sk", // key prefix shown in the UI
metadata: {}, // default metadata attached to new keys
},
}Learn More
For comprehensive documentation on all configuration options, customization, and advanced features, visit:
- Better Auth UI Documentation – Official documentation with demos and guides
- GitHub Repository (Fork) – Source code for the BTST fork
- Original Repository – Upstream repository
- better-auth Documentation – Documentation for the underlying auth library
