
Next.js 15 Middleware: A Complete Guide to RBAC and Multi-Tenant Security
Mastering Next.js 15 Middleware: The Backbone of Multi-Tenant Apps and Role-Based Security
Building a web application is easy, but building a scalable, secure, and professional multi-tenant system is a different beast entirely. If you are working with Next.js 15, Prisma, and Next Auth—much like the systems I develop during my breaks here in Saudi Arabia—you’ll quickly realize that security isn't just about a login page.
In a multi-tenant environment (where one app serves many different companies or users), you cannot afford to let "Tenant A" see "Tenant B’s" data. This is where Middleware becomes your best friend.
In this guide, we will dive deep into why Middleware is the secret sauce for RBAC (Role-Based Access Control) and how to implement it in a full-stack Next.js 15 environment.
Why Middleware is Non-Negotiable for Multi-Tenant Systems
When I first started building my Ads Management System, I faced a challenge: How do I ensure that a "Manager" can see the analytics, but a "Staff Member" can only upload images, all while keeping the "Super Admin" in total control?
You might think, "I'll just check the user's role on every single page." Stop right there. That is a recipe for disaster. If you forget to add that check to just one page, you’ve created a massive security hole.
1. The "Gatekeeper" Philosophy
Middleware acts as a gatekeeper standing at the entrance of your application. Before a request even reaches your page or API route, Middleware inspects the user’s "ID card" (the Session Token). If they don't have the right role, they are turned away instantly.
2. Multi-Tenancy Complexity
In a multi-tenant app, you often have subdomains or specific path segments (e.g., app.com/tenant-1/dashboard). Middleware allows you to:
Extract the tenant ID from the URL.
Verify if the logged-in user actually belongs to that tenant.
Redirect them if they try to "URL-hack" their way into another company's data.
3. Server-Side Performance
Because Middleware runs on the Edge runtime (close to the user), it is incredibly fast. It prevents the server from doing the heavy lifting of rendering a page if the user isn't even allowed to see it.
The Tech Stack: Next.js 15, Prisma, and Next Auth
Before we code, let’s look at our foundation. We are using:
Next.js 15: For the latest App Router features.
Next Auth (Auth.js): To handle JWT sessions and roles.
Prisma: To define our roles in the database.
Step 1: Defining Roles in Prisma
First, we need to make sure our database knows what a "Role" is. In your schema.prisma file:
Code snippet
enum Role {
SUPER_ADMIN
TENANT_ADMIN
USER
}
model User {
id String @id @default(cuid())
email String @unique
role Role @default(USER)
tenantId String?
// ... other fields
}
Step 2: Boosting the Next Auth Session
By default, Next Auth doesn't put the "Role" in the session token. We need to "inject" it using the callbacks. In your auth.ts configuration:
TypeScript
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role;
token.tenantId = user.tenantId;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.role = token.role;
session.user.tenantId = token.tenantId;
}
return session;
},
}
Implementing the Middleware
Now, let's build the logic. Create a file named middleware.ts in your src folder. This is where the magic happens.
The Code Logic
We want to protect two types of routes:
Global Admin Routes: Only
SUPER_ADMINcan enter.Tenant Routes: Only users belonging to that specific
tenantIdcan enter.
TypeScript
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';
import type { NextRequest } from 'next/server';
export async function middleware(req: NextRequest) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
const { pathname } = req.nextUrl;
// 1. If no token and trying to access protected routes
if (!token && (pathname.startsWith('/dashboard') || pathname.startsWith('/admin'))) {
return NextResponse.redirect(new URL('/login', req.url));
}
// 2. Super Admin Protection
if (pathname.startsWith('/admin') && token?.role !== 'SUPER_ADMIN') {
return NextResponse.redirect(new URL('/unauthorized', req.url));
}
// 3. Multi-Tenant Logic: Verify Tenant Access
// Example path: /tenant/[id]/settings
if (pathname.startsWith('/tenant')) {
const segments = pathname.split('/');
const requestedTenantId = segments[2];
if (token?.role !== 'SUPER_ADMIN' && token?.tenantId !== requestedTenantId) {
return NextResponse.redirect(new URL('/dashboard', req.url));
}
}
return NextResponse.next();
}
// Optimization: Only run middleware on these paths
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*', '/tenant/:path*'],
};In a real app, Middleware is your insurance policy. It’s the difference between a side project and a professional SaaS (Software as a Service) that businesses in Dubai or Pakistan would actually pay for.
Common Mistakes to Avoid:
Checking the DB in Middleware: Never do a Prisma fetch inside Middleware if you can avoid it. Middleware should be fast. Use the JWT token to store the user's role and tenant ID so you don't hit the database on every single click.
Infinite Redirect Loops: If your Middleware redirects to
/login, make sure the/loginpage itself is excluded from the middleware matcher. Otherwise, it will redirect to itself forever!Client-Side "Hiding" is not Security: Hiding a button in React using
user.role === 'ADMIN'is good for UI, but it's not security. A smart user can always open the console. Middleware is the only way to truly lock the door.





