Skip to content

Multi-tenant with subdomains: Working example, feedback requested (v 4.1) #1985

@gacharles23

Description

@gacharles23

Checklist

  • I have looked into the Readme, Examples, and FAQ and have not found a suitable solution or answer.
  • I have looked into the API documentation and have not found a suitable solution or answer.
  • I have searched the issues and have not found a suitable solution or answer.
  • I have searched the Auth0 Community forums and have not found a suitable solution or answer.
  • I agree to the terms within the Auth0 Code of Conduct.

Describe the problem you'd like to have solved

I'm building a multi-tenant NextJS 15 app with url access as such: client1.myapp.com, client2.myapp.com, etc. I have a version of this app running in NextJS 14 using the 3.5.0 auth0/nextjs-auth0 package. I'm struggling a bit to get the 4.0 auth0-nextjs sdk working.

I've hacked the following together which seems to work, but before I proceed much further, I'd welcome input on whether what I'm doing is off-base. I'm concerned with moving away from the singleton approach used in the examples to instead creating a new Auth0 client instance for each request since the Auth0 config needs to change based on the hostname/subdomain.

Feedback very much appreciated.

// lib/get-auth0-client.ts
import { Auth0Client } from "@auth0/nextjs-auth0/server";
import { getTenantFromHost } from "./tenant-utils";

export function getAuth0ClientForHost(host: string) {
  const tenant = getTenantFromHost(host);
  
  // Determine protocol based on environment
  const isProduction = process.env.NODE_ENV === 'production';
  const protocol = isProduction ? 'https' : 'http';
  
  return new Auth0Client({
    domain: process.env.AUTH0_DOMAIN!,
    clientId: process.env.AUTH0_CLIENT_ID!,
    clientSecret: process.env.AUTH0_CLIENT_SECRET!,
    secret: process.env.AUTH0_SECRET!,
    appBaseUrl: `${protocol}://${host}`,
    authorizationParameters: tenant?.orgId ? {
      organization: tenant.orgId
    } : undefined
  });
}


// lib/tenant-utils.ts
export type Tenant = {
  name: string;
  orgId: string;
}

// static here for POC; will eventually move to postgres
const tenants: Record<string, Tenant> = {
  'client1': {
    name: 'Client 1',
    orgId: process.env.AUTH0_CLIENT1_ORG_ID!, 
  },
  'client2': {
    name: 'Client 2',
    orgId: process.env.AUTH0_CLIENT2_ORG_ID!, 
  },
};

export function getTenantFromHost(host: string): Tenant | null {
  const subdomain = host.split('.')[0];
  return tenants[subdomain] || null;
}


// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { getAuth0ClientForHost } from "@/lib/get-auth0-client";

export async function middleware(request: NextRequest) {
  const host = request.headers.get('host') || '';
  
  // Get the tenant-specific Auth0 client
  const auth0 = getAuth0ClientForHost(host);
  
  // Process middleware
  const authRes = await auth0.middleware(request);
  
  if (request.nextUrl.pathname.startsWith('/auth')) {
    return authRes;
  }
  
  // For protected routes, check if user is authenticated
  if (request.nextUrl.pathname.startsWith('/dashboard') || 
      request.nextUrl.pathname.startsWith('/profile')) {
    const session = await auth0.getSession(request);
    
    if (!session) {
      const loginUrl = new URL('/auth/login', request.url);
      loginUrl.searchParams.set('returnTo', request.nextUrl.pathname);
      return NextResponse.redirect(loginUrl);
    }
  }
  
  // Return the auth response to ensure cookies are handled
  return authRes;
}

export const config = {
  matcher: [
    "/((?!_next/static|_next/image|_next/media|favicon.ico|sitemap.xml|robots.txt).*)",
  ],
};


// app/page.tsx
import { headers } from "next/headers";
import { getTenantFromHost } from "@/lib/tenant-utils";
import { getAuth0ClientForHost } from "@/lib/get-auth0-client";

export default async function Home() {
  const headersList = await headers();
  const host = headersList.get("host") || "";
  const tenant = getTenantFromHost(host);

  const auth0 = getAuth0ClientForHost(host);
  const session = await auth0.getSession();
  
  return (
    <main>
      <h1>Welcome to {tenant?.name || "Default"}</h1>
      
      {session ? (
        <>
          <p>Logged in as: {session.user.name}</p>
          <p>Organization: {session.user.org_id}</p>
          <a href="/auth/logout">Logout</a>
        </>
      ) : (
        <>
          <a href="/auth/login">Login</a>
        </>
      )}
      
      <div>
        <p>Clients:</p>
        <ul>
          <li><a href="http://client1.localtest.me:3000">client1.localtest.me:3000</a></li>
          <li><a href="http://client2.localtest.me:3000">client2.localtest.me:3000</a></li>
        </ul>
      </div>
    </main>
  );
}

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // resolve issue with WebSocket connection to subdomains causing webpack-hmr failure
  allowedDevOrigins: ['localtest.me', '*.localtest.me'],
};

export default nextConfig;

With this logic in place, within the Auth0 dashboard the following configs seem to work exactly as expected:

  • Allowed Callback URLs: http://{organization_name}.localtest.me:3000/auth/callback
  • Allowed Logout URLs: http://*.localtest.me:3000

So, as I say, this works as desired, but I'd really like some guidance if I'm off-the-mark with regard to this multi-tenant architecture for Auth0 v4.

Describe the ideal solution

Since this is a feature request post, the request would be for a reference architecture for multi-tenant subdomain documentation.

Alternatives and current workarounds

No response

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions