Subdomain-based routing

Route tenants using subdomains in a multi-tenant Makeswift application.

Subdomain-based routing uses the subdomain to identify the tenant. For example, siteA.example.com serves Site A and siteB.example.com serves Site B. The bare root domain (example.com) serves the default tenant. Subdomain URLs are also what the Visual Builder requires to connect to a site.

Before following this guide, complete the common configuration steps.

How it works

A request flows through the system in four steps:

  1. Detect the tenant from the subdomain, resolved against the configured ROOT_DOMAIN.
  2. Resolve to a Makeswift site by mapping the subdomain to the appropriate Site API key.
  3. Rewrite the URL so the resolved tenant is the first path segment.
  4. Serve tenant-specific content from the correct Makeswift site.
User visits: siteA.localhost:3000/products
├─> Middleware extracts "siteA" from the host header
├─> Resolves the tenant (falls back to "default" for the root
│ domain or any unknown host)
├─> Rewrites URL to: /siteA/products
└─> Routes to app/[[...path]]/page.tsx
├─> Extracts tenantId = "siteA" from the first path segment
├─> Calls getApiKey("siteA") → Site A's API key
├─> Creates a Makeswift client with Site A's API key
├─> Fetches the page snapshot for "/products" from Site A
└─> Renders the page with tenant-specific content

Add the middleware

Create a middleware.ts file at the root of your project:

middleware.ts
1import { NextRequest, NextResponse } from "next/server";
2
3import {
4 DEFAULT_TENANT_ID,
5 getSubdomainFromHost,
6 isValidTenantId,
7} from "./lib/makeswift/tenants";
8
9export function middleware(request: NextRequest) {
10 const host = request.headers.get("host") ?? "";
11 const url = request.nextUrl.clone();
12
13 const subdomain = getSubdomainFromHost(host);
14 const tenant =
15 subdomain != null && isValidTenantId(subdomain)
16 ? subdomain
17 : DEFAULT_TENANT_ID;
18
19 url.pathname = `/${tenant}${url.pathname}`;
20 return NextResponse.rewrite(url);
21}
22
23export const config = {
24 matcher: [
25 "/((?!api|_next/static|_next/image|favicon.ico).*)",
26 ],
27};

The middleware resolves the tenant from the subdomain and rewrites the URL so the tenant is always the first path segment. The bare root domain and any unrecognized host (for example, www or a platform preview URL) fall back to the default tenant.

RequestRewritten to
siteA.localhost:3000/products/siteA/products
localhost:3000/products/default/products
www.localhost:3000/products/default/products

Makeswift API routes (/api/makeswift/*) are excluded via config.matcher — the API handler resolves the tenant from the host itself.

Configure the Visual Builder host URLs

Each Makeswift site needs its host URL set to the subdomain URL so the Visual Builder can connect.

In the Makeswift dashboard, open the settings for each site and set the host URL:

SiteHost URL
Defaulthttp://localhost:3000
Site Ahttp://siteA.localhost:3000
Site Bhttp://siteB.localhost:3000

For production, replace localhost:3000 with your deployed domain (for example, siteA.example.com).

Test the setup

Start the development server and verify each tenant loads:

$npm run dev
URLTenant
http://localhost:3000Default
http://siteA.localhost:3000Site A
http://siteB.localhost:3000Site B

Most browsers (Chrome, Firefox, Edge) resolve *.localhost to 127.0.0.1 automatically. Safari does not — see Resolving subdomains on Safari below.

Production deployment

The same logic works in production with two changes:

  • Set ROOT_DOMAIN to your real domain (for example, ROOT_DOMAIN=example.com). Tenant subdomains are resolved relative to this value, and the bare apex domain maps to the default tenant.
  • Point each tenant subdomain at your deployment. Configure wildcard DNS (*.example.com) or an individual DNS record per tenant so siteA.example.com, siteB.example.com, and so on all reach the same app. Update each Makeswift site’s host URL to its production subdomain.

Resolving subdomains on Safari

Chrome, Firefox, and Edge resolve *.localhost to 127.0.0.1 automatically, but Safari does not. To make the tenant subdomains work in Safari during local development, map them in /etc/hosts:

  1. Edit /etc/hosts:

    $sudo vim /etc/hosts
  2. Add an entry for each subdomain:

    /etc/hosts
    127.0.0.1 siteA.localhost
    127.0.0.1 siteB.localhost
  3. Visit http://siteA.localhost:3000, http://siteB.localhost:3000, and so on.

Example repository

For a complete working implementation, see the multi-tenant host example repository.