BreafIO Team
Product & Engineering
Next.js App Router Architecture: Building Multi-Tenant SaaS with Team Permissions
Introduction
"Transitioning from the Pages router to the Next.js App Router unlocks powerful server-first architectures—especially when structuring complex multi-tenant SaaS platforms with team switching."
Modern B2B SaaS applications require isolating data across different organizations (tenants) while allowing users to belong to multiple teams. Doing this cleanly requires organizing Next.js layout trees, middleware routing, and Role-Based Access Control (RBAC).
This guide covers building a multi-tenant Next.js App Router structure with workspace switching and granular user permissions.
1. Directory Layout & Route Grouping (app/)
Organizing route groups using parenthetical folders (group) allows you to share layouts without altering URL routes:
app/
├── (auth)/ # Public authentication flow (login, register)
│ ├── login/
│ └── layout.tsx
├── (marketing)/ # High-performance static landing pages
│ └── page.tsx
└── (dashboard)/ # Authenticated multi-tenant app
├── [orgSlug]/ # Dynamic tenant context
│ ├── settings/
│ ├── team/
│ └── page.tsx
└── layout.tsx # Shared sidebar & organization switcher layout2. Multi-Tenant Middleware Routing (Subdomain / Path-Based)
Use Next.js Middleware (middleware.ts) to intercept incoming requests and rewrite custom domains or subdomains directly into the dynamic [orgSlug] layout:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
const url = req.nextUrl;
const hostname = req.headers.get('host') || '';
// Extract tenant subdomain (e.g., acme.yourdomain.com -> acme)
const currentHost = hostname.replace(`.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`, '');
if (currentHost !== hostname && currentHost !== 'www') {
// Rewrite internal URL to app/[orgSlug]/...
return NextResponse.rewrite(
new URL(`/${currentHost}${url.pathname}${url.search}`, req.url)
);
}
return NextResponse.next();
}3. Server Components & Role-Based Access Control (RBAC)
With React Server Components (RSC), enforce authorization at the database query layer before sending UI to the browser:
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { redirect } from 'next/navigation';
interface Props {
params: { orgSlug: string };
}
export default async function TeamSettingsPage({ params }: Props) {
const session = await auth();
// 1. Fetch user membership & role for current workspace
const membership = await db.membership.findFirst({
where: {
userId: session?.user?.id,
organization: { slug: params.orgSlug },
},
});
// 2. Reject unauthorized access directly on the server
if (!membership || membership.role !== 'ADMIN') {
redirect(`/${params.orgSlug}/dashboard?error=unauthorized`);
}
return <div>Admin Settings for {params.orgSlug}</div>;
}4. Organization Switcher Pattern
Build a workspace switcher component that queries all organizations the current user belongs to, and stores the active org in a URL segment or cookie:
// lib/organizations.ts
export async function getUserOrganizations(userId: string) {
return db.membership.findMany({
where: { userId },
include: { organization: true },
});
}The switcher component renders in the sidebar layout and navigates to /${orgSlug}/dashboard when the user selects a different workspace.
5. Key Performance Strategies for App Router SaaS
Parallel Routes (@slot): Render multiple interactive dashboard widgets independently without causing waterfall API delays.
Server Actions for Form Submissions: Eliminate client-side API boilerplate by handling team member invitations and settings updates via direct Server Actions.
Optimistic Organization Switching: Pre-fetch team workspace routes using Next.js `` for instant context switching.
Streaming & Suspense: Wrap slow data fetches in React Suspense boundaries to stream partial UI while data loads, keeping the time-to-interactive fast.
6. Tenant Data Isolation Patterns
Row-Level Security (RLS): Enforce tenant isolation at the database level with PostgreSQL RLS policies that filter by organization_id. This is the safest pattern — even if application code has a bug, the database blocks cross-tenant data access.
Middleware-Based Isolation: For simpler setups, extract the orgSlug from the URL and pass it as a filter to every database query. Less secure than RLS but easier to implement.
Shared Schema, Shared Database: The most common multi-tenant pattern for SaaS. All tenants share the same database and tables, with organization_id as the isolation boundary.
Conclusion
The Next.js App Router is purpose-built for multi-tenant SaaS architecture. Route groups organize public and authenticated flows, middleware handles subdomain routing, server components enforce RBAC at the data layer, and parallel routes enable independent dashboard widgets. The combination of App Router + Prisma + PostgreSQL RLS gives you tenant isolation at three levels: URL structure, application code, and database policy. This is the architecture pattern that scales from your first customer to your thousandth.
Ready to Build?
Get started with our production-ready starter kits and ship your project faster.
Browse Starter Kits