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.
"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:
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.
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
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
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:
- Update your
AuthUIProviderto useuseParamsto get the current slug from the URL - Create a nested dynamic route structure:
app/organization/[slug]/[path]/page.tsx
First, update your providers to extract the slug from the URL:
"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:
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.
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
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
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
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:
- Update your
AuthUIProviderin_app.tsxto useuseRouterto get the current slug from the URL - Create a nested dynamic route structure:
pages/organization/[slug]/[path].tsx
First, update your _app.tsx to extract the slug from the URL:
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:
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 } }
}