Next.js Auth0 with Sub-Domains: Multi-Tenant Setup (Next.js 13)

6 min readnextjstypescriptweb-development

Note, October 2026: this guide was written in October 2023 for Next.js 13 (app folder) and version 3 of @auth0/nextjs-auth0. Version 4 of the SDK changed the API: handleAuth and the /api/auth/[auth0] route are replaced by Auth0Client plus a middleware, initAuth0 is gone, environment variables were renamed (AUTH0_BASE_URL became APP_BASE_URL, AUTH0_ISSUER_BASE_URL became AUTH0_DOMAIN), and auth routes dropped the /api prefix. The code below applies to v3. The ideas still hold: a shared cookie domain for sub-domains, wildcard callback and logout URLs, and checking which middleware writes your session cookie. See the Auth0 v4 migration guide for current code. Next.js 16 also renamed middleware.ts to proxy.ts, see what changed in Next.js 16.3.

Hi guys!

In the article today we'll discuss and also I'll show you how to set up NextJS 13 (with app folder), Auth0 and sub-domains for a multi-tenant Next.js app.

I'm currently working on a web multi-tenancy app. So we need multiple sub-domains. For security reasons, we decided to use Auth0. If you are setting up auth from scratch in 2026, the passkeys in production guide covers what shipping passwordless on top of any of these providers actually looks like. We did follow the basic setup for Auth0 which can be found here. And everything worked smoothly and fine. But, we figured out that, we need the multiple sub-domains. So, I started researching, I got into a lot of trouble with this because every time I entered subdomain.domain.com I got an error. Something about err callback, grant_type etc, I can't reproduce the error now, but the status code was 400.

Anyway, after a lot of research, a colleague of mine, has found that we need to create our sdk. If you followed the basic quickstart from Auth0 you should have something like

typescript
// src/api/auth/[auth0]/route.ts
import { NextResponse, NextRequest } from 'next/server';
import { handleAuth } from '@auth0/nextjs-auth0';

export const GET = (req: NextRequest, res: NextResponse) => {
  return handleAuth()(req, res); // not neccesary to have (req, res)
};

And beside of that in our case, we had a middleware to set the user token from Auth0

You can ignore the first if statement for /api/status as we needed it for other checks in deployments. Is the only route on the application which doesn't require authentification.

typescript
// src/middleware.ts

import { NextRequest, NextResponse } from 'next/server';
import {
  withMiddlewareAuthRequired,
  getSession,
} from '@auth0/nextjs-auth0/edge';

export default withMiddlewareAuthRequired(async (req: NextRequest) => {
  if (req.nextUrl.pathname === '/api/status') {
    return NextResponse.next();
  }

  const res = NextResponse.next();
  const user = await getSession(req, res);
  if (user) {
    // set your user.token whenever you want, db, cookies, localStorage etc
    return res;
  }
  return res;
});
export const config = {
  matcher: '/',
};

So basically with this setup the domain.com login/logout was working fine.

Now, as I mentioned earlier, my colleague found out we have to create our own Auth0 SDK and we have to simulate a domain name.

* The following configuration is for macOS and Linux.

If you use Windows use a Google search such as: how to add sub domains local windows

So, open your terminal and run:

$ sudo nano /etc/host

Here you have to add a root domain and a few subdomains the name will not matter, but the extension yes. So they can be anything but need to end with .local.

typescript
127.0.0.1    cloudstar.local
127.0.0.1    apple.cloudstar.local
127.0.0.1    microsoft.cloudstar.local
127.0.0.1    github.cloudstar.local

After you finish click CTRL + X then press Y and enter

Now you need to flush it, in the terminal run $ sudo killall -HUP mDNSResponder

So now in Auth0 Dashboard at your application settings you'll have to set:

* Please note that before cloudstar.local there's a . (dot)

Allowed callbacks urls to:

http://.cloudstar.local:3000/api/auth/callback

Allowed logout urls to:

http://.cloudstar.local:3000

typescript
// src/lib/auth0.ts
import { initAuth0 } from '@auth0/nextjs-auth0';

export const initializeAuth0 = (req): ReturnType<typeof initAuth0> => {
  return initAuth0({
    baseURL: `http://${req.headers.get('host')}`, 
    secret: process.env.AUTH0_SECRET,
    issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL,
    clientID: process.env.AUTH0_CLIENT_ID,
    clientSecret: process.env.AUTH0_CLIENT_SECRET,
    session: {
      cookie: {
        domain: `.${req.headers
          .get('host')
          .split(':')[0]
          .split('.')
          .slice(-2)
          .join('.')}`,
      },
    },
  });
};

The req.headers.get('host') will return myapp.com or apple.myapp.com etc.

And at the session.cookie.domain we want to set up the domain. But without a port, we need to make sure the cookie will be set for .myapp.com. So basically, we want to be sure we set the cookie at the very top-level domain.

A small tip:

If you want to set HTTP or HTTPS based on development or production you can pass this to the base

typescript
baseURL: `${
      process.env.NODE_ENV === 'development' ? 'http' : 'https'
    }://${req.headers.get('host')}`

We need to modify the API route as well to use the custom Sdk

typescript
// src/api/auth/[auth0]/route.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { initializeAuth0 } from '@/lib/auth0';

export const GET = (req: NextApiRequest, res: NextApiResponse) => { 
    const auth0 = initializeAuth0(req); 
    return auth0.handleAuth()(req, res); 
}

Now if you're trying to access your instance, it has to work to log in but will not work to log out. Why you wonder? So, if you open the dev tools from your browser, and go to application -> cookies. You'll see two cookies:

First with the name appSesion, value, and the domain: myapp.com and the second one with domain: .myapp.com . That's normal for how the project is configured right now.

But if you want to log out will do the refresh I was saying at the start of the article and will log you back in.

And now come the fix:

The issue is middleware withMiddlewareAuthRequired. This middleware is setting the domain cookie aka myapp.com. Honestly I don't know whoever from Auth0 wrote the docs forgot to add that, or it was just ignorance or maybe it is writing somewhere that if you use withMiddlewareAuthRequired at the root level it creates a new cookie. I saw a lot of examples using withMiddlewareAuthRequired(PageName) and I don't know if that formula is still creating the cookie but, at the root level sure it is. Maybe Auth0 guys didn't know about it, who knows, I'm not here to point any fingers at anyone.

Let's get back at the fix. So, you just have to change the name of middleware from withMiddlewareAuthRequired to anything else, let's say middleware and it will work perfectly fine.

I'm also providing a GitHub repo for the final code, to be more concise because here I mostly explained what was the issues and so on, and we didn't focus too much on the code itself.

https://github.com/alexcloudstar/nextjs13-auth0-sub-domains

End note:

I'm pretty sad that, most of the discussions on Auth0 are open, there are a few responses or no responses at all and then the topic is closed. Or in some discussions, folks came and said:

"Oh yeah, I had the same issue, but I managed to fix it" - Yes, I'm glad you did, but how? Because I need a fix as well at that very moment.

Anyway, that's it for today guys!

Many thanks to my colleagues for help, the internet and you guys for reading. If you have any queries please let me know at alexcloudstar@gmail.com or on my social media.

Until next time!

Cheers! 🍻

Frequently Asked

How do I use Auth0 with Next.js sub-domains? Create your own Auth0 SDK instance per request with a baseURL from the request host, set the session cookie domain to the top-level domain (for example .myapp.com), and allow wildcard callback and logout URLs in the Auth0 dashboard.

Why does logout fail on sub-domains? In my case, withMiddlewareAuthRequired at the root level created an extra session cookie on the bare domain, so logout cleared one cookie and the other logged the user back in. Renaming the wrapper and handling the session check yourself fixed it in v3.

Does this still work with the current Auth0 SDK? No, not as written. Version 4 uses Auth0Client and middleware. Read the migration guide linked above.