DEV Community

dan
dan

Posted on • Originally published at listkars.com

How to Build Multi-Tenant Subdomains with Next.js 15 and Middleware

How to Build Multi-Tenant Subdomains with Next.js 15 and Middleware

Building a SaaS where each customer gets their own subdomain (customer.yourdomain.com)? Here's how I did it with Next.js 15 App Router.

The Architecture

tenant-a.listkars.com  →  Same Next.js app
tenant-b.listkars.com  →  Same Next.js app
tenant-c.listkars.com  →  Same Next.js app
Enter fullscreen mode Exit fullscreen mode

One deployment, many tenants. The subdomain determines which data to show.

3 Step 1: Middleware to Extract Subdomain

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

export function middleware(request: NextRequest) {
  const host = request.headers.get('host') ?? '';
  const subdomain = host.split('.')[0];

  // Skip for main domain
  if (subdomain === 'www' || subdomain === 'listkars') {
    return NextResponse.next();
  }

  // Pass subdomain to the app via header
  const response = NextResponse.next();
  response.headers.set('x-tenant-slug', subdomain);
  return response;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Read Tenant in Server Components

// app/[[...slug]]/page.tsx
import { headers } from 'next/headers';

export default async function Page() {
  const headersList = await headers();
  const tenantSlug = headersList.get('x-tenant-slug') ?? 'unknown';

  // Fetch tenant data from API
  const tenant = await fetch(`${API_URL}/storefront/${tenantSlug}`);
  // Render tenant's theme with their data
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Database Design

Single database with tenant_id on every table:

CREATE TABLE cars (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  title VARCHAR(255),
  price DECIMAL(12,2),
  -- ... other fields
);

CREATE INDEX idx_cars_tenant ON cars(tenant_id);
Enter fullscreen mode Exit fullscreen mode

Every query filters by tenant_id. No data leakage between tenants.

Step 4: Reverse Proxy (Caddy)

*.listkars.com {
  reverse_proxy storefront:3001 {
    header_up X-Tenant-Slug {labels.3}
  }
}
Enter fullscreen mode Exit fullscreen mode

Caddy extracts the subdomain and passes it as a header.

The Result

Each dealer at ListKars gets their own subdomain with a unique theme, car listings, and lead management — all from a single Next.js deployment.

Key Takeaways

  1. Middleware for routing — extract subdomain, pass as header
  2. Single DB, tenant_id everywhere — simple and scales well
  3. One deployment — no per-tenant infrastructure
  4. ISR cachingrevalidate = 60 means pages are fast but data stays fresh

This pattern powers listkars.com — a free platform for car dealers to create branded websites.

Top comments (0)