Skip to Content
Multitenancy

Multitenacy

This article describes how to host multiple tenants/websites, served from different subdomains in a single Next.js application. The back-end and front-end are served as different serverless functions, and the app is implemented as a single Next.js project, making it not only easy to maintain and deploy but also cost-effective, simplifying infrastructure management and reducing hosting costs.

Multitenancy

This guide will walk you through the steps to implement multitenancy in your Next.js application with a little help from Vovk.ts, making possible to serve different areas of your application under different subdomains, illustrated by implementing the following areas, showcasing multiple use-cases:

The live example is available at multitenant.vovk.dev , and the source code is available in the vovk-multitenant-example .

Each tenant has its own root API endpoint, served from the corresponding domain /api path. In other words, API for the customer tenant is served from customer.example.com/api, and API for the admin tenant is served from admin.example.com/api. These root endpoints are implemented as segments and handled by the Next.js middleware to rewrite the request to the appropriate path based on the tenant subdomain.

For the front-end, you’re going to use the built-in Next.js dynamic routes  to handle tenant-specific paths. For example, the specific customer tenant page will have its own page file src/customer/[customer_name]/page.tsx, and the admin tenant will use src/admin/page.tsx.

Vovk.ts provides a tiny routing utility multitenant that accepts request information and returns the action to take, such as redirecting to a specific subdomain or rewriting the request to a different path. It’s going to be used in the Next.js middleware to handle the request and redirect or rewrite it to the appropriate path based on the tenant subdomain.

Configure DNS

In order to set up multitenant.vovk.dev  with the subdomains I’ve used Vercel as a hosting provider. The DNS records are configured as follows:

TypeHostValue
CNAME Record*.multitenantcname.vercel-dns.com.

On the Vercel side, the project domains are configured as follows:

Domain configuration

See Vercel documentation  for more information on configuring domains or check your hosting provider documentation for configuring wildcard subdomains.

On the screenshot the following domains are configured:

  • multitenant.vovk.dev for the root tenant,
  • admin.multitenant.vovk.dev for the admin tenant,
  • customer.multitenant.vovk.dev for the customer tenant,
  • *.customer.multitenant.vovk.dev for a specific customer tenant, like acme.customer.multitenant.vovk.dev, it’s going to share customer tenant API,
  • pro.acme.customer.multitenant.vovk.dev to illustrate multiple subdomains. Vercel has limited support for wildcard subdomains, and acme is used as a placeholder for the customer name to illustrate the idea.

Organize front-end routes

Use Next.js built-in dynamic routes  to handle tenant-specific paths.

    • page.tsx domain: multitenant.vovk.dev, segment: "root"
      • page.tsx domain: admin.multitenant.vovk.dev, segment: "admin"
      • page.tsx domain: customer.multitenant.vovk.dev, segment: "customer"
        • page.tsx domain: *.customer.multitenant.vovk.dev, segment: "customer"
          • page.tsx domain: pro.*.customer.multitenant.vovk.dev, segment: "customer/pro"

Create back-end API segments and controllers

After setting up Vovk.ts application, the easiest way to get started is to use the Vovk.ts CLI to create the segments and controllers. The CLI will generate the necessary files for you, so you can focus on implementing the business logic.

First, create the API segments for each tenant. The segments will handle the requests to the root API endpoints for each tenant.

npx vovk new segment # create the root segment at src/app/api/[[...vovk]]/route.ts npx vovk new segment admin # create "admin" segment at src/app/api/admin/[[...vovk]]/route.ts npx vovk new segment customer # create "customer" segment at src/app/api/customer/[[...vovk]]/route.ts npx vovk new segment customer/pro # create "customer/pro" segment at src/app/api/customer/pro/[[...vovk]]/route.ts

Read more about Vovk.ts segments.

Next, create the controllers for each segment. The controllers will handle the requests to the API endpoints for each tenant.

Create the ProductService and ProductController for the root segment at src/modules/product/ folder:

npx vovk new controller service product

Create the UserService and UserController for the admin segment at src/modules/admin/user/ folder:

npx vovk new controller service admin/user

Read more about Vovk.ts controllers.

Enable segmented client

By default, Vovk.ts uses so-called β€œcomposed client” emitted to node_modules/.vovk-client folder, that in its turn re-exported from "vovk-client" package. The modules import all schemas from .vovk-schema folder, making the entire app schema be visible for an inspection in every front-end module that imports the client.

This problem is solved by introducing β€œsegmented client” that generates a separate client for each segment, allowing you to import only the schemas that are relevant to the segment you’re working on. Each segment will have its own directory in the project folder and the client will import only a schema that is relevant to that segment.

This approach also allows higher order segments (such as β€œadmin”) to import lower order segments (such as β€œcustomer”), making it possible to share the RPC libraries between segments but keep the back-end implementation secure.

Segmented client

In order to do that, you need to optionally disable the composed client (at this case you don’t need the "vovk-client" package) and enable the segmented client in your config file.

vovk.config.mjs
// @ts-check /** @type {import('vovk').VovkConfig} */ const config = { composedClient: { enabled: false, // Disable composed client }, segmentedClient: { enabled: true, // Enable segmented client }, }; export default config;

By default, the segmented client will be generated in the /src/client folder, but you can change the output directory by specifying the outDir option in the segmentedClient configuration.

Once the segmented client is enabled, you can import the client in your front-end code like this:

import { ProductRPC } from '@/client/product'; await ProductRPC.getProducts();

And the file structure of the generated client will look like this:

      • index.tsx segment: root
      • index.tsx segment: admin
      • index.tsx segment: customer
        • index.tsx segment: customer/pro

Update segment configuration

For each segment, you can specify the segmentNameOverride option in the segmentConfig object in your config file. This option allows you to override the default segment name used in the URL path (was "customer/pro", now an empty string "").

vovk.config.mjs
// @ts-check /** @type {import('vovk').VovkConfig} */ const config = { composedClient: { enabled: false, // Disable composed client }, segmentedClient: { enabled: true, // Enable segmented client }, segmentConfig: { // Segment configuration admin: { segmentNameOverride: '', }, customer: { segmentNameOverride: '', }, 'customer/pro': { segmentNameOverride: '', }, }, }; export default config;

Create middleware

Package "vovk" provides a utility function multitenant that plays a role of a router for your multitenant application. It accepts the request information and returns the action that middleware needs to take, such as redirecting to a specific subdomain or rewriting the request to a different path.

The function accepts the following parameters:

  • requestUrl: the URL of the request, including the protocol, host, and path, can be obtained from request.url.
  • requestHost: the host of the request, can be obtained from request.headers.get("host").
  • targetHost: the host to which the request should be redirected or rewritten, can be set to your production domain or localhost:3000 for local development.
  • overrides: an object that maps tenant subdomain domain names to their specific routing rules. Each rule is an array of objects with from and to properties, where from is the path prefix to match and to is the path to which the request should be rewritten or redirected.

When a wildcard subdomain is used, the square bracket pattern such as [customer_name] will be used to provide the Dynamic Segment  and passed with params.

src/middleware.ts
import { NextRequest, NextResponse } from 'next/server'; import { multitenant } from 'vovk'; export default function middleware(request: NextRequest) { const { action, destination, message, subdomains } = multitenant({ requestUrl: request.url, requestHost: request.headers.get('host') ?? '', targetHost: process.env.VERCEL ? 'multitenant.vovk.dev' : 'localhost:3000', overrides: { admin: [ // admin.multitenant.vovk.dev { from: 'api', to: 'api/admin' }, // API { from: '', to: 'admin' }, // UI ], customer: [ // customer.multitenant.vovk.dev { from: 'api', to: 'api/customer' }, // API { from: '', to: 'customer' }, // UI ], '[customer_name].customer': [ // *.customer.multitenant.vovk.dev { from: 'api', to: 'api/customer' }, // API { from: '', to: 'customer/[customer_name]' }, // UI ], 'pro.[customer_name].customer': [ // pro.*.customer.multitenant.vovk.dev { from: 'api', to: 'api/customer/pro' }, // API { from: '', to: 'customer/[customer_name]/pro' }, // UI ], }, }); console.log({ action, destination, message, subdomains, }); if (action === 'rewrite' && destination) { return NextResponse.rewrite(new URL(destination)); } else if (action === 'redirect' && destination) { return NextResponse.redirect(new URL(destination)); } else if (action === 'notfound') { return new NextResponse('Not Found', { status: 404 }); } return NextResponse.next(); } export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico, sitemap.xml, robots.txt (metadata files) * - SVG files */ '/((?!static|.*\\.png|.*\\.svg|.*\\.ico|.well-known|_next/image|_next/static).*)', ], };

Update hosts for local development

Update your /etc/hosts file to support subdomains on localhost.

127.0.0.1 *.localhost
Last updated on