Step by step: Multi-Tenant App with Next.js
Tags: nextjs, react, webdev, beginners
Published: 2023-03-01 23:23:35

Next.js now allows you to easily create a multi-tenant application using subdomains. This enables you to create web apps like Linktree, Super.so, and other apps where a user gets their own webpage for example. 
 

Before we start, here are some additional resources:
- [Platforms Starter Kit Tutorial](https://vercel.com/guides/nextjs-multi-tenant-application)
- [Platforms Starter Kit Demo](https://demo.vercel.pub/platforms-starter-kit)
- [Example of a webpage builder](https://github.com/vercel/static-fun)
- [Hostname rewrites example](https://github.com/vercel/examples/tree/main/edge-middleware/hostname-rewrites)


## Step 1: Create a blank Next.js app

`npx create-next-app`

You will be asked if you want Typescript, ESLint and other options. Hit yes for everything. 

Once the app is created, open it in your code editor (VSCode).

## Step 2: Deploy app to Vercel

You can do inside VSCode by opening the terminal (Command + J on Mac). 

Install the Vercel CLI:
`npm i -g vercel`

Once that is done, go ahead and run:
`vercel --prod` to deploy it to production

Click on the deployment link to end up in the Vercel dashboard. 


## Step 3: Setup your domain

In order to use the subdomain feature in Vercel, you will need to setup your own wildcard domain. If you are a developer, you should have plenty of unused domains ;) 

Mine is called buildwithnext.com

I am using Namecheap so here is how you do it:

In the Vercel dashboard add your wildcard domain
![Adding a wildcard domain in Vercel](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lhmzs7zasqscu2twm1b6.png)

Then log into your Namecheap account and add the following Nameserver DNS urls

![Set up custom Nameserver DNS in Namecheap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rfacj2822saebkyqb4ok.png)

_When you add a wildcard domain, Vercel will automatically populate all the other ones for you._


## Step 4: Setting up routing in Next.js

Add a new folder in the pages folder called `_sites` and then another one in there called `[site]`. Then add an index.tsx file in the `[site]` folder. 

Your new folder structure should now look like this:

```
pages
└───api
└───sites
│      │
│    [site]
│      │  index.tsx
package.json
etc
```

Inside the index.tsx file, add the following code:

```
import { useRouter } from "next/router";
// we will create these in the next step
import { getHostnameDataBySubdomain, getSubdomainPaths } from "@/lib/db";

// Our types for the site data
export interface Props {
  name: String
  description: String
  subdomain: String
  customDomain: String
}

export default function Index(props: Props) {
  const router = useRouter()

  if (router.isFallback) {
    return (
      <>
        <p>
          Loading...
        </p>
      </>
    )
  }

  return (
    <>
      <h1>
        {props.name}
      </h1>
    </>
  )
}

// Getting the paths for all the subdomains in our database
export async function getStaticPaths() {
  const paths = await getSubdomainPaths()

  return {
    paths,
    fallback: true
  }
}

// Getting data to display on each custom subdomain
export async function getStaticProps({ params: { site } }) {
  const sites = await getHostnameDataBySubdomain(site)

  return {
    props: sites,
    revalidate: 3600
  }
}
```


## Step 5: Adding Middleware and mock data

A middleware file allows us to intercept the api calls to our backend and do something with it. In this case we are using the middleware to determine what hostname the api call is being made for and display the correct data. 

Let's create a `middleware.ts` file in the root of our project directory

```
pages
└───api
└───sites
│      │
│    [site]
│      │  index.tsx
middleware.ts
package.json
etc
```

Inside of the middleware.ts file, add this code:

```
import { NextRequest, NextResponse } from 'next/server'
import { getHostnameDataOrDefault } from './lib/db'

export const config = {
  matcher: ['/', '/about', '/_sites/:path'],
}

export default async function middleware(req: NextRequest) {
  const url = req.nextUrl

  // Get hostname (e.g. vercel.com, test.vercel.app, etc.)
  const hostname = req.headers.get('host')

  // If localhost, assign the host value manually
  // If prod, get the custom domain/subdomain value by removing the root URL
  // (in the case of "subdomain-3.localhost:3000", "localhost:3000" is the root URL)
  // process.env.NODE_ENV === "production" indicates that the app is deployed to a production environment
  // process.env.VERCEL === "1" indicates that the app is deployed on Vercel
  const currentHost =
    process.env.NODE_ENV === "production" && process.env.VERCEL === "1"
      ? hostname
        .replace(`.buildwithnext.com`, "")
      : hostname.replace(`.localhost:3000`, "");


      
  const data = await getHostnameDataOrDefault(currentHost)

  // Prevent security issues – users should not be able to canonically access
  // the pages/sites folder and its respective contents.
  if (url.pathname.startsWith(`/_sites`)) {
    url.pathname = `/404`
  } else {
    // console.log('URL 2', req.nextUrl.href)
    // rewrite to the current subdomain under the pages/sites folder
    url.pathname = `/_sites/${data.subdomain}${url.pathname}`
  }

  return NextResponse.rewrite(url)
}
```

There is a lot happening here but I tried to add more comments to clear things up. It might look scary at first but try going line by line and reading the comments. It will eventually make sense. 


Now let's go ahead and add a lib folder in the root of our project directory and a db.ts file inside of it. 

```
lib
└───db.ts
pages
└───api
└───sites
│      │
│    [site]
│      │  index.tsx
middleware.ts
package.json
etc
```

Inside of the db.ts file, add the following code:

```
// Dummy data to be replaced with your database
const hostnamesDB = [
  {
    name: 'This is Site 1',
    description: 'Subdomain + custom domain',
    subdomain: 'test1',
    customDomain: 'custom-domain-1.com',
    // Default subdomain for Preview deployments and for local development
    defaultForPreview: true,
  },
  {
    name: 'This is Site 2',
    description: 'Subdomain only',
    subdomain: 'test2',
  },
  {
    name: 'This is Site 3',
    description: 'Subdomain only',
    subdomain: 'test3',
  },
]
const DEFAULT_HOST = hostnamesDB.find((h) => h.defaultForPreview)

/**
 * Returns the data of the hostname based on its subdomain or custom domain
 * or the default host if there's no match.
 *
 * This method is used by middleware.ts
 */
export async function getHostnameDataOrDefault(
  subdomainOrCustomDomain?: string
) {
  if (!subdomainOrCustomDomain) return DEFAULT_HOST

  // check if site is a custom domain or a subdomain
  const customDomain = subdomainOrCustomDomain.includes('.')

  // fetch data from mock database using the site value as the key
  return (
    hostnamesDB.find((item) =>
      customDomain
        ? item.customDomain === subdomainOrCustomDomain
        : item.subdomain === subdomainOrCustomDomain
    ) ?? DEFAULT_HOST
  )
}

/**
 * Returns the data of the hostname based on its subdomain.
 *
 * This method is used by pages under middleware.ts
 */
export async function getHostnameDataBySubdomain(subdomain: string) {
  return hostnamesDB.find((item) => item.subdomain === subdomain)
}

/**
 * Returns the paths for `getStaticPaths` based on the subdomain of every
 * available hostname.
 */
export async function getSubdomainPaths() {
  // get all sites that have subdomains set up
  const subdomains = hostnamesDB.filter((item) => item.subdomain)

  // build paths for each of the sites in the previous two lists
  return subdomains.map((item) => {
    return { params: { site: item.subdomain } }
  })
}

export default hostnamesDB
```

This one is pretty self explanatory. We basically have some dummy data that we use to get the subdomain and the data to display on the frontend. 

Again, go line by line and read the comments.

## Step 6: Test the app and deploy

We can now run `npm run dev` to test the app. 

Navigate to localhost:3000 (make sure you are running on localhost:3000 because that's what we put in the middleware.ts file).

Now you can try to go to `test2.localhost:3000`. The content should change to `This is Site 2`


It should work the same way if you deploy your application now! `vercel --prod`


![Next.js app running a multi-tenant architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jx7x926ckrdgzviivwpy.png)


## Step 7: Next Steps

You can now replace the dummy data with a database such as PlanetScale and use the Prisma ORM to have your users setup their own subdomain and add data to their sites! I'll be adding more tutorials on how to do that so stay tuned. 


I hope this article was helpful!