Framework
Version

How to Set Up Role-Based Access Control

This guide covers implementing role-based access control (RBAC) and permission-based routing in TanStack Router applications.

Quick Start

Extend your authentication context to include roles and permissions, create role-protected layout routes, and use beforeLoad to check user permissions before rendering routes.


Extend Authentication Context

1. Add Roles to User Type

Update your authentication context to include roles:

tsx
// src/auth.tsx
import React, { createContext, useContext, useState } from 'react'

interface User {
  id: string
  username: string
  email: string
  roles: string[]
  permissions: string[]
}

interface AuthState {
  isAuthenticated: boolean
  user: User | null
  hasRole: (role: string) => boolean
  hasAnyRole: (roles: string[]) => boolean
  hasPermission: (permission: string) => boolean
  hasAnyPermission: (permissions: string[]) => boolean
  login: (username: string, password: string) => Promise<void>
  logout: () => void
}

const AuthContext = createContext<AuthState | undefined>(undefined)

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  const [isAuthenticated, setIsAuthenticated] = useState(false)

  const hasRole = (role: string) => {
    return user?.roles.includes(role) ?? false
  }

  const hasAnyRole = (roles: string[]) => {
    return roles.some((role) => user?.roles.includes(role)) ?? false
  }

  const hasPermission = (permission: string) => {
    return user?.permissions.includes(permission) ?? false
  }

  const hasAnyPermission = (permissions: string[]) => {
    return (
      permissions.some((permission) =>
        user?.permissions.includes(permission),
      ) ?? false
    )
  }

  const login = async (username: string, password: string) => {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    })

    if (response.ok) {
      const userData = await response.json()
      setUser(userData)
      setIsAuthenticated(true)
    } else {
      throw new Error('Authentication failed')
    }
  }

  const logout = () => {
    setUser(null)
    setIsAuthenticated(false)
  }

  return (
    <AuthContext.Provider
      value={{
        isAuthenticated,
        user,
        hasRole,
        hasAnyRole,
        hasPermission,
        hasAnyPermission,
        login,
        logout,
      }}
    >
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}
// src/auth.tsx
import React, { createContext, useContext, useState } from 'react'

interface User {
  id: string
  username: string
  email: string
  roles: string[]
  permissions: string[]
}

interface AuthState {
  isAuthenticated: boolean
  user: User | null
  hasRole: (role: string) => boolean
  hasAnyRole: (roles: string[]) => boolean
  hasPermission: (permission: string) => boolean
  hasAnyPermission: (permissions: string[]) => boolean
  login: (username: string, password: string) => Promise<void>
  logout: () => void
}

const AuthContext = createContext<AuthState | undefined>(undefined)

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null)
  const [isAuthenticated, setIsAuthenticated] = useState(false)

  const hasRole = (role: string) => {
    return user?.roles.includes(role) ?? false
  }

  const hasAnyRole = (roles: string[]) => {
    return roles.some((role) => user?.roles.includes(role)) ?? false
  }

  const hasPermission = (permission: string) => {
    return user?.permissions.includes(permission) ?? false
  }

  const hasAnyPermission = (permissions: string[]) => {
    return (
      permissions.some((permission) =>
        user?.permissions.includes(permission),
      ) ?? false
    )
  }

  const login = async (username: string, password: string) => {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password }),
    })

    if (response.ok) {
      const userData = await response.json()
      setUser(userData)
      setIsAuthenticated(true)
    } else {
      throw new Error('Authentication failed')
    }
  }

  const logout = () => {
    setUser(null)
    setIsAuthenticated(false)
  }

  return (
    <AuthContext.Provider
      value={{
        isAuthenticated,
        user,
        hasRole,
        hasAnyRole,
        hasPermission,
        hasAnyPermission,
        login,
        logout,
      }}
    >
      {children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

2. Update Router Context Types

Update src/routes/__root.tsx:

tsx
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'

interface AuthState {
  isAuthenticated: boolean
  user: {
    id: string
    username: string
    email: string
    roles: string[]
    permissions: string[]
  } | null
  hasRole: (role: string) => boolean
  hasAnyRole: (roles: string[]) => boolean
  hasPermission: (permission: string) => boolean
  hasAnyPermission: (permissions: string[]) => boolean
  login: (username: string, password: string) => Promise<void>
  logout: () => void
}

interface MyRouterContext {
  auth: AuthState
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  component: () => (
    <div>
      <Outlet />
    </div>
  ),
})
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'

interface AuthState {
  isAuthenticated: boolean
  user: {
    id: string
    username: string
    email: string
    roles: string[]
    permissions: string[]
  } | null
  hasRole: (role: string) => boolean
  hasAnyRole: (roles: string[]) => boolean
  hasPermission: (permission: string) => boolean
  hasAnyPermission: (permissions: string[]) => boolean
  login: (username: string, password: string) => Promise<void>
  logout: () => void
}

interface MyRouterContext {
  auth: AuthState
}

export const Route = createRootRouteWithContext<MyRouterContext>()({
  component: () => (
    <div>
      <Outlet />
    </div>
  ),
})

Create Role-Protected Routes

1. Admin-Only Routes

Create src/routes/_authenticated/_admin.tsx:

tsx
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated/_admin')({
  beforeLoad: ({ context, location }) => {
    if (!context.auth.hasRole('admin')) {
      throw redirect({
        to: '/unauthorized',
        search: {
          redirect: location.href,
        },
      })
    }
  },
  component: AdminLayout,
})

function AdminLayout() {
  return (
    <div>
      <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
        <strong>Admin Area:</strong> You have administrative privileges.
      </div>
      <Outlet />
    </div>
  )
}
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated/_admin')({
  beforeLoad: ({ context, location }) => {
    if (!context.auth.hasRole('admin')) {
      throw redirect({
        to: '/unauthorized',
        search: {
          redirect: location.href,
        },
      })
    }
  },
  component: AdminLayout,
})

function AdminLayout() {
  return (
    <div>
      <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
        <strong>Admin Area:</strong> You have administrative privileges.
      </div>
      <Outlet />
    </div>
  )
}

2. Multiple Role Access

Create src/routes/_authenticated/_moderator.tsx:

tsx
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated/_moderator')({
  beforeLoad: ({ context, location }) => {
    const allowedRoles = ['admin', 'moderator']
    if (!context.auth.hasAnyRole(allowedRoles)) {
      throw redirect({
        to: '/unauthorized',
        search: {
          redirect: location.href,
          reason: 'insufficient_role',
        },
      })
    }
  },
  component: ModeratorLayout,
})

function ModeratorLayout() {
  const { auth } = Route.useRouteContext()

  return (
    <div>
      <div className="bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded mb-4">
        <strong>Moderator Area:</strong> Role: {auth.user?.roles.join(', ')}
      </div>
      <Outlet />
    </div>
  )
}
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated/_moderator')({
  beforeLoad: ({ context, location }) => {
    const allowedRoles = ['admin', 'moderator']
    if (!context.auth.hasAnyRole(allowedRoles)) {
      throw redirect({
        to: '/unauthorized',
        search: {
          redirect: location.href,
          reason: 'insufficient_role',
        },
      })
    }
  },
  component: ModeratorLayout,
})

function ModeratorLayout() {
  const { auth } = Route.useRouteContext()

  return (
    <div>
      <div className="bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded mb-4">
        <strong>Moderator Area:</strong> Role: {auth.user?.roles.join(', ')}
      </div>
      <Outlet />
    </div>
  )
}

3. Permission-Based Routes

Create src/routes/_authenticated/_users.tsx:

tsx
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated/_users')({
  beforeLoad: ({ context, location }) => {
    const requiredPermissions = ['users:read', 'users:write']
    if (!context.auth.hasAnyPermission(requiredPermissions)) {
      throw redirect({
        to: '/unauthorized',
        search: {
          redirect: location.href,
          reason: 'insufficient_permissions',
        },
      })
    }
  },
  component: () => <Outlet />,
})
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated/_users')({
  beforeLoad: ({ context, location }) => {
    const requiredPermissions = ['users:read', 'users:write']
    if (!context.auth.hasAnyPermission(requiredPermissions)) {
      throw redirect({
        to: '/unauthorized',
        search: {
          redirect: location.href,
          reason: 'insufficient_permissions',
        },
      })
    }
  },
  component: () => <Outlet />,
})

Create Specific Protected Pages

1. Admin Dashboard

Create src/routes/_authenticated/_admin/dashboard.tsx:

tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated/_admin/dashboard')({
  component: AdminDashboard,
})

function AdminDashboard() {
  const { auth } = Route.useRouteContext()

  return (
    <div className="p-6">
      <h1 className="text-3xl font-bold mb-6">Admin Dashboard</h1>

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        <div className="bg-white p-6 rounded-lg shadow">
          <h2 className="text-xl font-semibold mb-2">User Management</h2>
          <p className="text-gray-600">Manage all users in the system</p>
          <button className="mt-4 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
            View Users
          </button>
        </div>

        <div className="bg-white p-6 rounded-lg shadow">
          <h2 className="text-xl font-semibold mb-2">System Settings</h2>
          <p className="text-gray-600">Configure system-wide settings</p>
          <button className="mt-4 bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
            Open Settings
          </button>
        </div>

        <div className="bg-white p-6 rounded-lg shadow">
          <h2 className="text-xl font-semibold mb-2">Reports</h2>
          <p className="text-gray-600">View system reports and analytics</p>
          <button className="mt-4 bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700">
            View Reports
          </button>
        </div>
      </div>

      <div className="mt-8 bg-gray-100 p-4 rounded">
        <h3 className="font-semibold">Your Info:</h3>
        <p>Username: {auth.user?.username}</p>
        <p>Roles: {auth.user?.roles.join(', ')}</p>
        <p>Permissions: {auth.user?.permissions.join(', ')}</p>
      </div>
    </div>
  )
}
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated/_admin/dashboard')({
  component: AdminDashboard,
})

function AdminDashboard() {
  const { auth } = Route.useRouteContext()

  return (
    <div className="p-6">
      <h1 className="text-3xl font-bold mb-6">Admin Dashboard</h1>

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        <div className="bg-white p-6 rounded-lg shadow">
          <h2 className="text-xl font-semibold mb-2">User Management</h2>
          <p className="text-gray-600">Manage all users in the system</p>
          <button className="mt-4 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
            View Users
          </button>
        </div>

        <div className="bg-white p-6 rounded-lg shadow">
          <h2 className="text-xl font-semibold mb-2">System Settings</h2>
          <p className="text-gray-600">Configure system-wide settings</p>
          <button className="mt-4 bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
            Open Settings
          </button>
        </div>

        <div className="bg-white p-6 rounded-lg shadow">
          <h2 className="text-xl font-semibold mb-2">Reports</h2>
          <p className="text-gray-600">View system reports and analytics</p>
          <button className="mt-4 bg-purple-600 text-white px-4 py-2 rounded hover:bg-purple-700">
            View Reports
          </button>
        </div>
      </div>

      <div className="mt-8 bg-gray-100 p-4 rounded">
        <h3 className="font-semibold">Your Info:</h3>
        <p>Username: {auth.user?.username}</p>
        <p>Roles: {auth.user?.roles.join(', ')}</p>
        <p>Permissions: {auth.user?.permissions.join(', ')}</p>
      </div>
    </div>
  )
}

2. User Management Page

Create src/routes/_authenticated/_users/manage.tsx:

tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated/_users/manage')({
  beforeLoad: ({ context }) => {
    // Additional permission check at the page level
    if (!context.auth.hasPermission('users:write')) {
      throw new Error('You need write permissions to manage users')
    }
  },
  component: UserManagement,
})

function UserManagement() {
  const { auth } = Route.useRouteContext()

  const canEdit = auth.hasPermission('users:write')
  const canDelete = auth.hasPermission('users:delete')

  return (
    <div className="p-6">
      <h1 className="text-3xl font-bold mb-6">User Management</h1>

      <div className="bg-white rounded-lg shadow overflow-hidden">
        <table className="min-w-full">
          <thead className="bg-gray-50">
            <tr>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
                Name
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
                Email
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
                Role
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
                Actions
              </th>
            </tr>
          </thead>
          <tbody className="divide-y divide-gray-200">
            <tr>
              <td className="px-6 py-4 whitespace-nowrap">John Doe</td>
              <td className="px-6 py-4 whitespace-nowrap">john@example.com</td>
              <td className="px-6 py-4 whitespace-nowrap">
                <span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
                  User
                </span>
              </td>
              <td className="px-6 py-4 whitespace-nowrap text-sm">
                {canEdit && (
                  <button className="text-blue-600 hover:text-blue-900 mr-4">
                    Edit
                  </button>
                )}
                {canDelete && (
                  <button className="text-red-600 hover:text-red-900">
                    Delete
                  </button>
                )}
              </td>
            </tr>
          </tbody>
        </table>
      </div>

      <div className="mt-6 p-4 bg-blue-50 rounded">
        <h3 className="font-semibold text-blue-800">Your Permissions:</h3>
        <ul className="text-blue-700 text-sm">
          {auth.user?.permissions.map((permission) => (
            <li key={permission}>✓ {permission}</li>
          ))}
        </ul>
      </div>
    </div>
  )
}
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_authenticated/_users/manage')({
  beforeLoad: ({ context }) => {
    // Additional permission check at the page level
    if (!context.auth.hasPermission('users:write')) {
      throw new Error('You need write permissions to manage users')
    }
  },
  component: UserManagement,
})

function UserManagement() {
  const { auth } = Route.useRouteContext()

  const canEdit = auth.hasPermission('users:write')
  const canDelete = auth.hasPermission('users:delete')

  return (
    <div className="p-6">
      <h1 className="text-3xl font-bold mb-6">User Management</h1>

      <div className="bg-white rounded-lg shadow overflow-hidden">
        <table className="min-w-full">
          <thead className="bg-gray-50">
            <tr>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
                Name
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
                Email
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
                Role
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
                Actions
              </th>
            </tr>
          </thead>
          <tbody className="divide-y divide-gray-200">
            <tr>
              <td className="px-6 py-4 whitespace-nowrap">John Doe</td>
              <td className="px-6 py-4 whitespace-nowrap">john@example.com</td>
              <td className="px-6 py-4 whitespace-nowrap">
                <span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
                  User
                </span>
              </td>
              <td className="px-6 py-4 whitespace-nowrap text-sm">
                {canEdit && (
                  <button className="text-blue-600 hover:text-blue-900 mr-4">
                    Edit
                  </button>
                )}
                {canDelete && (
                  <button className="text-red-600 hover:text-red-900">
                    Delete
                  </button>
                )}
              </td>
            </tr>
          </tbody>
        </table>
      </div>

      <div className="mt-6 p-4 bg-blue-50 rounded">
        <h3 className="font-semibold text-blue-800">Your Permissions:</h3>
        <ul className="text-blue-700 text-sm">
          {auth.user?.permissions.map((permission) => (
            <li key={permission}>✓ {permission}</li>
          ))}
        </ul>
      </div>
    </div>
  )
}

Create Unauthorized Page

Create src/routes/unauthorized.tsx:

tsx
import { createFileRoute, Link } from '@tanstack/react-router'

export const Route = createFileRoute('/unauthorized')({
  validateSearch: (search) => ({
    redirect: (search.redirect as string) || '/dashboard',
    reason: (search.reason as string) || 'insufficient_permissions',
  }),
  component: UnauthorizedPage,
})

function UnauthorizedPage() {
  const { redirect, reason } = Route.useSearch()
  const { auth } = Route.useRouteContext()

  const reasonMessages = {
    insufficient_role: 'You do not have the required role to access this page.',
    insufficient_permissions:
      'You do not have the required permissions to access this page.',
    default: 'You are not authorized to access this page.',
  }

  const message =
    reasonMessages[reason as keyof typeof reasonMessages] ||
    reasonMessages.default

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full bg-white shadow-lg rounded-lg p-8 text-center">
        <div className="mb-6">
          <div className="mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center">
            <svg
              className="w-8 h-8 text-red-600"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
              />
            </svg>
          </div>
        </div>

        <h1 className="text-2xl font-bold text-gray-900 mb-4">Access Denied</h1>
        <p className="text-gray-600 mb-6">{message}</p>

        <div className="mb-6 text-sm text-gray-500">
          <p>
            <strong>Your roles:</strong> {auth.user?.roles.join(', ') || 'None'}
          </p>
          <p>
            <strong>Your permissions:</strong>{' '}
            {auth.user?.permissions.join(', ') || 'None'}
          </p>
        </div>

        <div className="space-y-3">
          <Link
            to="/dashboard"
            className="block w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 transition-colors"
          >
            Go to Dashboard
          </Link>

          <Link
            to={redirect}
            className="block w-full bg-gray-200 text-gray-800 py-2 px-4 rounded hover:bg-gray-300 transition-colors"
          >
            Try Again
          </Link>
        </div>
      </div>
    </div>
  )
}
import { createFileRoute, Link } from '@tanstack/react-router'

export const Route = createFileRoute('/unauthorized')({
  validateSearch: (search) => ({
    redirect: (search.redirect as string) || '/dashboard',
    reason: (search.reason as string) || 'insufficient_permissions',
  }),
  component: UnauthorizedPage,
})

function UnauthorizedPage() {
  const { redirect, reason } = Route.useSearch()
  const { auth } = Route.useRouteContext()

  const reasonMessages = {
    insufficient_role: 'You do not have the required role to access this page.',
    insufficient_permissions:
      'You do not have the required permissions to access this page.',
    default: 'You are not authorized to access this page.',
  }

  const message =
    reasonMessages[reason as keyof typeof reasonMessages] ||
    reasonMessages.default

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full bg-white shadow-lg rounded-lg p-8 text-center">
        <div className="mb-6">
          <div className="mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center">
            <svg
              className="w-8 h-8 text-red-600"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
              />
            </svg>
          </div>
        </div>

        <h1 className="text-2xl font-bold text-gray-900 mb-4">Access Denied</h1>
        <p className="text-gray-600 mb-6">{message}</p>

        <div className="mb-6 text-sm text-gray-500">
          <p>
            <strong>Your roles:</strong> {auth.user?.roles.join(', ') || 'None'}
          </p>
          <p>
            <strong>Your permissions:</strong>{' '}
            {auth.user?.permissions.join(', ') || 'None'}
          </p>
        </div>

        <div className="space-y-3">
          <Link
            to="/dashboard"
            className="block w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 transition-colors"
          >
            Go to Dashboard
          </Link>

          <Link
            to={redirect}
            className="block w-full bg-gray-200 text-gray-800 py-2 px-4 rounded hover:bg-gray-300 transition-colors"
          >
            Try Again
          </Link>
        </div>
      </div>
    </div>
  )
}

Component-Level Permission Checks

1. Conditional Rendering Hook

Create src/hooks/usePermissions.ts:

tsx
import { useRouter } from '@tanstack/react-router'

export function usePermissions() {
  const router = useRouter()
  const auth = router.options.context.auth

  return {
    hasRole: auth.hasRole,
    hasAnyRole: auth.hasAnyRole,
    hasPermission: auth.hasPermission,
    hasAnyPermission: auth.hasAnyPermission,
    user: auth.user,
  }
}
import { useRouter } from '@tanstack/react-router'

export function usePermissions() {
  const router = useRouter()
  const auth = router.options.context.auth

  return {
    hasRole: auth.hasRole,
    hasAnyRole: auth.hasAnyRole,
    hasPermission: auth.hasPermission,
    hasAnyPermission: auth.hasAnyPermission,
    user: auth.user,
  }
}

2. Permission Guard Component

Create src/components/PermissionGuard.tsx:

tsx
interface PermissionGuardProps {
  children: React.ReactNode
  roles?: string[]
  permissions?: string[]
  requireAll?: boolean
  fallback?: React.ReactNode
}

export function PermissionGuard({
  children,
  roles = [],
  permissions = [],
  requireAll = false,
  fallback = null,
}: PermissionGuardProps) {
  const { hasAnyRole, hasAnyPermission, hasRole, hasPermission } =
    usePermissions()

  const hasRequiredRoles =
    roles.length === 0 ||
    (requireAll ? roles.every((role) => hasRole(role)) : hasAnyRole(roles))

  const hasRequiredPermissions =
    permissions.length === 0 ||
    (requireAll
      ? permissions.every((permission) => hasPermission(permission))
      : hasAnyPermission(permissions))

  if (hasRequiredRoles && hasRequiredPermissions) {
    return <>{children}</>
  }

  return <>{fallback}</>
}
interface PermissionGuardProps {
  children: React.ReactNode
  roles?: string[]
  permissions?: string[]
  requireAll?: boolean
  fallback?: React.ReactNode
}

export function PermissionGuard({
  children,
  roles = [],
  permissions = [],
  requireAll = false,
  fallback = null,
}: PermissionGuardProps) {
  const { hasAnyRole, hasAnyPermission, hasRole, hasPermission } =
    usePermissions()

  const hasRequiredRoles =
    roles.length === 0 ||
    (requireAll ? roles.every((role) => hasRole(role)) : hasAnyRole(roles))

  const hasRequiredPermissions =
    permissions.length === 0 ||
    (requireAll
      ? permissions.every((permission) => hasPermission(permission))
      : hasAnyPermission(permissions))

  if (hasRequiredRoles && hasRequiredPermissions) {
    return <>{children}</>
  }

  return <>{fallback}</>
}

3. Using Permission Guards

tsx
import { PermissionGuard } from '../components/PermissionGuard'

function SomeComponent() {
  return (
    <div>
      <h1>Dashboard</h1>

      <PermissionGuard roles={['admin']}>
        <button className="bg-red-600 text-white px-4 py-2 rounded">
          Admin Only Button
        </button>
      </PermissionGuard>

      <PermissionGuard
        permissions={['users:write']}
        fallback={<p className="text-gray-500">You cannot edit users</p>}
      >
        <button className="bg-blue-600 text-white px-4 py-2 rounded">
          Edit Users
        </button>
      </PermissionGuard>

      <PermissionGuard
        roles={['admin', 'moderator']}
        permissions={['content:moderate']}
        requireAll={true}
      >
        <button className="bg-yellow-600 text-white px-4 py-2 rounded">
          Moderate Content (Admin/Mod + Permission)
        </button>
      </PermissionGuard>
    </div>
  )
}
import { PermissionGuard } from '../components/PermissionGuard'

function SomeComponent() {
  return (
    <div>
      <h1>Dashboard</h1>

      <PermissionGuard roles={['admin']}>
        <button className="bg-red-600 text-white px-4 py-2 rounded">
          Admin Only Button
        </button>
      </PermissionGuard>

      <PermissionGuard
        permissions={['users:write']}
        fallback={<p className="text-gray-500">You cannot edit users</p>}
      >
        <button className="bg-blue-600 text-white px-4 py-2 rounded">
          Edit Users
        </button>
      </PermissionGuard>

      <PermissionGuard
        roles={['admin', 'moderator']}
        permissions={['content:moderate']}
        requireAll={true}
      >
        <button className="bg-yellow-600 text-white px-4 py-2 rounded">
          Moderate Content (Admin/Mod + Permission)
        </button>
      </PermissionGuard>
    </div>
  )
}

Advanced Permission Patterns

1. Resource-Based Permissions

tsx
// Check if user can edit a specific resource
function canEditResource(auth: AuthState, resourceId: string, ownerId: string) {
  // Admin can edit anything
  if (auth.hasRole('admin')) return true

  // Owner can edit their own resources
  if (auth.user?.id === ownerId && auth.hasPermission('resource:edit:own'))
    return true

  // Moderators can edit with permission
  if (auth.hasRole('moderator') && auth.hasPermission('resource:edit:any'))
    return true

  return false
}

// Usage in component
function ResourceEditor({ resource }) {
  const { auth } = Route.useRouteContext()

  if (!canEditResource(auth, resource.id, resource.ownerId)) {
    return <div>You cannot edit this resource</div>
  }

  return <EditForm resource={resource} />
}
// Check if user can edit a specific resource
function canEditResource(auth: AuthState, resourceId: string, ownerId: string) {
  // Admin can edit anything
  if (auth.hasRole('admin')) return true

  // Owner can edit their own resources
  if (auth.user?.id === ownerId && auth.hasPermission('resource:edit:own'))
    return true

  // Moderators can edit with permission
  if (auth.hasRole('moderator') && auth.hasPermission('resource:edit:any'))
    return true

  return false
}

// Usage in component
function ResourceEditor({ resource }) {
  const { auth } = Route.useRouteContext()

  if (!canEditResource(auth, resource.id, resource.ownerId)) {
    return <div>You cannot edit this resource</div>
  }

  return <EditForm resource={resource} />
}

2. Time-Based Permissions

tsx
function hasTimeBasedPermission(auth: AuthState, permission: string) {
  const userPermissions = auth.user?.permissions || []
  const hasPermission = userPermissions.includes(permission)

  // Check if permission has time restrictions
  const timeRestricted = userPermissions.find((p) =>
    p.startsWith(`${permission}:time:`),
  )

  if (timeRestricted) {
    const [, , startHour, endHour] = timeRestricted.split(':')
    const currentHour = new Date().getHours()
    return (
      currentHour >= parseInt(startHour) && currentHour <= parseInt(endHour)
    )
  }

  return hasPermission
}
function hasTimeBasedPermission(auth: AuthState, permission: string) {
  const userPermissions = auth.user?.permissions || []
  const hasPermission = userPermissions.includes(permission)

  // Check if permission has time restrictions
  const timeRestricted = userPermissions.find((p) =>
    p.startsWith(`${permission}:time:`),
  )

  if (timeRestricted) {
    const [, , startHour, endHour] = timeRestricted.split(':')
    const currentHour = new Date().getHours()
    return (
      currentHour >= parseInt(startHour) && currentHour <= parseInt(endHour)
    )
  }

  return hasPermission
}

Common Problems

Role/Permission Data Not Loading

Problem: User roles/permissions are undefined in routes.

Solution: Ensure your authentication API returns complete user data:

tsx
const login = async (username: string, password: string) => {
  const response = await fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password }),
  })

  if (response.ok) {
    const userData = await response.json()
    // Ensure userData includes roles and permissions
    console.log('User data:', userData) // Debug log
    setUser(userData)
    setIsAuthenticated(true)
  }
}
const login = async (username: string, password: string) => {
  const response = await fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password }),
  })

  if (response.ok) {
    const userData = await response.json()
    // Ensure userData includes roles and permissions
    console.log('User data:', userData) // Debug log
    setUser(userData)
    setIsAuthenticated(true)
  }
}

Permission Checks Too Restrictive

Problem: Users locked out of areas they should access.

Solution: Use hierarchical permissions and role inheritance:

tsx
const roleHierarchy = {
  admin: ['admin', 'moderator', 'user'],
  moderator: ['moderator', 'user'],
  user: ['user'],
}

const hasRole = (requiredRole: string) => {
  const userRoles = user?.roles || []
  return userRoles.some((userRole) =>
    roleHierarchy[userRole]?.includes(requiredRole),
  )
}
const roleHierarchy = {
  admin: ['admin', 'moderator', 'user'],
  moderator: ['moderator', 'user'],
  user: ['user'],
}

const hasRole = (requiredRole: string) => {
  const userRoles = user?.roles || []
  return userRoles.some((userRole) =>
    roleHierarchy[userRole]?.includes(requiredRole),
  )
}

Performance Issues with Many Permission Checks

Problem: Too many permission checks slowing down renders.

Solution: Memoize permission computations:

tsx
import { useMemo } from 'react'

function usePermissions() {
  const { auth } = Route.useRouteContext()

  const permissions = useMemo(
    () => ({
      canEditUsers: auth.hasPermission('users:write'),
      canDeleteUsers: auth.hasPermission('users:delete'),
      isAdmin: auth.hasRole('admin'),
      isModerator: auth.hasAnyRole(['admin', 'moderator']),
    }),
    [auth.user?.roles, auth.user?.permissions],
  )

  return permissions
}
import { useMemo } from 'react'

function usePermissions() {
  const { auth } = Route.useRouteContext()

  const permissions = useMemo(
    () => ({
      canEditUsers: auth.hasPermission('users:write'),
      canDeleteUsers: auth.hasPermission('users:delete'),
      isAdmin: auth.hasRole('admin'),
      isModerator: auth.hasAnyRole(['admin', 'moderator']),
    }),
    [auth.user?.roles, auth.user?.permissions],
  )

  return permissions
}

Common Next Steps

After setting up RBAC, you might want to:

Our Partners
Clerk
Netlify
Neon
Convex
Sentry
Vercel
Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.

Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.