Hello! Welcome to your next lesson in the "Production-Ready Authentication Patterns" module.
In our previous lesson, we established a robust strategy for account linking, ensuring that each person has a single, unified user identity in your application, regardless of whether they sign in with a password or an OAuth provider. Now that we can reliably answer the question "Who is this user?" (Authentication), it's time to address the next crucial question: "What can this user do?" This is Authorization.
This lesson focuses on implementing Role-Based Access Control (RBAC). Your goal is to implement RBAC by validating user roles within server loaders and beforeLoad functions. We will build a system where you can define roles like admin, moderator, and user, and use those roles to protect entire pages and even fine-tune the data a user can see or the actions they can perform.
Authentication vs. Authorization
Before we dive into code, it's essential to be precise with our terminology. These two concepts are the pillars of application security.
Authentication | TanStack Start React Docs
The official TanStack Start documentation provides a concise and clear distinction between Authentication and Authorization. Let's start here to ground our understanding.
Please read the short section titled 'Authentication vs Authorization'. This will set the stage for everything that follows.
To summarize:
- Authentication is about verifying identity. The work we did with JWTs and OAuth was all about this.
- Authorization is about granting or denying permissions based on that verified identity. This is our focus today.
Step 1: Extending Your Data Model for Roles
To implement RBAC, you first need a place to store a user's role. This typically involves a change to your database schema. You might add a role column to your User table.
For example, using Prisma, your schema might be updated like this:
// Example schema.prisma
model User {
id String @id @default(cuid())
email String @unique
name String?
role String @default("user") // Added 'role' field
// ... other fields
}
With this in place, your server functions that fetch user data (like getCurrentUserFn) should now include this role field. This makes the user's role available throughout your server-side logic.
Step 2: Protecting Routes with beforeLoad
The primary mechanism for protecting routes in TanStack Router is the beforeLoad function. This function acts as a guard, executing on the server before a route's loader runs or its component renders. If the user isn't authorized, beforeLoad can throw a redirect, preventing any further processing of the route.
This diagram illustrates the execution flow, showing that beforeLoad checks are performed sequentially from the outermost layout route to the innermost page route, all before any loader functions are triggered.

Let's see how to implement this. The most common pattern is to create a "layout route" for an entire section of your site (e.g., an admin panel) and apply the protection there. Any route nested inside will automatically inherit this protection.
How to Set Up Role-Based Access Control
The TanStack Router documentation provides excellent, clear examples of creating role-protected routes. We will use this as our guide.
Read the sections 'Extend Authentication Context', 'Update Router Context Types', and 'Create Role-Protected Routes'. Focus on the 'Admin-Only Routes' example. Notice how it defines a layout route (_admin.tsx) that uses beforeLoad to check context.auth.hasRole('admin') and redirects if the check fails.
In TanStack Start, this pattern is often implemented by having the beforeLoad function call a dedicated server function to perform the check. This keeps your authorization logic cleanly separated and reusable.
Watch how Web Dev Cody implements this exact pattern to protect his admin dashboard.
How I Protect My Tanstack Start Applications
This video provides a practical walkthrough of protecting an /admin route in a TanStack Start application. Pay close attention to how the beforeLoad function calls a server function to handle the authorization logic.
Watch the section from 00:36 to 01:40. He demonstrates how a layout route for /admin uses beforeLoad to run an assertIsAdmin server function, which throws a redirect if the user is not an authenticated admin.
Here's what that code might look like, combining the concepts from the docs and the video:
// src/routes/_authenticated/_admin.tsx
// This is a layout route. Any file inside this folder is an admin route.
import { createFileRoute, redirect } from '@tanstack/react-router';
import { assertIsAdmin } from '~/server/auth'; // A server function
export const Route = createFileRoute('/_authenticated/_admin')({
beforeLoad: async ({ location }) => {
try {
// This server function will throw an error if the user is not an admin.
// The error can be a redirect error.
await assertIsAdmin();
} catch (error) {
// If it's a redirect error, re-throw it so the router can handle it.
if (error instanceof Response && error.status === 302) {
throw redirect({
to: '/login', // or '/unauthorized'
search: {
redirect: location.href,
},
});
}
// Handle other potential errors
throw error;
}
},
// ... component for the admin layout (e.g., with a sidebar)
});
// src/server/auth.ts (simplified)
import { createServerFn, redirect } from '@tanstack/react-start';
import { getAuthenticatedUser } from './session'; // Your function to get user from cookie
export const assertIsAdmin = createServerFn('POST', async () => {
const user = await getAuthenticatedUser();
if (!user) {
throw redirect({ to: '/login' });
}
if (user.role !== 'admin') {
// You could redirect to a generic 'unauthorized' page
throw redirect({ to: '/unauthorized' });
}
// If we reach here, the user is an admin. Do nothing.
return true;
});
Test your understanding!
Imagine you need to create a /moderator section. Access should be granted to users with the moderator role and to users with the admin role (since admins can do everything moderators can).
How would you modify the assertIsAdmin server function (let's call it assertIsModeratorOrAdmin) to implement this logic?
Show answer
You would modify the check inside the server function to see if the user's role is in an array of allowed roles.
// src/server/auth.ts
export const assertIsModeratorOrAdmin = createServerFn('POST', async () => {
const user = await getAuthenticatedUser();
if (!user) {
throw redirect({ to: '/login' });
}
const allowedRoles = ['moderator', 'admin'];
if (!allowedRoles.includes(user.role)) {
throw redirect({ to: '/unauthorized' });
}
return true;
});
This is a simple form of role hierarchy, which we can make more sophisticated later.
Step 3: Tailoring Page Content with Loaders
Authorization isn't just about blocking access entirely. Often, you want to show different data on the same page based on a user's role. For example, on a blog's homepage, a regular user might see all published posts, but an admin might see published posts, drafts, and pending posts.
The route loader is the perfect place for this logic. Since the beforeLoad check has already run, you know the user is authenticated. The beforeLoad can even pass the user object down to the loader and component. The loader then uses the user's role to adjust its data fetching query.
Let's return to the video from Web Dev Cody, where he explains exactly this.
How I Protect My Tanstack Start Applications
Now, let's see how roles can be used inside a loader to dynamically change the content a user sees.
Watch the segment from 01:40 to 03:26. He describes a course page where a loader fetches flags like isPremium or isAdmin. This data is then used in the component to decide whether to show the course content or an 'Upgrade to Premium' panel.
Here is a conceptual example of how this would look for our admin/user blog scenario:
// src/routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router';
import { getAuthenticatedUser } from '~/server/session';
import { db } from '~/db'; // Your database client
// Assume a parent route has already run an authentication check in `beforeLoad`
// and passed the user object down in the context.
export const Route = createFileRoute('/posts/')({
loader: async ({ context }) => {
// context.user is available from the parent route's beforeLoad
const user = context.user;
if (user && user.role === 'admin') {
// Admin: fetch all posts, including drafts
const posts = await db.post.findMany();
return { posts };
} else {
// Regular user: fetch only published posts
const posts = await db.post.findMany({ where: { status: 'published' } });
return { posts };
}
},
component: PostsPage,
});
function PostsPage() {
const { posts } = Route.useLoaderData();
// ... render the list of posts
}
Step 4: Securing the Backend with Server Function Middleware
So far, we've protected the UI. A user who isn't an admin can't navigate to /admin. But what if a malicious user bypasses the UI and tries to call a server function directly, like deleteUserFn({ userId: 'some-id' })?
This is why every sensitive server function must be independently secured.
TanStack Start provides a middleware system for server functions. This is analogous to middleware in server frameworks like Express. You can chain functions that run before your main handler, and one of those can be an authorization check.
How I Protect My Tanstack Start Applications
This is a critical piece of the puzzle. Let's watch how to apply middleware to server functions to secure your backend operations.
Watch from 03:16 to 05:50. Cody explains how he organizes all his server functions in one folder for easy security auditing. He then shows how he attaches an adminMiddleware to server functions that should only be accessible by admins. This middleware validates the user's session and role before the actual function logic is executed.
This completes the security loop. The beforeLoad check protects the frontend navigation, and the server function middleware protects the backend API endpoint. You need both for a secure system.
// src/server/middleware.ts
import { getAuthenticatedUser } from './session';
import { middleware } from '@tanstack/start';
// Define a reusable middleware for admin checks
export const adminMiddleware = middleware(async () => {
const user = await getAuthenticatedUser();
if (!user || user.role !== 'admin') {
throw new Error('UNAUTHORIZED'); // Throws a 401 error
}
// You can pass the validated user to the next function in the chain
return { user };
});
// src/server/user-actions.ts
import { createServerFn } from '@tanstack/react-start';
import { adminMiddleware } from './middleware';
// Apply the middleware to the server function
export const deleteUserFn = createServerFn('POST', adminMiddleware, async (userId: string, { user }) => {
// Because of the middleware, we know `user` is a validated admin.
console.log(`Admin ${user.email} is deleting user ${userId}`);
// ...database logic to delete the user
});
Conclusion
In this lesson, we have constructed a comprehensive Role-Based Access Control system for a TanStack Start application. You've learned how to secure your application at multiple layers, from frontend navigation to backend API calls.
Key Takeaways:
- Authorization vs. Authentication: You now understand the clear difference between verifying who a user is and what they are allowed to do.
beforeLoadfor Route Protection: ThebeforeLoadrouter hook is the primary tool for guarding entire routes or layouts, preventing unauthorized users from even attempting to load a page.- Loaders for Content Tailoring: Route loaders can use the user's role, established by
beforeLoad, to fetch and display different data, creating a dynamic experience on the same URL. - Middleware for Backend Security: Securing UI routes is not enough. Sensitive server functions must be protected with middleware to prevent direct, unauthorized API calls.
You've added a critical layer of production-grade security to your application. In our next lesson, we'll explore how to compose multiple authentication and authorization checks in a type-safe and reusable manner, making your security logic even more powerful and maintainable as your application grows in complexity.
Can't find a good explanation? Sign up and we'll make it for you
Sign up