3 min read

How to Create Multi-Tenancy Project with Next.js 15 App Router (Middleware)

Mehdi
Author

Ready to build a enterprise-grade multi-tenant application that can handle thousands of organizations from a single codebase? With Next.js 15's App Router and powerful middleware capabilities, you're about to discover how to create a sophisticated system that rivals industry leaders like Slack, Notion, and Shopify.

The App Router brings revolutionary changes to how we handle multi-tenancy, offering better performance, improved developer experience, and more intuitive patterns for building complex applications. Let's dive into creating a production-ready multi-tenant system that handles everything from subdomain routing to internationalization.

What is Multi-Tenancy in Next.js 15?

Multi-tenancy in Next.js 15 is like running a smart co-working space where each company gets their own customized office environment, but they all share the same building infrastructure. Your application serves multiple organizations (tenants) from a single deployment while maintaining complete data separation and customization.

With Next.js 15's App Router, multi-tenancy becomes more powerful and intuitive. The new architecture provides better support for server components, improved caching mechanisms, and more flexible routing patterns that make tenant isolation both easier and more performant.

Think about how modern SaaS platforms work: company1.yourapp.com shows completely different data and branding than company2.yourapp.com, yet both are powered by the same application. This isn't just about showing different data – it's about creating completely isolated experiences while maintaining operational efficiency.

Next.js 15 App Router vs Pages Router for Multi-Tenancy

The App Router brings significant advantages for multi-tenant applications. Unlike the Pages Router, which required complex workarounds for dynamic routing, the App Router provides native support for nested layouts, server components, and more granular control over rendering strategies.

With App Router, you can create tenant-specific layouts that wrap your entire application, implement server-side tenant detection more efficiently, and leverage React Server Components to reduce client-side JavaScript while maintaining rich interactivity.

Understanding Next.js 15 Middleware

Next.js 15 middleware is your application's traffic controller – it intercepts every request before it reaches your application code, allowing you to implement sophisticated routing logic, authentication checks, and tenant detection at the edge.

The middleware runs in the Edge Runtime, which means it executes closer to your users and with minimal cold start times. This is crucial for multi-tenant applications where every millisecond of latency affects user experience across multiple organizations.

Advanced Middleware Features in Next.js 15

Next.js 15 introduces enhanced middleware capabilities that are perfect for multi-tenancy. You get improved request/response manipulation, better TypeScript support, and more efficient pattern matching for complex routing scenarios.

The new middleware also provides better debugging tools and more predictable execution order, making it easier to implement complex tenant detection logic without worrying about edge cases or performance degradation.

Setting Up Your Next.js 15 Multi-Tenant Project

Let's start building your multi-tenant empire. First, create a new Next.js 15 project with the App Router enabled:

1npx create-next-app@latest my-multitenant-app --typescript --app
2cd my-multitenant-app
3npm install

App Router Directory Structure

Your App Router structure should be designed for multi-tenancy from day one. Here's the recommended organization:

my-multitenant-app/
├── middleware.ts
├── app/
│ ├── [lang]/
│ │ ├── [tenant]/
│ │ │ ├── dashboard/
│ │ │ └── settings/
│ │ ├── main/
│ │ │ ├── login/
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── api/
│ │ └── [tenant]/
│ └── globals.css
├── lib/
│ ├── tenant.ts
│ ├── auth.ts
│ └── i18n/
└── components/
├── tenant-specific/
└── shared/

This structure supports both language-based routing and tenant-specific content while maintaining clean separation between shared and tenant-specific resources.

Essential Dependencies for Multi-Tenancy

Install the packages you'll need for a production-ready multi-tenant application:

1npm install accept-language next-auth prisma @prisma/client
2npm install -D @types/accept-language

These dependencies provide internationalization support, robust authentication, and database management – all essential for enterprise multi-tenancy.

Building Production-Ready Middleware

Here's how to implement sophisticated middleware that handles real-world requirements like the example you provided:

1import acceptLanguage from 'accept-language';
2import { NextRequest, NextResponse } from 'next/server';
3import { cookieName, fallbackLng, languages } from './lib/i18n/settings';
4import { getSubdomain } from './lib/utils/url';
5
6acceptLanguage.languages(languages);
7
8export const config = {
9 matcher: ['/((?!api|_next|favicon.ico|sentry-tunnel|health-check).*)', '/'],
10};
11
12const safeSubdomains = [
13 'www', 'app', 'api', 'main', 'console', 'dashboard', 'admin',
14 'portal', 'account', 'auth', 'login', 'docs', 'help', 'support',
15 'status', 'blog', 'staging', 'dev', 'beta', 'demo', 'preview'
16];
17
18export function middleware(req: NextRequest) {
19 const url = req.nextUrl;
20 const host = req.headers.get('host') || '';
21 const subdomain = getSubdomain(host);
22
23 // Handle root domain redirects
24 if (host === 'yourapp.com') {
25 return NextResponse.redirect(
26 new URL(url.pathname + url.search, `https://www.yourapp.com`)
27 );
28 }
29
30 // Language detection and management
31 let lang = detectLanguage(req);
32
33 // Handle safe subdomains (non-tenant routes)
34 if (safeSubdomains.includes(subdomain)) {
35 return handleSafeDomain(req, lang, subdomain);
36 }
37
38 // Process tenant-specific requests
39 return handleTenantRequest(req, lang, subdomain);
40}
41
42function detectLanguage(req: NextRequest): string {
43 let lang;
44
45 if (req.cookies.has(cookieName)) {
46 lang = acceptLanguage.get(req.cookies.get(cookieName)?.value);
47 }
48
49 if (!lang) {
50 lang = acceptLanguage.get(req.headers.get('Accept-Language'));
51 }
52
53 return lang || fallbackLng;
54}
55
56function handleTenantRequest(req: NextRequest, lang: string, subdomain: string) {
57 const authenticated = req.cookies.has('token');
58 const url = req.nextUrl;
59 const pathParts = url.pathname.split('/').filter(Boolean);
60
61 // Check if language is in path
62 const hasLang = languages.some(loc => pathParts[0] === loc);
63 const currentLang = hasLang ? pathParts[0] : lang;
64 const pathWithoutLang = hasLang
65 ? '/' + pathParts.slice(1).join('/')
66 : url.pathname;
67
68 // Redirect to add language if missing
69 if (!hasLang) {
70 return NextResponse.redirect(
71 new URL(`/${currentLang}${pathWithoutLang}${url.search}`, req.url)
72 );
73 }
74
75 // Handle authentication logic
76 const authResult = handleAuthentication(req, currentLang, pathWithoutLang, authenticated);
77 if (authResult) return authResult;
78
79 // Rewrite to tenant-specific path
80 const rewritePath = subdomain && !safeSubdomains.includes(subdomain)
81 ? `/${currentLang}/${subdomain}${pathWithoutLang}`
82 : `/${currentLang}/main${pathWithoutLang}`;
83
84 return NextResponse.rewrite(new URL(`${rewritePath}${url.search}`, req.url));
85}

Subdomain Detection and Safe Domains

The concept of "safe domains" is crucial for production applications. These are subdomains that serve your main application rather than tenant-specific content. Think www.yourapp.com, api.yourapp.com, or docs.yourapp.com.

1// lib/utils/url.ts
2export function getSubdomain(host: string): string {
3 const parts = host.split('.');
4
5 // Handle localhost development
6 if (host.includes('localhost') || host.includes('127.0.0.1')) {
7 return 'main';
8 }
9
10 // Extract subdomain from production domains
11 if (parts.length >= 3) {
12 return parts[0];
13 }
14
15 return '';
16}
17
18export function isValidTenant(subdomain: string): boolean {
19 // Add your tenant validation logic
20 return subdomain.length > 0 && !safeSubdomains.includes(subdomain);
21}

Language Support with Internationalization

Multi-tenant applications often serve global customers, making internationalization essential. Your middleware should handle language detection seamlessly:

1// lib/i18n/settings.ts
2export const fallbackLng = 'en';
3export const languages = ['en', 'es', 'fr', 'de', 'ja'];
4export const cookieName = 'i18next';
5
6export const getOptions = (lng = fallbackLng) => ({
7 supportedLngs: languages,
8 fallbackLng,
9 lng,
10 fallbackNS: 'common',
11 defaultNS: 'common',
12 ns: ['common', 'dashboard', 'auth']
13});

Authentication Flow Integration

Your middleware should handle authentication flows intelligently, redirecting users based on their authentication status and intended destination:

1function handleAuthentication(
2 req: NextRequest,
3 currentLang: string,
4 pathWithoutLang: string,
5 authenticated: boolean
6) {
7 const callback = req.nextUrl.searchParams.get('callback');
8 const isAuthRoute = pathWithoutLang.includes('/login') ||
9 pathWithoutLang.includes('/forgot-password');
10
11 if (isAuthRoute && authenticated) {
12 const redirectUrl = callback || `/${currentLang}/dashboard`;
13 return NextResponse.redirect(new URL(redirectUrl, req.url));
14 }
15
16 const requiresAuth = pathWithoutLang.startsWith('/dashboard') ||
17 (pathWithoutLang === '/' && !safeSubdomains.includes(subdomain));
18
19 if (requiresAuth && !authenticated) {
20 const loginPath = `/${currentLang}/login`;
21 const redirectUrl = pathWithoutLang === '/'
22 ? loginPath
23 : `${loginPath}?callback=${encodeURIComponent(pathWithoutLang)}`;
24
25 return NextResponse.redirect(new URL(redirectUrl, req.url));
26 }
27
28 return null;
29}

App Router File Structure for Multi-Tenancy

The App Router's file-based routing system is perfect for multi-tenancy. You can create dynamic segments that automatically handle tenant routing:

1// app/[lang]/[tenant]/layout.tsx
2import { getTenantConfig } from '@/lib/tenant';
3import { notFound } from 'next/navigation';
4
5interface TenantLayoutProps {
6 children: React.ReactNode;
7 params: { lang: string; tenant: string };
8}
9
10export default async function TenantLayout({
11 children,
12 params
13}: TenantLayoutProps) {
14 const tenantConfig = await getTenantConfig(params.tenant);
15
16 if (!tenantConfig) {
17 notFound();
18 }
19
20 return (
21 <div
22 className="tenant-layout"
23 style={{
24 '--primary-color': tenantConfig.primaryColor,
25 '--brand-font': tenantConfig.fontFamily
26 } as React.CSSProperties}
27 >
28 <header className="tenant-header">
29 <img src={tenantConfig.logo} alt={`${tenantConfig.name} logo`} />
30 <nav>{/* Tenant-specific navigation */}</nav>
31 </header>
32 <main>{children}</main>
33 </div>
34 );
35}

Dynamic Route Segments in App Router

App Router makes dynamic routing intuitive with its folder-based approach. Each folder in brackets becomes a dynamic segment:

1// app/[lang]/[tenant]/dashboard/page.tsx
2interface DashboardPageProps {
3 params: { lang: string; tenant: string };
4 searchParams: { [key: string]: string | string[] | undefined };
5}
6
7export default async function DashboardPage({
8 params,
9 searchParams
10}: DashboardPageProps) {
11 const { lang, tenant } = params;
12 const tenantData = await getTenantDashboardData(tenant);
13
14 return (
15 <div className="dashboard">
16 <h1>Welcome to {tenantData.name} Dashboard</h1>
17 <TenantMetrics data={tenantData.metrics} />
18 <RecentActivity activities={tenantData.activities} />
19 </div>
20 );
21}
22
23// Generate metadata for each tenant
24export async function generateMetadata({ params }: DashboardPageProps) {
25 const tenantConfig = await getTenantConfig(params.tenant);
26
27 return {
28 title: `${tenantConfig.name} Dashboard`,
29 description: `Manage your ${tenantConfig.name} account and settings`
30 };
31}

Layout Components for Tenant Isolation

Layouts in App Router provide perfect boundaries for tenant-specific styling and configuration:

1// app/[lang]/[tenant]/dashboard/layout.tsx
2export default async function DashboardLayout({
3 children,
4 params
5}: {
6 children: React.ReactNode;
7 params: { lang: string; tenant: string };
8}) {
9 const tenantConfig = await getTenantConfig(params.tenant);
10
11 return (
12 <div className="dashboard-layout">
13 <aside className="sidebar">
14 <TenantNavigation tenant={params.tenant} config={tenantConfig} />
15 </aside>
16 <div className="main-content">
17 {children}
18 </div>
19 </div>
20 );
21}

Server Components vs Client Components

App Router's server components are game-changers for multi-tenancy. You can fetch tenant data on the server, reducing client-side JavaScript and improving performance:

1// Server Component (default)
2async function TenantDashboard({ tenant }: { tenant: string }) {
3 const data = await fetchTenantData(tenant); // Runs on server
4
5 return (
6 <div>
7 <h2>{data.name} Overview</h2>
8 <TenantMetrics data={data.metrics} />
9 <ClientInteractiveChart data={data.chartData} />
10 </div>
11 );
12}
13
14// Client Component for interactivity
15'use client';
16function ClientInteractiveChart({ data }: { data: ChartData }) {
17 const [selectedPeriod, setSelectedPeriod] = useState('month');
18
19 return (
20 <div>
21 <PeriodSelector value={selectedPeriod} onChange={setSelectedPeriod} />
22 <Chart data={data} period={selectedPeriod} />
23 </div>
24 );
25}

Database Architecture for App Router

App Router's server actions make database operations more straightforward while maintaining tenant isolation:

1// lib/db/tenant.ts
2import { prisma } from './prisma';
3
4export async function getTenantDatabase(tenantId: string) {
5 return {
6 user: {
7 findMany: (args: any) =>
8 prisma.user.findMany({
9 ...args,
10 where: { ...args.where, tenantId }
11 }),
12 create: (data: any) =>
13 prisma.user.create({
14 data: { ...data, tenantId }
15 })
16 },
17 // Other models with tenant filtering
18 };
19}

Server Actions with Tenant Context

Server Actions in App Router provide a clean way to handle form submissions and mutations with tenant context:

1// app/[lang]/[tenant]/settings/actions.ts
2'use server';
3
4import { revalidatePath } from 'next/cache';
5import { redirect } from 'next/navigation';
6
7export async function updateTenantSettings(
8 tenant: string,
9 formData: FormData
10) {
11 const name = formData.get('name') as string;
12 const primaryColor = formData.get('primaryColor') as string;
13
14 await prisma.tenant.update({
15 where: { slug: tenant },
16 data: { name, primaryColor }
17 });
18
19 revalidatePath(`/${tenant}/settings`);
20 redirect(`/${tenant}/settings?updated=true`);
21}

API Routes in App Router

App Router's API routes support dynamic segments for tenant-specific endpoints:

1// app/api/[tenant]/users/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { validateTenantAccess } from '@/lib/auth';
4
5export async function GET(
6 request: NextRequest,
7 { params }: { params: { tenant: string } }
8) {
9 const { tenant } = params;
10
11 // Validate tenant access
12 const isValid = await validateTenantAccess(request, tenant);
13 if (!isValid) {
14 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
15 }
16
17 const users = await getTenantUsers(tenant);
18 return NextResponse.json(users);
19}
20
21export async function POST(
22 request: NextRequest,
23 { params }: { params: { tenant: string } }
24) {
25 const { tenant } = params;
26 const body = await request.json();
27
28 const newUser = await createTenantUser(tenant, body);
29 return NextResponse.json(newUser, { status: 201 });
30}

Security and Authentication Patterns

Security in multi-tenant applications requires layered protection. Every request must validate both user authentication and tenant authorization:

1// lib/auth.ts
2import { cookies } from 'next/headers';
3import { jwtVerify } from 'jose';
4
5export async function validateTenantAccess(
6 request: NextRequest,
7 tenantId: string
8): Promise<boolean> {
9 const token = request.cookies.get('token')?.value;
10
11 if (!token) return false;
12
13 try {
14 const { payload } = await jwtVerify(
15 token,
16 new TextEncoder().encode(process.env.JWT_SECRET!)
17 );
18
19 const userTenants = await getUserTenants(payload.userId as string);
20 return userTenants.includes(tenantId);
21 } catch {
22 return false;
23 }
24}

Tenant-Based Access Control

Implement granular access control that considers both user roles and tenant boundaries:

1// lib/permissions.ts
2export type Permission = 'read' | 'write' | 'admin';
3export type Resource = 'users' | 'settings' | 'billing';
4
5export async function hasPermission(
6 userId: string,
7 tenantId: string,
8 resource: Resource,
9 permission: Permission
10): Promise<boolean> {
11 const userRole = await getUserRoleInTenant(userId, tenantId);
12 const permissions = rolePermissions[userRole] || [];
13
14 return permissions.some(p =>
15 p.resource === resource && p.permissions.includes(permission)
16 );
17}

Cookie and Session Management

Handle authentication cookies securely across subdomains and tenants:

1// lib/session.ts
2export function setAuthCookie(token: string, domain: string) {
3 cookies().set('token', token, {
4 httpOnly: true,
5 secure: process.env.NODE_ENV === 'production',
6 sameSite: 'lax',
7 domain: `.${getRootDomain(domain)}`, // Allows subdomain access
8 maxAge: 7 * 24 * 60 * 60 // 7 days
9 });
10}
11
12function getRootDomain(host: string): string {
13 const parts = host.split('.');
14 return parts.slice(-2).join('.');
15}

Performance Optimization in Next.js 15

App Router brings powerful caching mechanisms that are perfect for multi-tenant applications. You can cache tenant-specific data while maintaining isolation:

1import { cache } from 'react';
2
3export const getTenantConfig = cache(async (tenantId: string) => {
4 console.log(`Fetching config for tenant: ${tenantId}`);
5
6 return await prisma.tenant.findUnique({
7 where: { slug: tenantId },
8 include: { settings: true }
9 });
10});

Caching Strategies with App Router

Implement intelligent caching that respects tenant boundaries:

1// app/[lang]/[tenant]/dashboard/page.tsx
2import { unstable_cache } from 'next/cache';
3
4const getCachedTenantData = unstable_cache(
5 async (tenantId: string) => {
6 return await fetchTenantData(tenantId);
7 },
8 ['tenant-data'],
9 {
10 tags: [`tenant-${tenantId}`],
11 revalidate: 3600 // 1 hour
12 }
13);
14
15export default async function DashboardPage({ params }: Props) {
16 const data = await getCachedTenantData(params.tenant);
17
18 return <TenantDashboard data={data} />;
19}

Deployment and Production Setup

For production deployment, consider your subdomain strategy and DNS configuration. You'll need wildcard subdomains (*.yourapp.com) pointing to your application:

1# DNS Configuration (example)
2*.yourapp.com CNAME yourapp.vercel.app
3*.yourapp.io CNAME yourapp.vercel.app

Environment variables for production should include tenant-specific configurations:

1DATABASE_URL=your_connection_string
2JWT_SECRET=your_jwt_secret
3ALLOWED_DOMAINS=yourapp.com,yourapp.io
4DEFAULT_LANGUAGE=en
5REDIS_URL=your_redis_connection

Common Challenges and Solutions

The most frequent challenge is maintaining performance as you scale tenants. Monitor database query performance and implement tenant-aware query optimization. Use connection pooling and consider read replicas for high-traffic tenants.

Another common issue is subdomain routing in development. Use tools like ngrok or configure local DNS to test subdomain functionality during development.

Cross-tenant data leakage is the biggest security risk. Always validate tenant context in your API routes, database queries, and authentication checks. Never trust client-side tenant identification for security decisions.

Conclusion

Building multi-tenant applications with Next.js 15's App Router opens up incredible possibilities for scalable SaaS products. You've learned how to implement sophisticated middleware that handles subdomain routing, internationalization, and authentication flows – all while maintaining clean separation between tenants.

The App Router's server components, improved caching, and intuitive routing patterns make multi-tenancy more approachable than ever. With the foundation you've built here, you're equipped to create applications that can scale from a handful of tenants to thousands, all while maintaining excellent performance and security.

Remember that multi-tenancy is an evolving architecture. As your application grows, you'll need to continuously optimize performance, enhance security measures, and refine your tenant management strategies. But with Next.js 15's powerful features as your foundation, you're well-prepared for whatever scaling challenges come your way.

The middleware example you've seen represents real-world complexity – handling multiple domains, safe subdomains, language routing, and authentication flows. This isn't just theoretical code; it's production-ready architecture that can power serious SaaS businesses.

Share this article