Route protection ensures only authorized users can access specific pages in your application. The boilerplate provides multiple layers of protection for different scenarios.
Perfect for: Protecting dashboards, admin panels, subscription features, and role-specific content.
Accessible by everyone (including unauthenticated users)
// No protection needed
export default function PublicPage() {
return <div>Anyone can see this</div>
}Examples: Landing page, login, register, documentation
Requires authentication (any logged-in user)
// Using withAuth HOC
import { withAuth } from '@/components/features/auth/with-auth'
function DashboardPage() {
return <div>Only logged-in users see this</div>
}
export default withAuth(DashboardPage)Examples: Dashboard, account settings, user profile
Requires specific role or permission
import { withAuth } from '@/components/features/auth/with-auth'
function ModeratorPanel() {
return <div>Only moderators and above</div>
}
export default withAuth(ModeratorPanel, {
requiredRole: 'MODERATOR'
})Examples: Content moderation, user management, analytics
Admin/backoffice access only
import { withAuth } from '@/components/features/auth/with-auth'
function AdminDashboard() {
return <div>Admin backoffice</div>
}
export default withAuth(AdminDashboard, {
requiredRole: 'ADMIN',
redirectTo: '/unauthorized'
})Shortcut available: The boilerplate also includes withAuthAdmin() as a convenient shortcut for admin-only pages. It automatically requires ADMIN role and redirects unauthorized users.
Examples: User management, system settings, analytics dashboard
The withAuth HOC has a simple signature based on the actual boilerplate code:
withAuth<P>(WrappedComponent: ComponentType<P & WithAuthProps>, requiredRole?: Roles)// Default protection (USER role)
export default withAuth(MyPage)
// The component receives user props
function MyPage({ user }: WithAuthProps) {
return <div>Hello {user.name}</div>
}Authenticated routes are gated in src/proxy.ts before anything renders. This
matters under Cache Components: those routes stream, and a redirect() fired
mid-render leaves after a 200 has already started, so it can no longer change
the status code.
// src/proxy.ts
import { getSessionCookie } from 'better-auth/cookies'
const AUTHENTICATED_SEGMENTS = ['/account', '/admin', '/chat', '/dashboard', '/team']
export default function middleware(request: NextRequest) {
const locale = localeOf(request.nextUrl.pathname)
if (isAuthenticatedPath(request.nextUrl.pathname, locale) && !getSessionCookie(request)) {
return NextResponse.redirect(new URL(`/${locale}/login`, request.url))
}
// ... i18n routing, theme cookie
}getSessionCookie only checks that a cookie is present — it validates
nothing, and anyone can forge a cookie. This is Better Auth's documented
optimistic check: it saves a useless render, it is not a security boundary.
Real enforcement stays server-side, in withAuth, in the DAL, and in the
service-layer authorization.
Add your own protected segment to AUTHENTICATED_SEGMENTS when you create one.
Never await the session at the top level of a layout: it holds the whole
segment, {children} included, behind the request, and the static shell is lost.
The layout creates the promise and passes it down; consumers unwrap it behind a
<Suspense> boundary.
// src/app/[locale]/(app)/layout.tsx
export default function AppLayout({ children }: { children: React.ReactNode }) {
const userPromise = getCurrentUserDal() // created, never awaited
return (
<AuthProvider userPromise={userPromise}>
<Suspense fallback={<SidebarSkeleton />}>
<AppSidebar />
</Suspense>
{children}
</AuthProvider>
)
}getCurrentUserDal() uses 'use cache: private', the only directive allowed to
read cookies() and headers(). Its result is kept in the browser, never on the
server. Client hooks (useAuth, useOrganization) resolve it with use(), so
they suspend — call them only from a component behind a boundary.
The boilerplate includes a forbidden page for users who don't have sufficient permissions:
// src/app/forbidden.tsx
export default function ForbiddenPage() {
return (
<div className="container mx-auto py-16 text-center">
<h1 className="text-4xl font-bold mb-4">403 - Forbidden</h1>
<p className="text-lg mb-8">You don't have permission to access this resource.</p>
<Link href="/dashboard" className="btn btn-primary">
Go to Dashboard
</Link>
</div>
)
}// src/app/dashboard/page.tsx
import withAuth, { WithAuthProps } from '@/components/features/auth/with-auth'
import { RoleConst } from '@/services/types/domain/auth-types'
function DashboardPage({ user }: WithAuthProps) {
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {user.name}!</p>
</div>
)
}
// Requires USER role (default)
export default withAuth(DashboardPage)// src/app/admin/page.tsx
import withAuth, { WithAuthProps } from '@/components/features/auth/with-auth'
import { RoleConst } from '@/services/types/domain/auth-types'
function AdminPage({ user }: WithAuthProps) {
return (
<div>
<h1>Admin Panel</h1>
<p>Admin user: {user.name}</p>
</div>
)
}
// Requires ADMIN role
export default withAuth(AdminPage, RoleConst.ADMIN)// src/app/admin/users/page.tsx
import { withAuthAdmin, WithAuthProps } from '@/components/features/auth/with-auth'
function UserManagementPage({ user }: WithAuthProps) {
return (
<div>
<h1>User Management</h1>
<p>Admin: {user.name}</p>
</div>
)
}
// Shortcut for ADMIN role requirement
export default withAuthAdmin(UserManagementPage)Pages secured! Your application routes are now protected with flexible, role-based access control that scales with your needs.