Skip to content

How to Build a Multi-Tenant App with Custom Domains Using Next.js

In this guide, you'll learn how to build a full-stack multi-tenant application by using the Platforms Starter Kit and the following technologies:

If you already have an existing project and only want to see the steps for multi-tenancy, skip ahead to steps 4 and 5. Also, the code for this app can be found here.

We’ll be using the Platforms Starter Kit to kickstart our Next.js project.

First, open up your terminal and navigate and run the following:

npx create-next-app --example platforms

This will create a new folder in your current directory called platforms. Then, you can navigate into the folder, install the dependencies, and launch the app:

cd platforms && npm i && npm run dev

The new application has the following structure:

│   │   index.tsx
│   │   login.tsx
│   │   settings.tsx
│   │
│   └───post
│   │   │   ...
│   │
│   └───site
│       │   ...
│   │   index.tsx
│       │
│       [site]
│       │   │   index.tsx
│       │   │   [slug].tsx

Aside from the /api folder, there are 3 main folders in the /pages directory:

  • /app: All routes for the app subdomain (, where users can customize their individual content pages.
  • /home: All routes for the landing page (
  • /_sites: All routes for all user content pages (e.g.,

These folders contain the basic app structure for a multi-tenant app. However, only the /home route works for now. Let’s continue by adding our database.

Note: Don't forget to convert the .env.example file that’s located at the root of the repo into a .env file – it'll come in handy later.

  1. Prerequisite: You need to have the PlanetScale CLI installed

  2. Create a new account with PlanetScale.

  3. Using the PlanetScale CLI, create a new database called platforms.

    pscale db create platforms
  4. Next, connect to the database branch:

    pscale connect platforms main --port 3309
  5. In a different terminal window, use the db push command to push the schema defined in prisma/schema.prisma:

    npx prisma db push
  6. Now that the initial schema has been added, promote your main branch to production:

    pscale branch promote platforms main
  7. You've just provisioned your database! Now, when you go to app.localhost:3000, you should see the following screen:

  8. To push additional schema changes to your database, follow the instructions from PlanetScale on Prisma Migrations.

  9. Don't forget to get your production MySQL DATABASE_URL from your Planetscale database – you'll need it for when you deploy your app to Vercel later. You can do that by following this guide. Note that your DATABASE_URL should be in the following format:


Now, let’s add authentication to allow users to create accounts, add new sites, and add custom domains.

We will use the next-auth library for authentication. This example is preconfigured to use GitHub OAuth. All user data is stored in your PlanetScale database, based on the Prisma schema defined.

To set up GitHub for authentication:

  1. Go to Developer Settings on GitHub.

  2. Click on "New GitHub App".

  3. Name your GitHub App. In our example, we'll call it "Platforms Starter Kit (dev)".

  4. Add your homepage URL (or a placeholder, if you don't have a website yet).

  5. For the "Callback URL" field, put http://app.localhost:3000. Since GitHub only allows one callback URL per app, we have to create separate apps for localhost and production (hence the "dev" name in step 3).

  6. If the "Active" field under "Webhook" is checked, uncheck it. Now, click on "Create Github App".

  7. Once your app is created, you should see the following screen. Click on "Generate a new client secret":

  8. Copy the client secret you generated and paste it under the GITHUB_SECRET value in your .env file:

  9. Copy the Client ID and paste it under the GITHUB_ID value in your .env file:

You're all set! You can now go back to the app login page and log in with GitHub.

Vercel Edge Functions give you the benefits of static with the power of dynamic. Inside this template, we use Middleware to create flexible rewrite rules.

First, navigate to the _middleware.js file at the root of your /pages folder. Inside this file, we have set up rewrite rules to map each subdomain/custom domain to their dynamic routes in /pages.

You’ll need to replace all instances of with your own domain to configure rewrites correctly. If you don't have a custom domain, you can add the domain that your project was assigned and use that as your custom domain.

Next, we’ll be deploying the repo to Vercel. Note that this is not the final step since we’ll still need to configure the feature for adding custom domains in the next step.

  1. First, create a new Github repository and push your local changes.
  2. Deploy it to Vercel. Ensure you add all Environment Variables in your .env file to Vercel during the import process.
    • Ensure you create another GitHub App for production environment as well, and then add the production callback URL as an Environment Variable. It should be in the following format: https://app.<YOURDOMAIN.COM>/api/auth/callback/github.
    • Fill in the SECRET token in the .env file and add that as an Environment Variable.
    • Add the production DATABASE_URL you retrieved in step 2 as an Environment Variable.
  3. In your Vercel project, add your root domain & wildcard domain.
    • When adding your custom domain, ignore the recommended step to "add the www. version of your domain and redirect your root domain to it" – just add the root domain.
    • To set up wildcard domains, you'll need to add the domain using the Nameservers method (as opposed to the recommended A records method).

We’ll use the Vercel API to add custom domains to your project and assign it to the user's account:

  1. Get your AUTH_BEARER_TOKEN from your Vercel Accounts Settings page under Tokens. Add that value to your .env file under the AUTH_BEARER_TOKEN key.
  2. Get the VERCEL_PROJECT_ID for your project from<TEAM_OR_USER_NAME>/<PROJECT_SLUG>/settings. It should be in the format prj_xxxxxx...
  3. If you're deploying your project under a team account, you'll also need to get your VERCEL_TEAM_ID – this can be found at<TEAM_SLUG>/settings
  4. We’ve set up a few API routes to help you handle custom domains:
    1. /api/add-domain: Add domains to your Vercel project using this endpoint whenever a user adds it on your platform. This returns 3 possible outcomes:
      1. Status code 403: The domain is already owned by another team but you can still request delegation from the team and add it.
      2. Status code 409: The domain is already being used by a different project. You can’t add it unless the domain is removed from the project.
      3. Status code 200: The domain is successfully added.
    2. /api/check-domain: Checks if a domain has been successfully configured for your project. Returns two possible values:
      1. true: domain has been configured successfully
      2. false: domain has not been configured correctly
    3. /api/request-delegation: If a domain is owned by another team, you can use this endpoint to request delegation from the team and add it. Caveat: we are actively working on improving our domain delegation process and it’s likely that there won’t be the need for this endpoint in the future
    4. /api/remove-domain: Removes domains from your Vercel project using this endpoint whenever a user removes it from your platform.

Note: The /api/add-domain endpoint only adds the root domain. If you want to add the www. subdomain and redirect it to the root, you'll have to perform an additional API call.

Here are some supplementary code snippets that might be required to build Platforms on Vercel:

Avoid [Cumulative Layout Shift](</docs/concepts/analytics/web-vitals#cumulative-layout-shift-(cls)) (CLS) from the native Twitter embed by using our static tweets implementation (supports image, video, gif, poll, retweets, quote retweets, and more).

You'll need a Twitter auth bearer token, which you'll paste into the TWITTER_AUTH_TOKEN field in your .env file. Here's how you can get a Twitter auth bearer token.

Cloudinary is used to handle image uploads. Here’s the reusable component we created and here’s the code we used to generate a blurhash from the uploaded images.

You'll need a Cloudinary cloud account (cloudName) and a Cloudinary upload preset (uploadPreset). Here's how you can get those variables set up.

In this guide, you learned how to build a full-stack multi-tenant application by using the Platforms Starter Kit. From blogging platforms to low-code tools, this starter kit can be a starter kit for a number of different types of applications, we’re excited to see what you build!

If you run into any issues or have any questions about this guide, feel free to raise them on GitHub or drop them in the Next.js Discord.

Couldn't find the guide you need?