Multi-tenancy

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:

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

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 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 for type-safe environment validation:

$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
1import { createEnv } from "@t3-oss/env-nextjs";
2import { z } from "zod";
3
4export const env = createEnv({
5 server: {
6 ROOT_DOMAIN: z.string().min(1),
7 DEFAULT_MAKESWIFT_SITE_API_KEY: z.string().min(1),
8 SITE_A_SUBDOMAIN: z.string().min(1),
9 SITE_A_MAKESWIFT_SITE_API_KEY: z.string().min(1),
10 SITE_B_SUBDOMAIN: z.string().min(1),
11 SITE_B_MAKESWIFT_SITE_API_KEY: z.string().min(1),
12 },
13 client: {},
14 runtimeEnv: {
15 ROOT_DOMAIN: process.env.ROOT_DOMAIN,
16 DEFAULT_MAKESWIFT_SITE_API_KEY:
17 process.env.DEFAULT_MAKESWIFT_SITE_API_KEY,
18 SITE_A_SUBDOMAIN: process.env.SITE_A_SUBDOMAIN,
19 SITE_A_MAKESWIFT_SITE_API_KEY:
20 process.env.SITE_A_MAKESWIFT_SITE_API_KEY,
21 SITE_B_SUBDOMAIN: process.env.SITE_B_SUBDOMAIN,
22 SITE_B_MAKESWIFT_SITE_API_KEY:
23 process.env.SITE_B_MAKESWIFT_SITE_API_KEY,
24 },
25});

Add the values to .env.local:

.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
1import { env } from "env";
2
3export const DEFAULT_TENANT_ID = "default";
4
5const SUBDOMAIN_TO_API_KEY: Record<string, string> = {
6 [DEFAULT_TENANT_ID]: env.DEFAULT_MAKESWIFT_SITE_API_KEY,
7 [env.SITE_A_SUBDOMAIN]: env.SITE_A_MAKESWIFT_SITE_API_KEY,
8 [env.SITE_B_SUBDOMAIN]: env.SITE_B_MAKESWIFT_SITE_API_KEY,
9};
10
11export function getApiKey(subdomain: string) {
12 const apiKey = SUBDOMAIN_TO_API_KEY[subdomain];
13
14 if (!apiKey) {
15 throw new Error(
16 `Invalid subdomain: ${subdomain}. Only ${Object.keys(SUBDOMAIN_TO_API_KEY).join(", ")} are supported.`
17 );
18 }
19
20 return apiKey;
21}
22
23export function isValidTenantId(subdomain: string) {
24 return Object.prototype.hasOwnProperty.call(
25 SUBDOMAIN_TO_API_KEY,
26 subdomain
27 );
28}
29
30export function getSubdomainFromHost(host: string): string | null {
31 const hostname = host.split(":")[0];
32
33 if (hostname === env.ROOT_DOMAIN) {
34 return null;
35 }
36
37 const suffix = `.${env.ROOT_DOMAIN}`;
38 if (hostname.endsWith(suffix)) {
39 const subdomain = hostname.slice(0, -suffix.length);
40 return subdomain.length > 0 ? subdomain : null;
41 }
42
43 return null;
44}
45
46export function getTenantFromHost(host: string): string {
47 const subdomain = getSubdomainFromHost(host);
48
49 return subdomain != null && isValidTenantId(subdomain)
50 ? subdomain
51 : DEFAULT_TENANT_ID;
52}

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
1import { notFound } from "next/navigation";
2
3import { Makeswift, Page as MakeswiftPage } from "@makeswift/runtime/next";
4import { getSiteVersion } from "@makeswift/runtime/next/server";
5
6import { runtime } from "../../../../../lib/makeswift/runtime";
7import { DEFAULT_TENANT_ID, getApiKey } from "../../../../../lib/makeswift/tenants";
8
9export default async function Page({
10 params,
11}: {
12 params: Promise<{ path?: string[] }>;
13}) {
14 const pathSegments = (await params)?.path ?? [];
15
16 if (pathSegments.length === 0) return notFound();
17
18 const subdomainFromPath = pathSegments.at(0) ?? DEFAULT_TENANT_ID;
19 const remainingPath = pathSegments.slice(1);
20 const makeswiftPath = "/" + remainingPath.join("/");
21
22 const makeswiftClient = new Makeswift(getApiKey(subdomainFromPath), {
23 runtime,
24 });
25
26 const snapshot = await makeswiftClient.getPageSnapshot(makeswiftPath, {
27 siteVersion: getSiteVersion(),
28 });
29
30 if (snapshot == null) return notFound();
31
32 return <MakeswiftPage snapshot={snapshot} />;
33}

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
1import { NextRequest } from "next/server";
2import { headers } from "next/headers";
3
4import { MakeswiftApiHandler } from "@makeswift/runtime/next/server";
5
6import "../../../../../lib/makeswift/components";
7import { runtime } from "../../../../../lib/makeswift/runtime";
8import { getApiKey, getTenantFromHost } from "../../../../../lib/makeswift/tenants";
9
10async function handler(
11 req: NextRequest,
12 context: { params: Promise<{ makeswift: string[] }> }
13) {
14 const headersList = await headers();
15 const host = headersList.get("host") ?? "";
16
17 const apiKey = getApiKey(getTenantFromHost(host));
18
19 return await MakeswiftApiHandler(apiKey, { runtime })(req, context);
20}
21
22export { 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
1import type { Metadata } from "next";
2
3import { getSiteVersion } from "@makeswift/runtime/next/server";
4
5import "../../../../../lib/makeswift/components";
6import { MakeswiftProvider } from "../../../../../lib/makeswift/provider";
7
8export const metadata: Metadata = {
9 title: "Multi-Tenant Makeswift Site",
10 description: "A multi-tenant website powered by Makeswift",
11};
12
13export default async function RootLayout({
14 children,
15}: Readonly<{
16 children: React.ReactNode;
17}>) {
18 return (
19 <html lang="en">
20 <body>
21 <MakeswiftProvider siteVersion={await getSiteVersion()}>
22 {children}
23 </MakeswiftProvider>
24 </body>
25 </html>
26 );
27}

Add a new tenant

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

1

Add environment variables

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

.env.local
$SITE_C_SUBDOMAIN=siteC
$SITE_C_MAKESWIFT_SITE_API_KEY=paste-your-site-c-api-key-here
2

Update env.ts

Register the new variables in both server and runtimeEnv:

env.ts
1server: {
2 // ... existing entries
3 SITE_C_SUBDOMAIN: z.string().min(1),
4 SITE_C_MAKESWIFT_SITE_API_KEY: z.string().min(1),
5},
6runtimeEnv: {
7 // ... existing entries
8 SITE_C_SUBDOMAIN: process.env.SITE_C_SUBDOMAIN,
9 SITE_C_MAKESWIFT_SITE_API_KEY:
10 process.env.SITE_C_MAKESWIFT_SITE_API_KEY,
11},
3

Update tenants.ts

Add the mapping in SUBDOMAIN_TO_API_KEY:

lib/makeswift/tenants.ts
1const SUBDOMAIN_TO_API_KEY: Record<string, string> = {
2 // ... existing entries
3 [env.SITE_C_SUBDOMAIN]: env.SITE_C_MAKESWIFT_SITE_API_KEY,
4};
4

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: