Roxabi Boilerplate
Guides

Navigation Guards

How frontend route guards protect pages based on authentication state, permissions, and roles — with a complete decision matrix and route inventory.

Overview

The frontend uses TanStack Router's beforeLoad hook to enforce access control before a page renders. This prevents flashes of forbidden content and provides a smooth redirect experience.

Three guard functions handle all navigation scenarios:

GuardPurposeSource
requireAuth()Redirect unauthenticated users to /loginapps/web/src/lib/routeGuards.ts
requireGuest()Redirect authenticated users away from auth pagesapps/web/src/lib/routeGuards.ts
enforceRoutePermission()Check org-level permissions or system rolesapps/web/src/lib/routePermissions.ts

The backend enforces the same rules via the global AuthGuard (see Auth & Security Architecture). Frontend guards exist for UX — they prevent unnecessary page loads and provide meaningful redirects. The backend remains the source of truth.

This table covers every user-state and page-type combination:

User StatePage TypeExample RoutesOutcome
Not authenticatedPublic/, /legal/*, /design-system, /docs/*Renders normally
Not authenticatedGuest-only/login, /register, /reset-passwordRenders normally
Not authenticatedProtected/dashboard, /settings/*Redirect to /login?redirect=<current_path>
Not authenticatedPermission-gated/admin/*Redirect to /login
AuthenticatedPublic/, /legal/*, /design-system, /docs/*Renders normally
AuthenticatedGuest-only/login, /register, /reset-passwordRedirect to /dashboard
AuthenticatedProtected/dashboard, /settings/*Renders normally
Authenticated + has permissionOrg permission-gated/admin/members, /admin/settingsRenders normally
Authenticated + missing permissionOrg permission-gated/admin/members, /admin/settingsRedirect to /dashboard
Authenticated + superadminSystem role-gated/admin/users, /admin/audit-logs, /admin/organizationsRenders normally
Authenticated + not superadminSystem role-gated/admin/users, /admin/audit-logs, /admin/organizationsRedirect to /admin
Soft-deleted accountAny protected/dashboard, /admin/*Backend returns 401/403, treated as unauthenticated

Guard Functions

requireAuth()

Used in beforeLoad for pages that need an authenticated user. Reads the session from ctx.context.session — populated by the root route's beforeLoad (see Root Context Auth below). If no session exists, redirects to /login with the current path captured as a redirect search parameter.

export const Route = createFileRoute('/dashboard')({
  beforeLoad: requireAuth,
  component: DashboardPage,
})

After login, safeRedirect() validates the captured redirect URL before navigating — blocking protocol-relative URLs (//), URL-encoded traversal (/%2F, /%5C), and cross-origin redirects. Invalid values fall back to /dashboard.

requireGuest()

Used in beforeLoad for auth pages (login, register, reset password). If the user already has a valid session, redirects to /dashboard.

export const Route = createFileRoute('/login')({
  beforeLoad: requireGuest,
  component: LoginPage,
})

enforceRoutePermission()

A generic guard that reads staticData.permission from the route definition. Supports two permission formats:

FormatExampleCheckRedirect on Failure
Permission stringmembers:writeChecks session.permissions array/dashboard
Role prefixrole:superadminChecks session.user.role/admin

Superadmin users automatically pass all org-level permission checks.

export const Route = createFileRoute('/admin/members')({
  staticData: { permission: 'members:write' },
  beforeLoad: enforceRoutePermission,
  component: AdminMembersPage,
})

The staticData.permission field is type-safe via declaration merging on TanStack Router's StaticDataRouteOption interface (defined in apps/web/src/lib/routePermissions.ts).

Client-Side Navigation Flow

Loading diagram...

Complete Route Inventory

Public Pages (no guard)

RouteDescription
/Landing page
/legal/*Legal pages (CGU, privacy, cookies, mentions legales)
/design-systemDesign system showcase
/docs/$Documentation (catch-all)
/talks, /talks/claude-code, /talks/dev-processTalk pages
/magic-link/verifyMagic link verification callback
/magic-link-sentConfirmation page shown after magic link is sent
/verify-emailEmail verification callback
/account-deletedDeletion confirmation
/account-reactivationAccount recovery
/$Catch-all 404

Guest-Only Pages (requireGuest)

RouteRedirect if authenticated
/login/dashboard
/register/dashboard
/reset-password/dashboard
/reset-password/confirm/dashboard

Protected Pages (requireAuth)

RouteDescription
/dashboardMain dashboard
/settingsSettings layout (children inherit auth)
/settings/profileProfile settings (inherits from /settings)
/settings/accountAccount settings (inherits from /settings)
/settings/api-keysAPI key management (inherits from /settings)

Admin — Org-Level (enforceRoutePermission)

RoutePermissionRedirect on Failure
/adminmembers:write/dashboard
/admin/membersmembers:write/dashboard
/admin/settingsmembers:write/dashboard

Admin — System-Level (enforceRoutePermission)

RoutePermissionRedirect on Failure
/admin/usersrole:superadmin/admin
/admin/users/$userIdrole:superadmin/admin
/admin/audit-logsrole:superadmin/admin
/admin/organizationsrole:superadmin/admin
/admin/organizations/$orgIdrole:superadmin/admin
/admin/feature-flagsrole:superadmin/admin
/admin/system-settingsrole:superadmin/admin
/admin/healthrole:superadmin (disabled)/admin

Legacy Redirects

RouteRedirects to
/org/admin
/org/members/admin/members
/org/settings/admin/settings
/changelog/docs/$

Layout Inheritance

TanStack Router's layout routes apply beforeLoad guards to all children. Two layouts use this pattern:

  • /settings — applies requireAuth. All /settings/* routes inherit authentication without declaring their own guard.
  • /admin — applies enforceRoutePermission with members:write. Child routes can declare additional permissions (e.g., role:superadmin for /admin/users), which are checked in addition to the parent guard.

This means a route like /admin/users goes through two guard checks: first the /admin layout guard (members:write), then its own (role:superadmin).

SSR and Edge Cases

Root Context Auth

Guards no longer fetch the session independently. Instead, the root route (__root.tsx) fetches the session once in its beforeLoad via getServerEnrichedSession() — a createServerFn defined in routePermissions.ts:

  • During SSR: the server function executes inline, forwarding cookies via getRequestHeader. No client round-trip occurs.
  • During client-side navigation: the server function runs as an RPC call.

The session is returned from root beforeLoad and merged into TanStack Router's context (MyRouterContext). All child route guards simply read ctx.context.session — zero additional API calls and no SSR environment checks. Auth is enforced identically on SSR and in the browser.

// router.tsx — initial context
createRouter({ context: { session: null } })

// __root.tsx — root beforeLoad
const session = await getServerEnrichedSession()
return { session }

// routeGuards.ts — guard reads from context
export function requireAuth(ctx: BeforeLoadContext) {
  if (!ctx.context.session) throw redirect({ to: '/login', ... })
}

Session Fetch Failures

When getServerEnrichedSession() encounters a network error or the API is unavailable, it catches the error and returns null. Guards treat a null session as unauthenticated and redirect accordingly. There is no retry logic at the guard level.

Post-Login Redirect

When requireAuth() redirects to /login, it captures the original path:

/login?redirect=/admin/members

After successful login, safeRedirect() validates the URL before navigating. Invalid redirect values (cross-origin, protocol-relative, encoded traversal) are silently replaced with /dashboard.

UI-Level Permission Checks

Beyond route guards, individual components use useCanAccess(path) and hasPermission(session, permission) to conditionally render UI elements (navigation links, buttons, menu items). This prevents showing links to pages the user cannot access.

File Reference

FilePurpose
apps/web/src/routes/__root.tsxRoot route — fetches session via getServerEnrichedSession() server function, provides it to all child routes via context
apps/web/src/lib/routeGuards.tsrequireAuth(), requireGuest(), safeRedirect()
apps/web/src/lib/routePermissions.tsenforceRoutePermission(), getServerEnrichedSession(), useCanAccess(), useEnrichedSession()
apps/web/src/lib/permissions.tshasPermission(), hasAllPermissions(), hasAnyPermission()

We use cookies to improve your experience. You can accept all, reject all, or customize your preferences.