> This page is for Developer.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.makeswift.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.makeswift.com/_mcp/server.

# Path-based routing

> Route tenants using URL path prefixes in a multi-tenant Makeswift application.

Route tenants using URL path prefixes in a multi-tenant Makeswift application.

Path-based routing uses the first segment of the URL path to identify the tenant for public visitors. For example, `example.com/siteA/products` serves the products page for Site A, while the bare root domain (`example.com`) serves the default tenant.

This is a hybrid approach: public visitors are identified by the URL path, while the Visual Builder connects via subdomain (because a Makeswift host URL is an origin only and cannot contain a path). Both styles render the same tenant content.

Before following this guide, complete the [common configuration](/developer/docs/guides/how-to/multi-tenancy) steps.

## How it works

The middleware resolves the tenant in three steps:

1. **Subdomain first.** If the host header has a valid tenant subdomain (for example, `siteA.localhost:3000`), rewrite to `/siteA/...`. This is how the Visual Builder connects.
2. **Path fallback.** If the first path segment is already a valid tenant (for example, `/siteA/products`), pass it through unchanged.
3. **Default.** Otherwise — the bare root domain or any unrecognized host — prepend the `default` tenant.

```
Public (path):                 localhost:3000/siteA/products
  └─> Middleware: no tenant subdomain; first path segment "siteA"
      is a valid tenant → passed through unchanged → /siteA/products

Public (subdomain) / Builder:  siteA.localhost:3000/products
  └─> Middleware: subdomain "siteA" is a valid tenant
      → rewritten → /siteA/products

Both → app/[[...path]]/page.tsx with params.path = ["siteA", "products"]
  │
  ├─> 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`**

```ts title="middleware.ts"
import { NextRequest, NextResponse } from "next/server";

import {
  DEFAULT_TENANT_ID,
  getSubdomainFromHost,
  isValidTenantId,
} from "./lib/makeswift/tenants";

export function middleware(request: NextRequest) {
  const host = request.headers.get("host") ?? "";
  const url = request.nextUrl.clone();

  // 1. Prefer the subdomain — this is how the Visual Builder connects.
  const subdomain = getSubdomainFromHost(host);
  if (subdomain != null && isValidTenantId(subdomain)) {
    if (
      url.pathname !== `/${subdomain}` &&
      !url.pathname.startsWith(`/${subdomain}/`)
    ) {
      url.pathname = `/${subdomain}${url.pathname}`;
    }
    return NextResponse.rewrite(url);
  }

  // 2. Fall back to path-based routing for public viewing.
  const firstPathSegment = url.pathname.split("/").at(1) ?? null;
  if (firstPathSegment != null && isValidTenantId(firstPathSegment)) {
    return NextResponse.next();
  }

  // 3. No tenant in the host or the path — use the default tenant.
  url.pathname = `/${DEFAULT_TENANT_ID}${url.pathname}`;
  return NextResponse.rewrite(url);
}

export const config = {
  matcher: [
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
  ],
};
```

| Request                                     | Rewritten to        |
| ------------------------------------------- | ------------------- |
| `siteA.localhost:3000/products` (subdomain) | `/siteA/products`   |
| `localhost:3000/siteA/products` (path)      | `/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

Path-based URLs don't carry tenant information in the host header, so the Visual Builder cannot connect using a path URL alone. Each Makeswift site needs its host URL set to the subdomain URL.

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

| Site    | Host URL                      |
| ------- | ----------------------------- |
| Default | `http://localhost:3000`       |
| Site A  | `http://siteA.localhost:3000` |
| Site B  | `http://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:

```bash
npm run dev
```

Both path and subdomain URLs work for public viewing:

| URL                           | Tenant             |
| ----------------------------- | ------------------ |
| `http://localhost:3000`       | Default            |
| `http://localhost:3000/siteA` | Site A (path)      |
| `http://localhost:3000/siteB` | Site B (path)      |
| `http://siteA.localhost:3000` | Site A (subdomain) |
| `http://siteB.localhost:3000` | Site B (subdomain) |

> **Note**
>
> Path URLs work everywhere. For the subdomain URLs (and the Visual Builder), most browsers (Chrome, Firefox, Edge) resolve `*.localhost` to `127.0.0.1` automatically. Safari does not — see [Resolving subdomains on Safari](#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.** Path-based public URLs (`example.com/siteA`) work without any DNS changes, but the subdomain URLs and the Visual Builder require each tenant subdomain to resolve. Configure wildcard DNS (`*.example.com`) or an individual DNS record per tenant.

## 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`:

   ```bash
   sudo vim /etc/hosts
   ```

2. Add an entry for each subdomain:

   **`/etc/hosts`**

   ```text title="/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 path example](https://github.com/makeswift/multi-tenant-example-path/) repository.