> 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.

# Multi-tenancy

> Serve multiple Makeswift sites from a single Next.js application by mapping tenants to Site API keys.

Serve multiple Makeswift sites from a single Next.js application by mapping tenants to Site API keys.

Multi-tenancy lets you run one Next.js codebase that serves content for many independent Makeswift sites. Each tenant gets its own content, pages, and branding while sharing the same set of registered components and a single deployment.

## Routing approaches

Choose the routing approach that fits your use case:

#### [Subdomain-based](/developer/docs/guides/how-to/multi-tenancy/subdomain-based)

Route tenants with subdomains like `siteA.example.com`

#### [Path-based](/developer/docs/guides/how-to/multi-tenancy/path-based)

Route tenants with URL path prefixes like `example.com/siteA`

Both approaches share the common configuration described on this page. The routing-specific middleware is covered in each sub-guide.

> **Note**
>
> The Visual Builder requires a subdomain-based host URL to connect to each site, because a Makeswift host URL is an origin only and cannot contain a path. Both routing approaches use subdomain host URLs for the builder.

## Prerequisites

Before setting up multi-tenancy, make sure you have:

* An existing Next.js App Router project with Makeswift installed. If you don't have one, follow the [App Router installation](/developer/docs/get-started/installation/app-router) guide.
* Two or more Makeswift sites in your workspace, each with its own Site API key.

## Set up environment variables

Install [`@t3-oss/env-nextjs`](https://env.t3.gg/) for type-safe environment validation:

```bash
npm install @t3-oss/env-nextjs zod
```

Create an `env.ts` file at the root of your project. Each tenant needs a subdomain identifier and a Site API key. The `ROOT_DOMAIN` variable identifies the root domain the app is served from, used to distinguish tenant subdomains from the bare root domain.

**`env.ts`**

```ts title="env.ts"
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    ROOT_DOMAIN: z.string().min(1),
    DEFAULT_MAKESWIFT_SITE_API_KEY: z.string().min(1),
    SITE_A_SUBDOMAIN: z.string().min(1),
    SITE_A_MAKESWIFT_SITE_API_KEY: z.string().min(1),
    SITE_B_SUBDOMAIN: z.string().min(1),
    SITE_B_MAKESWIFT_SITE_API_KEY: z.string().min(1),
  },
  client: {},
  runtimeEnv: {
    ROOT_DOMAIN: process.env.ROOT_DOMAIN,
    DEFAULT_MAKESWIFT_SITE_API_KEY:
      process.env.DEFAULT_MAKESWIFT_SITE_API_KEY,
    SITE_A_SUBDOMAIN: process.env.SITE_A_SUBDOMAIN,
    SITE_A_MAKESWIFT_SITE_API_KEY:
      process.env.SITE_A_MAKESWIFT_SITE_API_KEY,
    SITE_B_SUBDOMAIN: process.env.SITE_B_SUBDOMAIN,
    SITE_B_MAKESWIFT_SITE_API_KEY:
      process.env.SITE_B_MAKESWIFT_SITE_API_KEY,
  },
});
```

Add the values to `.env.local`:

**`.env.local`**

```bash title=".env.local"
ROOT_DOMAIN=localhost
DEFAULT_MAKESWIFT_SITE_API_KEY=paste-your-default-api-key-here
SITE_A_SUBDOMAIN=siteA
SITE_A_MAKESWIFT_SITE_API_KEY=paste-your-site-a-api-key-here
SITE_B_SUBDOMAIN=siteB
SITE_B_MAKESWIFT_SITE_API_KEY=paste-your-site-b-api-key-here
```

Set `ROOT_DOMAIN` to `localhost` during development. In production, set it to your real domain (for example, `example.com`).

## Create the tenant mapping

Create a `tenants.ts` file in `lib/makeswift/` that maps subdomain identifiers to Site API keys and exposes helpers for resolving a tenant from a host.

**`lib/makeswift/tenants.ts`**

```ts title="lib/makeswift/tenants.ts"
import { env } from "env";

export const DEFAULT_TENANT_ID = "default";

const SUBDOMAIN_TO_API_KEY: Record<string, string> = {
  [DEFAULT_TENANT_ID]: env.DEFAULT_MAKESWIFT_SITE_API_KEY,
  [env.SITE_A_SUBDOMAIN]: env.SITE_A_MAKESWIFT_SITE_API_KEY,
  [env.SITE_B_SUBDOMAIN]: env.SITE_B_MAKESWIFT_SITE_API_KEY,
};

export function getApiKey(subdomain: string) {
  const apiKey = SUBDOMAIN_TO_API_KEY[subdomain];

  if (!apiKey) {
    throw new Error(
      `Invalid subdomain: ${subdomain}. Only ${Object.keys(SUBDOMAIN_TO_API_KEY).join(", ")} are supported.`
    );
  }

  return apiKey;
}

export function isValidTenantId(subdomain: string) {
  return Object.prototype.hasOwnProperty.call(
    SUBDOMAIN_TO_API_KEY,
    subdomain
  );
}

export function getSubdomainFromHost(host: string): string | null {
  const hostname = host.split(":")[0];

  if (hostname === env.ROOT_DOMAIN) {
    return null;
  }

  const suffix = `.${env.ROOT_DOMAIN}`;
  if (hostname.endsWith(suffix)) {
    const subdomain = hostname.slice(0, -suffix.length);
    return subdomain.length > 0 ? subdomain : null;
  }

  return null;
}

export function getTenantFromHost(host: string): string {
  const subdomain = getSubdomainFromHost(host);

  return subdomain != null && isValidTenantId(subdomain)
    ? subdomain
    : DEFAULT_TENANT_ID;
}
```

Key functions:

* `getApiKey` returns the Site API key for a tenant and throws if the tenant is unknown.
* `isValidTenantId` checks whether a subdomain is a known tenant.
* `getSubdomainFromHost` extracts the subdomain from a host header relative to `ROOT_DOMAIN`.
* `getTenantFromHost` resolves a host to a known tenant, falling back to `default` for the root domain or any unrecognized host.

## Update the catch-all page route

Replace your existing catch-all route with one that extracts the tenant from the first path segment and creates a tenant-specific Makeswift client.

**`app/[[...path]]/page.tsx`**

```tsx title="app/[[...path]]/page.tsx"
import { notFound } from "next/navigation";

import { Makeswift, Page as MakeswiftPage } from "@makeswift/runtime/next";
import { getSiteVersion } from "@makeswift/runtime/next/server";

import { runtime } from "@/lib/makeswift/runtime";
import { DEFAULT_TENANT_ID, getApiKey } from "@/lib/makeswift/tenants";

export default async function Page({
  params,
}: {
  params: Promise<{ path?: string[] }>;
}) {
  const pathSegments = (await params)?.path ?? [];

  if (pathSegments.length === 0) return notFound();

  const subdomainFromPath = pathSegments.at(0) ?? DEFAULT_TENANT_ID;
  const remainingPath = pathSegments.slice(1);
  const makeswiftPath = "/" + remainingPath.join("/");

  const makeswiftClient = new Makeswift(getApiKey(subdomainFromPath), {
    runtime,
  });

  const snapshot = await makeswiftClient.getPageSnapshot(makeswiftPath, {
    siteVersion: getSiteVersion(),
  });

  if (snapshot == null) return notFound();

  return <MakeswiftPage snapshot={snapshot} />;
}
```

The middleware (configured in each sub-guide) rewrites the URL so the first path segment is always the tenant identifier. The remaining segments form the Makeswift page path used to fetch the correct snapshot.

## Update the Makeswift API handler

The API handler enables Draft Mode and the Visual Builder. It is excluded from middleware, so it resolves the tenant from the host header directly via `getTenantFromHost`.

**`app/api/makeswift/[...makeswift]/route.ts`**

```ts title="app/api/makeswift/[...makeswift]/route.ts"
import { NextRequest } from "next/server";
import { headers } from "next/headers";

import { MakeswiftApiHandler } from "@makeswift/runtime/next/server";

import "@/lib/makeswift/components";
import { runtime } from "@/lib/makeswift/runtime";
import { getApiKey, getTenantFromHost } from "@/lib/makeswift/tenants";

async function handler(
  req: NextRequest,
  context: { params: Promise<{ makeswift: string[] }> }
) {
  const headersList = await headers();
  const host = headersList.get("host") ?? "";

  const apiKey = getApiKey(getTenantFromHost(host));

  return await MakeswiftApiHandler(apiKey, { runtime })(req, context);
}

export { handler as GET, handler as POST, handler as OPTIONS };
```

Because the builder always connects via a subdomain host URL, `getTenantFromHost` resolves the correct tenant from that subdomain. For the root domain or unknown hosts, it falls back to the default tenant.

## Update the root layout

**`app/layout.tsx`**

```tsx title="app/layout.tsx"
import type { Metadata } from "next";

import { getSiteVersion } from "@makeswift/runtime/next/server";

import "@/lib/makeswift/components";
import { MakeswiftProvider } from "@/lib/makeswift/provider";

export const metadata: Metadata = {
  title: "Multi-Tenant Makeswift Site",
  description: "A multi-tenant website powered by Makeswift",
};

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <MakeswiftProvider siteVersion={await getSiteVersion()}>
          {children}
        </MakeswiftProvider>
      </body>
    </html>
  );
}
```

## Add a new tenant

Adding a tenant requires environment variables plus a small wiring change in two files:

#### Add environment variables

Add the new tenant's subdomain and Site API key to `.env.local` (and your hosting platform):

**`.env.local`**

```bash title=".env.local"
SITE_C_SUBDOMAIN=siteC
SITE_C_MAKESWIFT_SITE_API_KEY=paste-your-site-c-api-key-here
```

#### Update env.ts

Register the new variables in both `server` and `runtimeEnv`:

**`env.ts`**

```ts title="env.ts"
server: {
  // ... existing entries
  SITE_C_SUBDOMAIN: z.string().min(1),
  SITE_C_MAKESWIFT_SITE_API_KEY: z.string().min(1),
},
runtimeEnv: {
  // ... existing entries
  SITE_C_SUBDOMAIN: process.env.SITE_C_SUBDOMAIN,
  SITE_C_MAKESWIFT_SITE_API_KEY:
    process.env.SITE_C_MAKESWIFT_SITE_API_KEY,
},
```

#### Update tenants.ts

Add the mapping in `SUBDOMAIN_TO_API_KEY`:

**`lib/makeswift/tenants.ts`**

```ts title="lib/makeswift/tenants.ts"
const SUBDOMAIN_TO_API_KEY: Record<string, string> = {
  // ... existing entries
  [env.SITE_C_SUBDOMAIN]: env.SITE_C_MAKESWIFT_SITE_API_KEY,
};
```

#### Connect the host URL

In the Makeswift dashboard, set the new site's host URL to its subdomain (for example, `http://siteC.localhost:3000` in development).

## Next steps

Follow one of the routing-specific guides to configure the middleware for your approach:

* [Subdomain-based routing](/developer/docs/guides/how-to/multi-tenancy/subdomain-based) — tenants identified by subdomain (`siteA.example.com`)
* [Path-based routing](/developer/docs/guides/how-to/multi-tenancy/path-based) — tenants identified by URL path prefix (`example.com/siteA`)