Skip to content

makeswift/multi-tenant-example-path

Repository files navigation

Multi-Tenant Makeswift Implementation (Path-Based / Hybrid)

This project demonstrates how to implement multi-tenancy in a Next.js application using Makeswift, where each tenant maps to its own Makeswift site via that site's API key.

It uses hybrid routing: public visitors are identified by the URL path (e.g. localhost:3000/siteA serves Site A), while the Makeswift builder connects via the subdomain (e.g. siteA.localhost:3000). The bare root domain (localhost:3000) serves the default tenant. Subdomain URLs also work for public viewing, so both styles render the same tenant content — but the builder requires a subdomain because a Makeswift host URL is an origin only and cannot contain a path.

Prerequisites

This guide assumes you've set up a Makeswift site before. If you haven't, start with the Makeswift quickstart.

You'll need:

  • One Makeswift site per tenant (this example uses three: a default plus Site A and Site B). Each site has its own content and its own Site API key, found in the Makeswift dashboard under the site's settings.
  • Node.js and pnpm (pnpm@9 — see packageManager in package.json).

Quickstart

  1. Install dependencies:

    pnpm install
  2. Create your env file by copying the example, then fill in each Site API key:

    cp .env.example .env.local
    ROOT_DOMAIN=localhost                 # "localhost" in dev; your real domain (e.g. "example.com") in prod
    DEFAULT_MAKESWIFT_SITE_API_KEY=...    # used for the bare root domain
    SITE_A_SUBDOMAIN=siteA                # doubles as the path prefix (public) and subdomain (builder)
    SITE_A_MAKESWIFT_SITE_API_KEY=...
    SITE_B_SUBDOMAIN=siteB
    SITE_B_MAKESWIFT_SITE_API_KEY=...
  3. Connect each Makeswift site to its subdomain. In each site's host settings in the Makeswift dashboard, set the host URL to that tenant's subdomain so the builder loads the right content (the builder cannot use a path):

    • Default site → http://localhost:3000
    • Site A → http://siteA.localhost:3000
    • Site B → http://siteB.localhost:3000
  4. Run the development server:

    pnpm dev
  5. Visit each tenant in your browser — both routing styles work for public viewing:

    • Default tenant: http://localhost:3000
    • Site A: http://localhost:3000/siteA (path) or http://siteA.localhost:3000 (subdomain)
    • Site B: http://localhost:3000/siteB (path) or http://siteB.localhost:3000 (subdomain)

    Path URLs work everywhere. For the subdomain URLs (and the builder), most browsers (Chrome, Firefox, Edge) resolve *.localhost to 127.0.0.1 automatically. Safari does not — see Resolving subdomains on Safari below.

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), or, for path-based public URLs, from the first path segment.
  2. Resolve to a Makeswift site by mapping the tenant identifier 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.
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

Key Files

env.ts — Environment configuration

Defines and validates the environment variables for each tenant using @t3-oss/env-nextjs. Each tenant needs a subdomain identifier and a Makeswift Site API key. The validated env object is consumed throughout the app. See env.ts.

lib/makeswift/tenants.ts — Identifier-to-API-key mapping

The core of the multi-tenancy logic. It maps tenant identifiers to Site API keys and exposes helpers for resolving a tenant from a host. See lib/makeswift/tenants.ts.

  • SUBDOMAIN_TO_API_KEY — the identifier → API key map. DEFAULT_TENANT_ID ('default') is used for the bare root domain and any unrecognized host.
  • getApiKey(subdomain) — returns the API key for a tenant identifier, and throws if it isn't a known tenant.
  • getTenantFromHost(host) — resolves a host header to a known tenant id, falling back to default and never throwing. This is why the API route can safely derive a tenant from an arbitrary host (the apex domain, www, a preview URL, etc.).
  • getSubdomainFromHost(host) / isValidTenantId(subdomain) — supporting helpers used by the middleware. isValidTenantId uses hasOwnProperty, so inherited keys (__proto__, constructor, …) are never mistaken for valid tenants.

middleware.ts — Request interception & URL rewriting

Resolves the tenant and rewrites incoming URLs so the resolved tenant is always the first path segment. See middleware.ts.

  1. Subdomain first. If the host header has a valid tenant subdomain relative to ROOT_DOMAIN (e.g. siteA.localhost:3000), rewrite to /siteA/.... This is the path the Visual Builder uses, since a Makeswift host URL cannot contain a path.
  2. Path fallback. Otherwise, if the first path segment is already a valid tenant (e.g. /siteA/products), pass it through unchanged so the catch-all route reads the tenant from there.
  3. Default. Otherwise — the bare root domain or any unrecognized host (e.g. www, a platform preview URL) — prepend the default tenant, so the page route never throws.
  4. Skips Makeswift API routes and static files via config.matcher (the API handler resolves the tenant from the host itself).

Examples (with ROOT_DOMAIN=localhost):

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

app/[[...path]]/page.tsx — Main page handler

This catch-all route renders Makeswift pages for the appropriate tenant. See app/[[...path]]/page.tsx.

  1. Reads the tenant id from the first path segment (inserted/validated by middleware).
  2. Reconstructs the Makeswift path by removing that segment.
  3. Creates a tenant-specific Makeswift client via getApiKey(subdomain).
  4. Fetches the page snapshot for the requested path from the correct site.
  5. Renders the page, or returns a 404 if no snapshot is found.

app/api/makeswift/[...makeswift]/route.ts — Makeswift API handler

Handles Makeswift's draft mode and preview functionality so you can use the builder. These requests are excluded from the middleware, so the handler resolves the tenant from the host itself via getTenantFromHost — the builder always connects via a subdomain host URL. See app/api/makeswift/[...makeswift]/route.ts.

Key Benefits

  • Unified codebase, multiple sites — a single deployment serves any number of tenant sites, with one set of components registered once and available to all tenants.
  • Content isolation — each tenant's pages and assets live in a separate Makeswift site, isolated at the API-key level, while sharing the same component library.
  • Flexible public URLs — public visitors can reach a tenant by path (example.com/siteA) with no per-tenant DNS, while the builder still connects by subdomain.
  • Scales horizontally — the same infrastructure handles any number of tenants from a single hosting environment.

Adding a New Tenant

Adding a tenant is mostly configuration, plus a small wiring change in two files:

  1. Add environment variables in .env.local (and your hosting platform):

    SITE_C_SUBDOMAIN=siteC
    SITE_C_MAKESWIFT_SITE_API_KEY=your-new-api-key
  2. Register them in env.ts (both server and runtimeEnv):

    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,
    }
  3. Add the mapping in lib/makeswift/tenants.ts:

    const SUBDOMAIN_TO_API_KEY = {
      // ... existing entries
      [env.SITE_C_SUBDOMAIN]: env.SITE_C_MAKESWIFT_SITE_API_KEY,
    }
  4. Connect the new site's host URL in the Makeswift dashboard (e.g. http://siteC.localhost:3000).

Production Deployment

The same logic works in production with two changes:

  • Set ROOT_DOMAIN to your real domain (e.g. 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 Makeswift builder require each tenant subdomain to resolve. Configure wildcard DNS (*.example.com) or an individual DNS record per tenant so siteA.example.com, siteB.example.com, etc. all reach the same app, and update each Makeswift site's host URL to its production subdomain.

Resolving Subdomains on Safari (Optional)

Path-based public URLs (localhost:3000/siteA) work in every browser without any setup. The subdomain URLs — and therefore the Makeswift builder — need *.localhost to resolve to 127.0.0.1. Chrome, Firefox, and Edge do this automatically, but Safari does not. To use the subdomain URLs in Safari, map them explicitly in /etc/hosts. This keeps ROOT_DOMAIN=localhost, so no other config changes:

  1. Edit /etc/hosts with sudo:

    sudo vim /etc/hosts
  2. Add an entry per subdomain:

    127.0.0.1    siteA.localhost
    127.0.0.1    siteB.localhost
    
  3. Visit http://siteA.localhost:3000, http://siteB.localhost:3000, etc.

Limitations

  • The builder requires a subdomain. A Makeswift host URL is an origin only and cannot contain a path, so each site is configured with a subdomain host URL. Public visitors can still use path-based URLs; the path is mapped to the same tenant.
  • The first path segment is reserved for the resolved tenant, so tenant identifiers cannot collide with real top-level page paths.
  • Adding a tenant requires a small code change to env.ts and lib/makeswift/tenants.ts, in addition to environment variables — see Adding a New Tenant.
  • Browser support for *.localhost varies; Safari requires the /etc/hosts approach for subdomain URLs.
  • Subdomain-only alternative. If you'd rather distinguish tenants purely by subdomain (no tenant prefix in the public path), see the subdomain-based multi-tenant example.

About

Makeswift multi-tenant example (path-based routing)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors