Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Next.js URL Rewrites for Multi-Tenant Apps: Subdomains Without Duplication

Sean

Platform Writer

Sep 02, 2026
9 min read

Multi-tenancy in Next.js comes down to one trick: middleware reads the Host header, works out which tenant is being addressed, and rewrites the request onto a dynamic route that already knows how to render a tenant — so acme.example.com/pricing is served by app/s/[tenant]/pricing without the visitor ever seeing that path.

Next.js URL Rewrites for Multi-Tenant Apps: Subdomains Without Duplication

A rewrite is not a redirect. The URL in the browser does not change, no extra round trip happens, and the visitor sees a clean tenant-branded URL. Everything else — the DNS, the certificates, the data isolation — is the work around that one idea, and it is where the real decisions are. Here is the whole shape.

Table of contents

Rewrite versus redirect, since the distinction is everything

A redirect sends a 3xx and a Location header. The browser makes a second request, and the address bar changes. The visitor ends up somewhere visibly different.

A rewrite is internal. The server maps the incoming path to a different route and renders it. One request, one response, and the URL stays exactly as typed.

For multi-tenancy you want rewrites throughout. A tenant visiting acme.example.com/pricing must stay at that URL — redirecting them to example.com/s/acme/pricing exposes your internal structure, breaks the white-label impression, and adds a round trip to every navigation.

Next.js offers rewrites in two places. next.config.js handles static, known-at-build-time patterns. Middleware handles anything requiring a runtime decision — and tenant resolution is a runtime decision, since tenants are rows in a database rather than entries in a config file. So middleware it is.

The middleware

The core is short. Read the host, strip the root domain, and rewrite.

// middleware.js
import { NextResponse } from 'next/server';

const ROOT = process.env.NEXT_PUBLIC_ROOT_DOMAIN; // e.g. example.com

export function middleware(request) {
  const url = request.nextUrl;
  const host = request.headers.get('host') || '';
  const hostname = host.split(':')[0]; // drop the port in development

  // Requests to the root domain and www are the marketing site.
  if (hostname === ROOT || hostname === `www.${ROOT}`) {
    return NextResponse.next();
  }

  let tenant = null;

  if (hostname.endsWith(`.${ROOT}`)) {
    // acme.example.com -> acme
    tenant = hostname.slice(0, -(ROOT.length + 1));
  } else {
    // A fully custom domain the tenant pointed at us.
    tenant = hostname;
  }

  if (!tenant) return NextResponse.next();

  // Rewrite onto the tenant route tree. URL in the browser is unchanged.
  return NextResponse.rewrite(
    new URL(`/s/${tenant}${url.pathname}${url.search}`, request.url)
  );
}

export const config = {
  matcher: ['/((?!api|_next|_static|favicon.ico|.*\\..*).*)'],
};

Two details in there that matter. The port is stripped from the host because in development you will be on acme.localhost:3000 and the comparison fails otherwise. And the matcher excludes API routes, Next internals and anything with a file extension — rewriting static assets breaks them, and it is a common early mistake.

The route tree it rewrites into is an ordinary dynamic segment: app/s/[tenant]/page.js, app/s/[tenant]/pricing/page.js and so on. One set of components serves every tenant.

Handle reserved subdomains explicitly — www, app, api, admin, mail, blog — either in the middleware or by blocking them at signup. A tenant who registers the subdomain www will cause a genuinely confusing incident.

Resolving the tenant without a database call per request

Middleware runs on every matched request, which makes a database query there a per-request latency cost on your entire application. It is also a problem if your middleware runs at the edge, where a connection to a regional database is slow or unavailable.

The pattern that works: middleware does only the string manipulation, passing the tenant identifier onward without validating it. Validation and data loading happen in the page or layout, where you already have data-fetching infrastructure and caching.

Pass the identifier through a request header so downstream code does not re-parse the host:

const headers = new Headers(request.headers);
headers.set('x-tenant', tenant);

return NextResponse.rewrite(
  new URL(`/s/${tenant}${url.pathname}${url.search}`, request.url),
  { request: { headers } }
);

Then the layout looks the tenant up, caches the result, and renders a 404 for an unknown one. If you must resolve in middleware — for example to reject unknown hosts before rendering anything — use an edge-compatible key-value store rather than your primary database, and treat it as a cache populated from the database on tenant changes.

Validate the identifier before interpolating it into a path. A tenant string containing path traversal characters that gets rewritten into a URL is a route-confusion bug at best. Restrict to lowercase alphanumerics and hyphens, and reject anything else.

Wildcard DNS and the certificate problem

The application code is the easy half. Serving *.example.com requires two things that people discover late.

Wildcard DNS. A record for *.example.com pointing at your host, so every subdomain resolves without you creating records per tenant. Note that a wildcard matches only one label — *.example.com covers acme.example.com but not a.b.example.com.

A wildcard certificate. This is the awkward part, because a wildcard certificate can only be issued through DNS validation, not HTTP validation. Your certificate client needs API credentials for your DNS provider so it can publish the required TXT record. That is a credential with broad power over your zone, and it needs to live somewhere appropriate.

Note also that a wildcard certificate covers *.example.com but not example.com itself, so the certificate needs both names on it.

Custom domains are a separate problem. Once tenants want app.theircompany.com rather than a subdomain of yours, wildcards do not help — each custom domain needs its own certificate, issued on demand after the tenant points a CNAME at you, and renewed automatically thereafter. This is a meaningful piece of infrastructure and it is worth deciding early whether you are offering custom domains, because retrofitting on-demand certificate issuance is significantly more work than planning for it.

In development, *.localhost resolves to loopback in most browsers without any hosts-file editing, so acme.localhost:3000 generally just works.

Data isolation, which is the part that actually matters

Routing is cosmetic. The consequential decision is making sure tenant A can never read tenant B’s data, and a rewrite does nothing to help with that.

The failure mode is precise: middleware resolves the tenant from the host, but a query somewhere filters only by a record identifier taken from the URL. Change the identifier in the address bar and you read another tenant’s record. Every query must be scoped by tenant, taken from the server-side context rather than from anything the client supplied.

  • Scope in the query itselfWHERE id = ? AND tenant_id = ?. Not fetch-then-check, which has a branch someone will forget.
  • Take the tenant from server context, never from a client-supplied parameter or header the browser could set. The rewritten path segment is derived from the host, which is trustworthy; a query parameter is not.
  • Consider row-level security if you are on Postgres. Enforcing isolation in the database means an unscoped query returns nothing rather than everything, which is a much better failure mode than relying on every developer remembering.
  • Test it. An automated test that authenticates as tenant A and requests tenant B’s resources, asserting a 404, is worth more than any amount of code review.

Also scope everything adjacent to the data: cache keys must include the tenant or one tenant will serve another’s cached page, uploaded files need per-tenant prefixes with access checks, and background jobs need the tenant in their payload rather than inheriting it from ambient state.

Get isolation right first. The rewrite is twenty lines; a cross-tenant data leak is an incident with a disclosure obligation.

How this fits the rest of the stack

Multi-tenancy is mostly a hosting shape: wildcard certificates, on-demand certificates for custom domains, and a database whose connection limits have to absorb every tenant at once. The RunxBuild hosting calculator puts the service, database, storage and bandwidth on one screen so the ceiling is a number you picked. RunxBuild deploys Next.js from a repository with custom domains and certificates handled at deploy, managed Postgres or MySQL with configurable connection limits behind private networking, and autoscaling between a floor and ceiling plan.

Useful related references:

FAQ

What is the difference between a rewrite and a redirect in Next.js?

A redirect sends a 3xx response and the browser makes a second request, changing the URL in the address bar. A rewrite maps the request to a different route internally — one request, and the URL stays as typed. Multi-tenancy needs rewrites so tenant URLs stay clean and no round trip is added.

How do I map subdomains to routes in Next.js?

In middleware, read the Host header, strip the root domain to get the tenant identifier, and rewrite to a dynamic route such as /s/[tenant] plus the original path. Strip the port from the host so development works, and exclude API routes, _next and files with extensions from the matcher.

Should I look up the tenant in middleware?

Generally no. Middleware runs on every matched request, so a database query there adds latency to the whole application and may not work at the edge. Do string manipulation in middleware, pass the identifier through a request header, and validate and load the tenant in the layout where caching already exists.

Why do I need DNS validation for a wildcard certificate?

Certificate authorities only issue wildcards through DNS validation, never HTTP validation, so the certificate client needs API access to your DNS provider to publish a TXT record. Note also that a wildcard for *.example.com does not cover example.com itself — both names need to be on the certificate.

How do I stop one tenant reading another tenant’s data?

Scope every query by tenant in the query itself, taking the tenant from server-side context derived from the host rather than from any client-supplied value. Consider Postgres row-level security so an unscoped query returns nothing rather than everything, and add a test that authenticates as one tenant and asserts a 404 on another’s resources.

#rewrite url in next js like in multi-tenant app#Next.js#multi-tenancy#middleware#subdomains