I publish once a week, about anything I find interesting but mainly programming and anime.

Building a Multi-Tenant Web App with Next.js

May 21, 2022

Next.js has excellent built-in support for building multi-tenant apps, allowing every customer to use a subdomain of your application or even bring their own top-level domains.


The basic idea is to rewrite incoming requests to different pages based on the Host header, the best place to do this is in pages/_middleware.ts:

import { NextRequest, NextResponse } from 'next/server'

const OUR_DOMAIN = process.env.NODE_ENV === 'production'
	? ''
    : 'localhost:3000'

export default (req: NextRequest) => {
  const host = req.headers.get('host')
  if (host.endsWith(`.${OUR_DOMAIN}`)) {
    const subdomain = host.replace(`.${OUR_DOMAIN}`, '')
    const url = req.nextUrl.clone()
    url.pathname = `_tenant/${subdomain}${url.pathname}`
    return NextResponse.rewrite(url)

The above code basically:

  1. Check if host is a subdomain
  2. Rewrite to _tenant/$subdomain/$originalPath if it is

The logic for supporting a custom domain is very similar.

you can use *.localhost:3000 to test subdomains in local development, for example egoist.localhost:3000.

What about Remix and SvelteKit

File-system based routing is good in many ways, but it's actually the reason why this feature was not added until recently I suppose, you genuinely don't have complete control over how a URL maps to a page in a framework like Next.js, Remix, or SvelteKit unless they have it built-in or make the routing more flexible.

I'm closely watching SvelteKit cause it seems to offer a degree of flexibility: The hooks API is a great example.

So for now, in order to achieve multi-tenancy in Remix or SvelteKit, you can conditionally render different components in the same route, based on the host header:

import AppHome from './components/AppHome.svelte'
import UserHome from './components/UserHome.svelte'

// Add a page endpoint to get following props
export let subdomain
export let propsForUser
export let propsForApp

{#if subdomain}
   <UserHome {...propsForUser} />
  <AppHome  {...propsForApp}  />

As your project grows, the code will get much more complex and confusing, so I really hope SvelteKit would have this feature by default some day.


Now you have a multi-tenant app, it's easy to issue SSL for custom domains if you're using Vercel, or Cloudflare, the best part about using a PaaS is it usually comes with a CDN by default.

The major differences among the three:
Pricingfirst 100 free, $0.1 each after$20/month, unlimitedfirst 10 free, $0.1 each after
CDNFullProxy, CachingProxy, Caching

I don't know what black magic Vercel is using, but adding domains on Vercel almost always takes effect immediately, while on it's sometimes a struggle, probably because of DNS cache.

Read More

Check out this Vercel Platform Kit for a demonstration.