BTST

Better Auth UI Plugin (Beta)

Beautiful shadcn/ui authentication components for better-auth

Better Auth UI Demo

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, and localization overrides

Installation

Before starting, ensure you have:

  • A Next.js project with @btst/stack already set up (see Installation)
  • better-auth configured (server-side auth)
  • A better-auth client (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-ui

Or with npm/yarn:

npm install @btst/better-auth-ui
# or
yarn add @btst/better-auth-ui

2. Configure the Stack Client

Import and register the auth plugins in your stack-client.tsx file:

lib/stack-client.tsx
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.

app/p/[[...all]]/layout.tsx
"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:

app/globals.css
@import "@btst/better-auth-ui/css";

Available Routes

Once configured, the following routes become available under your configured basePath.

Auth Routes (/p/auth/...)

RouteDescription
/p/auth/sign-inSign in page
/p/auth/sign-upSign up page
/p/auth/forgot-passwordPassword reset request
/p/auth/reset-passwordPassword reset form
/p/auth/magic-linkMagic link landing page
/p/auth/email-otpEmail OTP landing page
/p/auth/two-factorTwo-factor verification
/p/auth/recover-accountBackup code recovery
/p/auth/callbackOAuth callback handler
/p/auth/sign-outSign out page
/p/auth/accept-invitationOrganization invitation acceptance
/p/auth/email-verificationEmail verification page

Account Routes (/p/account/...)

RouteDescription
/p/account/settingsProfile & account settings
/p/account/securityPassword, 2FA, passkeys, sessions
/p/account/api-keysAPI key management (apiKey: true)
/p/account/organizationsUser's organization memberships
/p/account/teamsUser's team memberships (teams: true)

Organization Routes (/p/org/...)

RouteDescription
/p/org/settingsOrganization name, logo, danger zone
/p/org/membersMember management, invitations, roles
/p/org/api-keysOrganization API keys (apiKey: true)
/p/org/teamsTeam 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)

OptionTypeDefaultDescription
authClientAnyAuthClientRequiredBetter Auth client
basePathstring"/auth"Base path for auth routes
baseURLstringFront-end base URL for OAuth callbacks
redirectTostring"/"Redirect URL after login
credentialsboolean | CredentialsOptionstrueEmail/password login
signUpboolean | SignUpOptionstrueSign-up flow
socialSocialOptionsSocial provider config
genericOAuthGenericOAuthOptionsCustom OAuth providers
magicLinkbooleanfalsePasswordless magic link
emailOTPbooleanfalsePasswordless email OTP
passkeybooleanfalseWebAuthn passkeys
oneTapbooleanfalseGoogle One Tap
twoFactor("otp" | "totp")[]Two-factor authentication
multiSessionbooleanfalseMultiple session support
emailVerificationbooleanRequire email verification
changeEmailbooleantrueAllow email changes
nameRequiredbooleantrueName field required on sign-up
apiKeyboolean | { prefix?, metadata? }API key plugin support
gravatarboolean | GravatarOptionsGravatar avatars
avatarboolean | AvatarOptionsAvatar upload
additionalFieldsAdditionalFieldsExtra user fields
captchaCaptchaOptionsCAPTCHA integration
localizationAuthLocalizationOverride all UI strings
viewPathsPartial<AuthViewPaths>Custom route sub-paths
freshAgenumber86400Session freshness in seconds
persistClientbooleanfalseForce session refresh on callback
optimisticbooleanfalseOptimistic user updates
hooksPartial<AuthHooks>Custom data fetching hooks
mutatorsPartial<AuthMutators>Custom mutation handlers
LinkLink<a>Custom link component
navigate(href: string) => voidlocation.hrefNavigation function
replace(href: string) => voidnavigateReplace navigation
toastRenderToastSonnerCustom toast renderer
onSessionChange() => voidSession change callback
onRouteError(name, error, ctx) => voidRoute error callback
pagePropsSee Per-Page PropsPer-page className/classNames/localization

Account Plugin (AccountPluginOverrides)

Extends Partial<AuthPluginOverrides> with:

OptionTypeDefaultDescription
accountboolean | Partial<AccountOptions>{ fields: ["image", "name"] }Account view config
deleteUserboolean | DeleteUserOptionsAccount deletion
teamsboolean | TeamOptionsTeams support
pagePropsSee Per-Page PropsPer-page className/classNames/localization

Organization Plugin (OrganizationPluginOverrides)

Extends Partial<AuthPluginOverrides> with:

OptionTypeDefaultDescription
organizationboolean | OrganizationOptionsOrganization config
teamsboolean | TeamOptionsTeams within organizations
pagePropsSee Per-Page PropsPer-page className/classNames/localization

OrganizationOptions:

OptionTypeDefaultDescription
basePathstring"/organization"Base path for org routes
logoboolean | Partial<OrganizationLogoOptions>Logo upload
customRoles{ role: string; label: string }[][]Extra roles beyond owner/admin/member
apiKeybooleanfalseAPI keys for organizations
pathMode"default" | "slug""default"Route mode
slugstringActive organization slug (when pathMode: "slug")
personalPathstringRedirect path when Personal Account is selected
viewPathsPartial<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)

PropTypeDescription
classNamestringWrapper class
classNamesAuthViewClassNamesFine-grained class overrides
localizationPartial<AuthLocalization>Override specific strings
socialLayout"auto" | "horizontal" | "grid" | "vertical"Social provider button layout
callbackURLstringURL sent to OAuth providers as callback
redirectTostringOverride the post-auth redirect
cardHeaderReactNodeReplace the card header
cardFooterReactNodeReplace the card footer
otpSeparators0 | 1 | 2OTP input separator count

callback and signOut only accept { redirectTo }. acceptInvitation only accepts { className }.

AccountPageProps (account pages)

PropTypeDescription
classNamestringWrapper class
classNames{ base?, cards?, drawer?, sidebar?, card? }Fine-grained class overrides
localizationPartial<AuthLocalization>Override specific strings
hideNavbooleanHide the sidebar/drawer navigation
showTeamsbooleanShow teams tab on the page

OrganizationPageProps (organization pages)

PropTypeDescription
classNamestringWrapper class
classNames{ base?, cards?, drawer?, sidebar?, card? }Fine-grained class overrides
localizationPartial<AuthLocalization>Override specific strings
hideNavbooleanHide the sidebar/drawer navigation
slugstringOverride the active organization slug

Common Recipes

Social Login

auth: {
  ...authConfig,
  social: {
    providers: ["github", "google", "discord"],
  },
}
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: