This project demonstrates how to implement multi-tenancy in a Next.js application using Makeswift, where different domains (or subdomains) are mapped to different Makeswift sites via their respective API keys.
The multi-tenancy system supports two routing approaches:
siteA.localhost:3000- Required for the Makeswift builder
localhost:3000/siteA- Works for page navigation and public viewing
Both approaches are equivalent for rendering pages, but the Makeswift builder requires subdomain-based URLs to function correctly.
- Detecting the tenant from either the subdomain or the first path segment
- Mapping the tenant identifier to the appropriate Makeswift site API key
- Rewriting URLs to include the tenant identifier in the routing
- Serving tenant-specific content from the correct Makeswift site
Use Subdomain-Based Routing (siteA.localhost:3000) when:
- Working in the Makeswift builder (required)
- Setting up the Makeswift host URL in your site settings
Use Path-Based Routing (localhost:3000/siteA) when:
- Routing to published pages
This multi-tenant architecture provides several advantages:
- Single deployment serves unlimited tenant sites
- One set of components registered once, available to all tenants
- Centralized updates - deploy new features and components to all sites simultaneously
- Reduced maintenance - manage one codebase instead of multiple separate projects
- Complete content separation - each tenant's pages, images, and assets are stored in separate Makeswift sites
- Independent content management - tenants can manage their own content without affecting others
- Shared component library - all tenants use the same components but with their own unique content and branding
- API key-based isolation - content isolation is enforced at the Makeswift API level
- Add new tenants by simply adding environment variables (no code changes required)
- Horizontal scaling - the same infrastructure handles any number of tenants
- Potentially cost efficient - single hosting environment for multiple sites
This file defines and validates the environment variables for each tenant's subdomain and Makeswift API key.
What it does:
- Uses
@t3-oss/env-nextjsfor type-safe environment variables - Validates that each site has both a subdomain and a Makeswift API key
- Exposes the
envobject for use throughout the application
Required Environment Variables:
DEFAULT_MAKESWIFT_SITE_API_KEY=your-default-api-key # Used when no subdomain is present (e.g., "localhost:3000")
SITE_A_SUBDOMAIN=siteA # Just the subdomain part (e.g., "siteA" from "siteA.localhost")
SITE_A_MAKESWIFT_SITE_API_KEY=your-api-key-for-site-a
SITE_B_SUBDOMAIN=siteB # Just the subdomain part (e.g., "siteB" from "siteB.localhost")
SITE_B_MAKESWIFT_SITE_API_KEY=your-api-key-for-site-bThis is the core of the multi-tenancy logic, mapping subdomains to their Makeswift API keys.
The 'default' tenant is used when no subdomain is detected, either from the domain or the path.
To add a new tenant: Simply add new environment variables in env.ts and extend the SUBDOMAIN_TO_API_KEY object.
The middleware intercepts all incoming requests and rewrites URLs to include the tenant identifier. It supports both subdomain-based and path-based routing.
What it does:
- Extracts the subdomain from the
hostheader (e.g.,siteA.localhost:3000βsiteA) - If no subdomain is present:
- Checks if the first path segment is a valid tenant ID
- If valid (e.g.,
/siteA/products), allows the request to continue - If not valid (e.g.,
/products), rewrites to/default/products
- If a subdomain is present:
- Validates the tenant using
isValidTenantId() - Skips rewriting for Makeswift API routes (
/api/makeswift/*) - Rewrites the URL by prepending the subdomain to the pathname
- Validates the tenant using
- Skips static files like favicon.ico and Next.js internal routes
Examples:
- Subdomain:
siteA.localhost:3000/productsβ rewrites to/siteA/products - Path:
localhost:3000/siteA/productsβ no rewrite needed (already includes tenant) - Default:
localhost:3000/productsβ rewrites to/default/products - Makeswift API:
siteA.localhost:3000/api/makeswift/...β no rewrite (API handles subdomain extraction)
This catch-all route handler renders Makeswift pages for the appropriate tenant.
What it does:
- Extracts the subdomain from the first path segment (inserted by middleware)
- Reconstructs the Makeswift path by removing the subdomain from the URL
- Creates a tenant-specific Makeswift client using
getApiKey(subdomain) - Fetches the page snapshot for the requested path from the correct Makeswift site
- Renders the Makeswift page or returns a 404 if not found
Example Flow:
- Middleware rewrote URL to:
/siteA/products pathSegments = ['siteA', 'products']subdomain = 'siteA'makeswiftPath = '/products'- Fetches
/productsfrom Site A's Makeswift instance
This API route handler manages Makeswift's draft mode and preview functionality, allowing you to use the builder. These requests require the subdomain which is defined in the host setting.
What it does:
- Extracts the subdomain from the request headers (e.g.,
siteAfromsiteA.localhost:3000, ordefaultfromlocalhost:3000) - Gets the tenant-specific API key using
getApiKey(subdomain) - Delegates to Makeswift's API handler with the correct API key
- Supports all HTTP methods (GET, POST, OPTIONS) for Makeswift operations
Here's how requests flow through the multi-tenant system:
1. User visits: siteA.localhost:3000/products
β
ββ> Middleware extracts "siteA" from host header
β
ββ> Validates "siteA" is a valid tenant
β
ββ> Rewrites URL to: /siteA/products
β
ββ> Routes to [[...path]]/page.tsx
2. Page Component receives: params.path = ['siteA', 'products']
β
ββ> Extracts tenantId = 'siteA'
β
ββ> Calls getApiKey('siteA') β Gets Site A's API key
β
ββ> Creates Makeswift client with Site A's API key
β
ββ> Fetches page snapshot for '/products' from Site A
β
ββ> Renders the page with tenant-specific content
1. User visits: localhost:3000/siteA/products
β
ββ> Middleware finds no subdomain
β
ββ> Checks first path segment: "siteA"
β
ββ> Validates "siteA" is a valid tenant
β
ββ> No rewrite needed (path already includes tenant)
β
ββ> Routes to [[...path]]/page.tsx
2. Page Component receives: params.path = ['siteA', 'products']
β
ββ> Extracts tenantId = 'siteA'
β
ββ> Calls getApiKey('siteA') β Gets Site A's API key
β
ββ> Creates Makeswift client with Site A's API key
β
ββ> Fetches page snapshot for '/products' from Site A
β
ββ> Renders the page with tenant-specific content
Note: Both routing approaches result in the same internal URL structure (/siteA/products), ensuring consistent page rendering regardless of how the user accesses the site.
To add a new tenant site:
-
Add environment variables in
.env.local(or your hosting platform):SITE_C_SUBDOMAIN=siteC SITE_C_MAKESWIFT_SITE_API_KEY=your-new-api-key
-
Update
env.tsto include the new variables: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
lib/makeswift/tenants.tsto add the mapping:const SUBDOMAIN_TO_API_KEY = { // ... existing entries [env.SITE_C_SUBDOMAIN]: env.SITE_C_MAKESWIFT_SITE_API_KEY, }
-
Install dependencies:
pnpm install
-
Create
.envfile with your tenant configuration:DEFAULT_MAKESWIFT_SITE_API_KEY=your-default-site-key SITE_A_SUBDOMAIN=siteA SITE_A_MAKESWIFT_SITE_API_KEY=your-site-a-key SITE_B_SUBDOMAIN=siteB SITE_B_MAKESWIFT_SITE_API_KEY=your-site-b-key
-
Run the development server:
pnpm dev
-
Test different tenants using either routing approach:
Subdomain-based (required for Makeswift builder):
- Default tenant:
http://localhost:3000 - Site A:
http://siteA.localhost:3000 - Site B:
http://siteB.localhost:3000
Path-based (works for page navigation):
- Default tenant:
http://localhost:3000 - Site A:
http://localhost:3000/siteA - Site B:
http://localhost:3000/siteB
- Default tenant:
If you want to use custom local domains like siteA.local, you'll need to configure /etc/hosts:
-
Edit
/etc/hostswith sudo (e.g., usingvimor your preferred editor)sudo vim /etc/hosts
-
Add entries for each subdomain:
127.0.0.1 siteA.local 127.0.0.1 siteB.local -
Visit your custom domains:
http://siteA.local:3000http://siteB.local:3000