React Router v7 authorization: A developer's guide for 2026
A complete guide to authorization in React Router v7, from roles and permissions to organization-scoped access and enterprise RBAC.
Authentication answers "who is this user?" Authorization answers "what can this user do?" They require separate mechanisms, and in React Router v7 they touch the same primitives (loaders, actions, and middleware) in ways that are easy to get wrong.
The most common mistake: protecting a route with a loader that checks the session, then forgetting to protect the action that handles the mutation. A loader guard redirects unauthenticated users before the page renders, but it does nothing to stop a direct POST to the action. Actions are HTTP endpoints. Anyone who knows the URL can submit a form or call fetcher.submit() directly, bypassing your loader entirely. Authorization must be enforced inside each action that handles sensitive writes, not just in the loader that renders the page.
This guide covers authorization in React Router v7 framework mode from the ground up: how to model roles and permissions, how to enforce them in loaders and actions, how to build reusable helpers that carry authorization context through your application, and how to handle the patterns that B2B applications require, including organization-scoped roles, resource ownership, and enterprise RBAC.
!!This guide assumes you have authentication already working in your React Router v7 app. If you are starting from scratch, read Building authentication in React Router applications first.!!
Why actions are the security boundary
React Router v7 framework mode gives every route two server entry points: a loader and an action. Loaders run on GET requests and return data. Actions run on POST, PUT, PATCH, and DELETE requests and handle mutations. Both run on the server before any response reaches the browser.
The mental model most developers carry from client-side React Router is that route guards live at the component level: render a redirect if the user is not authenticated. In framework mode, the server handles this, but the same trap exists at a different layer. A layout route's loader can check the session and redirect unauthenticated users. That check runs before the nested route's loader renders. It does not run before a nested route's action.
The layout loader protects the page. The child action is still a standalone HTTP endpoint. To protect it, you need an authorization check inside the action itself.
This is the equivalent of the TanStack Start pattern where beforeLoad guards do not protect server functions. In React Router, layout loaders do not protect nested actions. The principle is the same: route-level checks are for user experience, and handler-level checks are for actual security.
Modeling roles and permissions
Before writing any helpers or guards, you need a model. The two most common approaches are flat roles and permission-based RBAC.
Flat roles
The simplest model: each user has a single role, and roles are checked directly.
Flat roles work well for simple applications. Their weakness is that they do not scale cleanly when you need fine-grained control. An admin who can delete users but not modify billing settings cannot be expressed with a single role hierarchy.
Permission-based RBAC
The more flexible model: permissions are discrete strings representing specific actions, roles are collections of permissions, and users are assigned roles. Name permissions as resource:action pairs so they read clearly in code and scale without ambiguity.
Store the user's role in your database and include it in the session. When you look up the user on each request, you get their role, and from the role you derive their permissions at runtime.
Enforcing authorization in loaders and actions
React Router v7 does not have a built-in middleware system like TanStack Start's createMiddleware. Instead, you compose authorization with helper functions that wrap your loaders and actions.
Helper functions for permission checks
Build a requireAuth helper that reads the session and verifies permissions:
Using helpers in loaders and actions
Now use these helpers in both loaders and actions. This is the critical part: the action gets its own authorization check, independent of whatever the loader does.
Notice: billing:read in the loader, billing:write in the action. They are different permissions because reading billing data and modifying a billing plan are different levels of access.
Protecting layout routes
Use layout routes for role-gated sections of your application. A layout loader redirects users who lack the required role before any nested route renders:
Redirect to /unauthorized rather than /login for permission failures. Redirecting to /login for a logged-in user with the wrong role is confusing and makes it look like their session expired.
Remember: this layout loader protects the nested page from rendering. It does not protect the actions on nested routes. Every action in _admin/users.tsx, _admin/billing.tsx, and _admin/settings.tsx still needs its own permission check.
Conditional UI rendering
Beyond route guards, you will often want to show or hide UI elements based on the user's role. A viewer should not see the "Delete user" button even if clicking it would be blocked server-side.
Pull the user's role from loader data and check permissions in your components:
Pass the role down from useLoaderData in the parent route. The layout loader already has the user, so nested routes can access it through React Router's route hierarchy.
One thing to be clear about: UI permission checks are a user experience concern, not a security control. Hiding buttons does not prevent a malicious user from calling the actions directly. The action's requirePermission call is the security boundary. The UI check is so legitimate users do not see controls they cannot use.
Organization-scoped roles
Most B2B applications do not have a single global role per user. A user might be an admin in one organization and a viewer in another. This pattern, where roles are scoped to an organization, is the standard for SaaS applications.
The data model changes: instead of users.role, you have a memberships table.
Your session now tracks which organization is active:
And your auth helper resolves the membership for the active organization:
Your permission helper then checks against the org-scoped role:
Actions that modify organization data should also validate that the target resource belongs to the active organization. Without this check, an admin in org A could modify data belonging to org B:
Return Not found rather than Forbidden for cross-tenant access attempts. Forbidden confirms the resource exists; Not found does not.
Resource-level permissions
Sometimes role-based permissions are not granular enough. You need to check ownership: can this user access this specific record?
A helper function keeps this readable when ownership checks get complex:
Practical design decisions
A few patterns that will save you from refactoring later:
- Start with permission-based RBAC from the beginning. Flat roles feel simpler, but you will outgrow them the first time a customer asks "can my billing admin see invoices without being able to delete users?" Adding roles later is easy; removing them once users are assigned is painful.
- Name permissions as
resource:actionpairs.billing:write,users:delete,org:settings:read. This reads clearly in code and scales without ambiguity. - Log permission denials. A spike in 403s from a single user is an anomaly worth investigating.
- Throw
Responseobjects, notErrorobjects. React Router treats thrownResponseobjects as expected control flow with proper status codes. Athrow new Response('Forbidden', { status: 403 })will hit your error boundary with the right status. Athrow new Error('Forbidden')will produce a 500. - Return 404 for cross-tenant access attempts, not 403. Returning 403 confirms the resource exists to someone who should not know about it.
Testing authorization
Authorization logic is high-stakes and should be unit tested. The permission functions are pure and easy to test:
Write integration tests that call actions directly without going through layout loaders. This is the test that catches the most common authorization gap:
Test the denied case as explicitly as the allowed case for every sensitive action.
Managed RBAC with WorkOS
Building the permission model above handles most applications. The things that become expensive to build yourself are role management that non-engineers can configure, IdP group syncing (so enterprise customers can manage roles from Okta or Azure AD), and audit trails of every access control change.
WorkOS RBAC handles this. Roles and permissions are configured in the WorkOS dashboard or via the API, and they are embedded directly in the session as JWT claims. The role and permissions claims are available in your loaders and actions without an additional database query.
The WorkOS SDK for React Router is @workos-inc/authkit-react-router. It provides authkitLoader for protecting routes and withAuth for direct session access in loaders and actions.
Protecting routes with authkitLoader
Reading roles and permissions with withAuth
For loaders and actions where you need direct access to the role and permissions:
withAuth returns user, sessionId, organizationId, role, roles, permissions, entitlements, featureFlags, impersonator, and accessToken. The role and permissions fields come directly from the session JWT, so there is no database query to resolve them. When a user switches organizations, the session is refreshed and the claims update to reflect their membership in the new org.
Building a reusable permission helper with WorkOS
You can build the same requirePermission pattern on top of withAuth:
Then your loaders and actions stay clean:
The pattern in your application code stays the same: helper functions in loaders and actions, layout loaders for route-level redirects. What changes is where roles and permissions are managed and how they get into the session.
Multiple roles
WorkOS supports assigning multiple roles to a single organization membership. This avoids creating roles for every combination of permissions (e.g., designer-editor). The roles claim in the session is an array, and permissions is the merged set of all permissions from all assigned roles.
Multiple roles is an environment-level setting and is disabled by default. For most applications, start with single-role assignments for simplicity and predictability, and enable multiple roles when overlapping permission sets become common.
Beyond RBAC: fine-grained authorization
RBAC works well when access decisions can be made from a user's role alone. It gets awkward when you need to ask more specific questions: can this user access this particular project? Can they edit this document but not that one?
These are resource-scoped questions, and they require a different model. Instead of "does the user have the content:write permission," you need to ask "does the user have content:write on resource project-abc."
The naive solution is to keep adding ownership checks in your action handlers. That works up to a point, but it scales poorly as your product adds workspaces, shared resources, and nested hierarchies. WorkOS Fine-Grained Authorization (FGA) provides a relationship-based authorization model that handles these patterns without requiring you to build a custom authorization engine.
Conclusion
Authorization in React Router v7 follows a simple principle: loaders and actions are both security boundaries, and each needs its own checks. The layout loader protects the page. It does not protect the actions on nested routes. Once you internalize this, the rest follows naturally. Helper functions compose cleanly. Layout routes handle role-gated sections. The useLoaderData pattern carries permission state through your component tree without prop drilling.
Build the minimum permission model your application needs today. Name permissions clearly so you can add granularity later. Test that direct action calls are rejected, not just that the UI hides the right buttons.
If your B2B customers will eventually require configurable roles, directory sync, or audit logs, WorkOS RBAC handles that without requiring you to rebuild your authorization model. If you later need resource-scoped access control, WorkOS FGA extends the same foundation incrementally.
Sign up for WorkOS and add RBAC to your React Router application.