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
- Platforms Starter Kit Demo
- Example of a webpage builder
- Hostname rewrites example
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
Then log into your Namecheap account and add the following Nameserver DNS urls
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
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!
Top comments (12)
My challenge is to host this with AWS rather than Vercel
Hi @iskurbanov
I’m following your guide on implementing a multi-tenant architecture for one of our existing products. However, we have a situation where our client already has separate hosting for domain.com and app.domain.com. Could you let me know if using wildcard subdomains might interfere with these existing domains?
Thanks!
Thanks, @iskurbanov , this post helps me a lot.
I have a multi-tenancy SSG app, how can I add i18n? Can you give me some advice?
Same question here, I've also implemented a multi-tenant app with i18n
next-intl
and it works but I still facing some conflict problems in the app structure, cuz I need to set up localization for the dashboard and landing page only without affecting the dynamic route like[domain]
couple of questions
I've implemented analagous logic with the App dir and it's working perfectly well! If fact your file structure in the App dir will look exactly the same, just create the "site" logic in a
page.tsx
file instead ofindex.tsx
.I also added the following to route both my domain and localhost to another folder such that I could work with that instead of the root domain defualting to the
DEFAULT_HOST
like in this tutorial. Helped with running locally and debugging!Great article !
Do you have any idea on how to set up environnement variable for each sub-site ?
Thanks !
Will adding a prefix help?
For example key is
domain:key
Would it be possible for customers to bring their own custom domain instead of the wildcard ?
Yes of course, checkout the Next.js platform starter kit for example of using custom domains: vercel.com/templates/next.js/platf...
This is cool. Can u explain how can i using multitenancy and pathname both?
e.g.: user1.hosted-url.com/about
Thanks before
Same logic :)
Add the following logic for user1.hosted-url.com/about