Subdomain routing in Next.js can make your multi-tenant SaaS feel like a single, cohesive application, even when serving entirely different customers from the same codebase.

Let’s watch a request flow. A user hits customer-a.your-app.com. The request lands on your Next.js server. Your middleware, running before any page is rendered, inspects the incoming Host header. It sees customer-a.your-app.com. Based on this, it modifies the request object, adding a tenantId property (e.g., req.tenantId = 'customer-a'). This tenantId is then available to any page or API route via context.req.tenantId (in getServerSideProps) or cookies (if you’ve set it there). Your page logic then uses this tenantId to fetch data specific to customer-a from your database.

Here’s how you might set this up:

First, you need a way to map subdomains to your internal tenant identifiers. A simple approach is a database table:

CREATE TABLE tenants (
    id VARCHAR(255) PRIMARY KEY, -- e.g., 'customer-a', 'customer-b'
    domain VARCHAR(255) UNIQUE NOT NULL, -- e.g., 'customer-a.your-app.com'
    -- other tenant-specific settings
);

Populate it:

INSERT INTO tenants (id, domain) VALUES
('customer-a', 'customer-a.your-app.com'),
('customer-b', 'customer-b.your-app.com');

Next, create a middleware file at the root of your pages directory (or src/middleware.ts if using src directory): middleware.ts.

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(req: NextRequest) {
  const url = req.nextUrl.clone();
  const hostname = req.headers.get('host')!;
  const subdomain = hostname.split('.')[0]; // Simple extraction

  // If it's the main domain (e.g., your-app.com), don't do tenant routing
  if (subdomain === 'www' || subdomain === 'localhost') {
    return NextResponse.next();
  }

  // In a real app, you'd query your database here to find the tenant
  // based on the subdomain. For this example, we'll simulate it.
  const tenantId = subdomain; // Assuming subdomain directly maps to tenantId

  // Store tenantId in a request header for pages/API routes to access
  // or set a cookie. Headers are often simpler for server-side access.
  req.headers.set('x-tenant-id', tenantId);

  return NextResponse.next({
    request: {
      headers: req.headers,
    },
  });
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

Now, in your pages/index.tsx (or any page/API route), you can access the tenantId. If using getServerSideProps:

// pages/index.tsx
import type { GetServerSideProps, NextPage } from 'next';

interface Props {
  tenantId: string | null;
}

const HomePage: NextPage<Props> = ({ tenantId }) => {
  return (
    <div>
      <h1>Welcome to {tenantId ? `${tenantId}'s` : 'Our'} App!</h1>
      {tenantId && <p>This is your personalized dashboard.</p>}
    </div>
  );
};

export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
  const tenantId = context.req.headers['x-tenant-id']?.[0] || null; // Access via headers

  return {
    props: {
      tenantId,
    },
  };
};

export default HomePage;

To test this locally, you can use your /etc/hosts file to simulate subdomains. Add these lines:

127.0.0.1 customer-a.localhost
127.0.0.1 customer-b.localhost

Then run your Next.js app (npm run dev) and visit http://customer-a.localhost:3000 and http://customer-b.localhost:3000.

When deploying, your DNS provider must be configured to point all subdomains (or a wildcard *.your-app.com) to your Next.js server. If you’re using a platform like Vercel, it handles wildcard subdomains automatically for you if you configure your custom domain.

The middleware.ts file is the linchpin. It intercepts requests before they reach your Next.js page components or API routes, allowing you to dynamically alter the request context. This means you can inject tenant-specific information without cluttering your page logic with routing concerns. The x-tenant-id header is a common pattern for passing information from middleware to server-rendered components or API routes.

The config.matcher in middleware is crucial for performance. It tells Next.js which paths to run the middleware on. By excluding static assets and API routes that don’t need tenant context, you reduce unnecessary processing.

A more robust solution would involve a database lookup in the middleware to map the incoming host header to a canonical tenantId. This decouples your application logic from the specific domains customers use. You might also consider using cookies to store the tenantId for client-side access or for persistent sessions, though headers are generally preferred for initial server-side rendering.

The most surprising part is how seamlessly you can inject this context. By modifying request headers within middleware, you effectively "tag" each request with its tenant identity, which is then available for any server-side logic to consume. This makes your page components and API routes cleaner, as they don’t need to parse hostnames themselves.

The next hurdle you’ll likely face is managing tenant-specific configurations and feature flags across this multi-tenant setup.

Want structured learning?

Take the full Nextjs course →