BETTER-AUTH. UI
Integrations

Next.js

This guide covers integrating @daveyplate/better-auth-ui into your Next.js project.

Starter Project

Want to skip the installation? Check out the starter here:

App Router

Follow these steps to set up @daveyplate/better-auth-ui in your Next.js project using the App Router:

AuthUIProvider

The first step is to set up the <AuthUIProvider /> client component with your authClient, wrapping your layout. This is required to provide the context & hooks to your authentication components across your application.

app/providers.tsx
"use client"

import { AuthUIProvider } from "@daveyplate/better-auth-ui"
import Link from "next/link"
import { useRouter } from "next/navigation"
import type { ReactNode } from "react"

import { authClient } from "@/lib/auth-client"

export function Providers({ children }: { children: ReactNode }) {
    const router = useRouter()

    return (
        <AuthUIProvider
            authClient={authClient}
            navigate={router.push}
            replace={router.replace}
            onSessionChange={() => {
                // Clear router cache (protected routes)
                router.refresh()
            }}
            Link={Link}
        >
            {children}
        </AuthUIProvider>
    )
}

Note: Since the Next.js App Router caches routes by default, navigation to protected routes may fail until you perform a router.refresh() to clear the cache. To prevent this issue, you must use router.refresh() in the provided onSessionChange callback. This forces Next.js to clear the router cache and reload middleware-protected content, ensuring subsequent navigations accurately reflect the current auth state.

Once configured, wrap your layout component with the Providers component:

app/layout.tsx
import type { ReactNode } from "react"
import { Providers } from "./providers"

export default function RootLayout({ children }: { children: ReactNode }) {
    return (
        <html lang="en">
            <body>
                <Providers>{children}</Providers>
            </body>
        </html>
    )
}

The <AuthUIProvider /> can be fully customized with plugins, styles, localization and more. For more information and all available props, see the <AuthUIProvider /> component documentation.

Auth Pages

Create a dynamic route segment for authentication views in app/auth/[path]/page.tsx.

app/auth/[path]/page.tsx
import { AuthView } from "@daveyplate/better-auth-ui"
import { authViewPaths } from "@daveyplate/better-auth-ui/server"

export const dynamicParams = false

export function generateStaticParams() {
    return Object.values(authViewPaths).map((path) => ({ path }))
}

export default async function AuthPage({ params }: { params: Promise<{ path: string }> }) {
    const { path } = await params
    
    return (
        <main className="container flex grow flex-col items-center justify-center self-center p-4 md:p-6">
            <AuthView path={path} />
        </main>
    )
}

The newly created dynamic route covers the following paths by default:

  • /auth/sign-in – Sign in via email/password and social providers
  • /auth/sign-up – New account registration
  • /auth/magic-link – Email login without a password
  • /auth/forgot-password – Trigger email to reset forgotten password
  • /auth/two-factor – Two-factor authentication
  • /auth/recover-account – Recover account via backup code
  • /auth/reset-password – Set new password after receiving reset link
  • /auth/sign-out – Log the user out of the application
  • /auth/callback – Internal route to handle Auth callbacks
  • /auth/accept-invitation – Accept an invitation to an organization

Ensure that any links to the authentication process utilize these routes accordingly. All routes will render the <AuthView /> component and automatically handle navigation and authentication flow.

Account Pages

app/account/[path]/page.tsx
import { AccountView } from "@daveyplate/better-auth-ui"
import { accountViewPaths } from "@daveyplate/better-auth-ui/server"

export const dynamicParams = false

export function generateStaticParams() {
    return Object.values(accountViewPaths).map((path) => ({ path }))
}

export default async function AccountPage({ params }: { params: Promise<{ path: string }> }) {
    const { path } = await params

    return (
        <main className="container p-4 md:p-6">
            <AccountView path={path} />
        </main>
    )
}

Organization Pages

app/organization/[path]/page.tsx
import { OrganizationView } from "@daveyplate/better-auth-ui"
import { organizationViewPaths } from "@daveyplate/better-auth-ui/server"

export const dynamicParams = false

export function generateStaticParams() {
    return Object.values(organizationViewPaths).map((path) => ({ path }))
}

export default async function OrganizationPage({ params }: { params: Promise<{ path: string }> }) {
    const { path } = await params

    return (
        <main className="container p-4 md:p-6">
            <OrganizationView path={path} />
        </main>
    )
}

Slug-Based Organization URLs

If you prefer slug-based organization URLs (e.g., /organization/my-org/settings), you'll need to:

  1. Update your AuthUIProvider to use useParams to get the current slug from the URL
  2. Create a nested dynamic route structure: app/organization/[slug]/[path]/page.tsx

First, update your providers to extract the slug from the URL:

app/providers.tsx
"use client"

import { AuthUIProvider } from "@daveyplate/better-auth-ui"
import Link from "next/link"
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 { slug } = useParams<{ slug: string }>()

    return (
        <AuthUIProvider
            authClient={authClient}
            navigate={router.push}
            replace={router.replace}
            onSessionChange={() => {
                router.refresh()
            }}
            organization={{
                pathMode: "slug",
                basePath: "/organization",
                slug
            }}
            Link={Link}
        >
            {children}
        </AuthUIProvider>
    )
}

Then create the page component with the nested route structure:

app/organization/[slug]/[path]/page.tsx
import { OrganizationView } from "@daveyplate/better-auth-ui"
import { organizationViewPaths } from "@daveyplate/better-auth-ui/server"

export function generateStaticParams() {
    return Object.values(organizationViewPaths).map((path) => ({ path }))
}

export default async function OrganizationPage({ params }: { params: Promise<{ path: string }> }) {
    const { path } = await params

    return (
        <main className="container p-4 md:p-6">
            <OrganizationView path={path} />
        </main>
    )
}

Note: Since the slug parameter comes from useParams, it will be undefined when not on an organization page. This is expected behavior—the AuthUIProvider will handle this gracefully and only use the slug when navigating within organization routes.

Pages Router

Follow these steps to set up @daveyplate/better-auth-ui in your Next.js project using the Pages Router:

AuthUIProvider

First set up the <AuthUIProvider /> within your custom App component in _app.tsx.

pages/_app.tsx
import type { AppProps } from "next/app"
import { AuthUIProvider } from "@daveyplate/better-auth-ui"
import { useRouter } from "next/router"
import Link from "next/link"

import { authClient } from "@/lib/auth-client"

export default function App({ Component, pageProps }: AppProps) {
    const router = useRouter()

    return (
        <AuthUIProvider
            authClient={authClient}
            navigate={router.push}
            replace={router.replace}
            Link={Link}
        >
            <Component {...pageProps} />
        </AuthUIProvider>
    )
}

Now the authentication context is available across your entire application.

Auth Pages

Create a page with a dynamic segment in your Pages directory in pages/auth/[authView].tsx

pages/auth/[path].tsx
import { AuthView } from "@daveyplate/better-auth-ui"
import { authViewPaths } from "@daveyplate/better-auth-ui/server"

export default function AuthPage({ path }: { path: string }) {
    return (
        <main className="container mx-auto flex grow flex-col items-center justify-center gap-3 self-center p-4 md:p-6">
            <AuthView path={path} />
        </main>
    )
}

export async function getStaticPaths() {
    return {
        paths: Object.values(authViewPaths).map((path) => ({ params: { path } })),
        fallback: false
    }
}

export async function getStaticProps({ params }: { params: { path: string } }) {
    return { props: { path: params.path } }
}

These routes match the list shown in the App Router section above.

Account Pages

pages/account/[path].tsx
import { AccountView } from "@daveyplate/better-auth-ui"
import { accountViewPaths } from "@daveyplate/better-auth-ui/server"

export default function AccountPage({ path }: { path: string }) {
    return (
        <main className="container mx-auto p-4 md:p-6">
            <AccountView path={path} />
        </main>
    )
}

export async function getStaticPaths() {
    return {
        paths: Object.values(accountViewPaths).map((path) => ({ params: { path } })),
        fallback: false
    }
}

export async function getStaticProps({ params }: { params: { path: string } }) {
    return { props: { path: params.path } }
}

Organization Pages

pages/organization/[path].tsx
import { OrganizationView } from "@daveyplate/better-auth-ui"
import { organizationViewPaths } from "@daveyplate/better-auth-ui/server"

export default function OrganizationPage({ path }: { path: string }) {
    return (
        <main className="container mx-auto p-4 md:p-6">
            <OrganizationView path={path} />
        </main>
    )
}

export async function getStaticPaths() {
    return {
        paths: Object.values(organizationViewPaths).map((path) => ({ params: { path } })),
        fallback: false
    }
}

export async function getStaticProps({ params }: { params: { path: string } }) {
    return { props: { path: params.path } }
}

Slug-Based Organization URLs

If you prefer slug-based organization URLs (e.g., /organization/my-org/settings), you'll need to:

  1. Update your AuthUIProvider in _app.tsx to use useRouter to get the current slug from the URL
  2. Create a nested dynamic route structure: pages/organization/[slug]/[path].tsx

First, update your _app.tsx to extract the slug from the URL:

pages/_app.tsx
import type { AppProps } from "next/app"
import { AuthUIProvider } from "@daveyplate/better-auth-ui"
import { useRouter } from "next/router"
import Link from "next/link"

import { authClient } from "@/lib/auth-client"

export default function App({ Component, pageProps }: AppProps) {
    const router = useRouter()
    const slug = router.query.slug as string | undefined

    return (
        <AuthUIProvider
            authClient={authClient}
            navigate={router.push}
            replace={router.replace}
            organization={{
                pathMode: "slug",
                basePath: "/organization",
                slug
            }}
            Link={Link}
        >
            <Component {...pageProps} />
        </AuthUIProvider>
    )
}

Then create the page component with the nested route structure:

pages/organization/[slug]/[path].tsx
import { OrganizationView } from "@daveyplate/better-auth-ui"
import { organizationViewPaths } from "@daveyplate/better-auth-ui/server"

export default function OrganizationPage({ path }: { path: string }) {
    return (
        <main className="container mx-auto p-4 md:p-6">
            <OrganizationView path={path} />
        </main>
    )
}

export async function getStaticPaths() {
    return {
        paths: Object.values(organizationViewPaths).map((path) => ({ params: { slug: "", path } })),
        fallback: "blocking"
    }
}

export async function getStaticProps({ params }: { params: { path: string } }) {
    return { props: { path: params.path } }
}