4

See my exact error screenshots at the end.

Context:

I developed a NextJs Web App with Clerk Auth and It worked fine! I later decided to add Multitenancy by using Next Multitenancy Template which used a middleware to look for domain name changes, also the template used Next Auth. So, I merged my previous Web App with it. I left next auth for the multitenancy admin system and want to use clerk auth for the tenants and so far, there is no conflict there.

Now, my clerk auth middleware and multitenancy middleware are at a conflict.

Previous App Directory

enter image description here

App Directory after merging

enter image description here

I copied all my app files from my original app inside the [domain] directory of the multitenant template and resolved all dependency issues, components, hooks, libs to the best of my knowledge.

My middleware code :


import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";


// // Developer Message 2 : This is for Clerk support. To Further Check Proceed To Link : https://clerk.com/docs/quickstarts/nextjs
// // After this (auth) folder was created in app
// //Below is Developer Message 3

import { authMiddleware } from "@clerk/nextjs";
 
// This example protects all routes including api/trpc routes
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your middleware

export default authMiddleware({

      // Developer Message 3
      //Public Routes Enable Unauthorised users to visit below mentioned routes 

      publicRoutes: ["/","/api/webhook"]

});



export const config = {
  matcher: [
    /*
     * Match all paths except for:
     * 1. /api routes
     * 2. /_next (Next.js internals)
     * 3. /_static (inside /public)
     * 4. all root files inside /public (e.g. /favicon.ico)
     */
    '/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)',  // from clerk auth
    "/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)", //from multitenancy template
  ],
};

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

  // Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3000)
  const hostname = req.headers
    .get("host")!
    .replace(".localhost:3000", `.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`);

  const searchParams = req.nextUrl.searchParams.toString();
  // Get the pathname of the request (e.g. /, /about, /blog/first-post)
  const path = `${url.pathname}${
    searchParams.length > 0 ? `?${searchParams}` : ""
  }`;

  // rewrites for app pages
  if (hostname == `app.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`) {
    const session = await getToken({ req });
    if (!session && path !== "/login") {
      return NextResponse.redirect(new URL("/login", req.url));
    } else if (session && path == "/login") {
      return NextResponse.redirect(new URL("/", req.url));
    }
    return NextResponse.rewrite(
      new URL(`/app${path === "/" ? "" : path}`, req.url),
    );
  }

  // special case for `vercel.pub` domain
  if (hostname === "vercel.pub") {
    return NextResponse.redirect(
      "https://vercel.com/blog/platforms-starter-kit",
    );
  }

  // rewrite root application to `/home` folder
  if (
    hostname === "localhost:3000" ||
    hostname === process.env.NEXT_PUBLIC_ROOT_DOMAIN
  ) {
    return NextResponse.rewrite(
      new URL(`/home${path === "/" ? "" : path}`, req.url),
    );
  }

  // rewrite everything else to `/[domain]/[slug] dynamic route
  return NextResponse.rewrite(new URL(`/${hostname}${path}`, req.url));
}

I've placed both middleware code in the same but two defaults cannot exist together and so I tried to export the multitenancy middleware() without the default keyword and it seemingly worked. I also combined both configs.

I'm added tags enveloping the html tags so that authentication work on the whole app. I also experimented with placing tags inside the [domain] directory app layout to only make it work for tenants, but the error did not go away.

Whenever I sign in through the clerk auth and I am supposed to land on dashboard then the following error occurs

enter image description here

enter image description here

I think my middleware is causing the issue and I'm out of ideas.

3
  • I would suggest checking out this Docs for doing additional Middleware with Clerk's if you get stuck doing that come to our Discord :) clerk.com/docs/references/nextjs/… Commented Nov 15, 2023 at 17:19
  • 2
    Hey Jacob, does clerk support authentication for sub-domains and custom domains as well? I can't get it figured out if it does or not? Much importantly can it work together with another auth services such that it's AuthProvider is nested inside of the other authprovider? Commented Dec 5, 2023 at 23:11
  • Interesting. I dont see why it couldnt work with another Auth provider technically speaking. As for subdomains AND custom domains, yes I believe so... I would need to find the docs. You can also join clerk.com/discord for more direct communication Commented Dec 7, 2023 at 3:39

2 Answers 2

7

I did something like this, and its working

import { authMiddleware, redirectToSignIn } from "@clerk/nextjs";
import { NextResponse } from "next/server";

// This example protects all routes including api/trpc routes
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
export default authMiddleware({
  publicRoutes: ["/"],
  afterAuth: (auth, req) => {
    const url = req.nextUrl;

    // Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3123)
    // biome-ignore lint/style/noNonNullAssertion: <explanation>
    let hostname = req.headers.get("host")!.replace(".localhost:3123", `.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`);

    // special case for Vercel preview deployment URLs
    if (hostname.includes("---") && hostname.endsWith(`.${process.env.NEXT_PUBLIC_VERCEL_DEPLOYMENT_SUFFIX}`)) {
      hostname = `${hostname.split("---")[0]}.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`;
    }

    const searchParams = req.nextUrl.searchParams.toString();
    // Get the pathname of the request (e.g. /, /about, /blog/first-post)
    const path = `${url.pathname}${searchParams.length > 0 ? `?${searchParams}` : ""}`;

    // rewrites for app pages
    if (hostname === `app.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`) {
      if (!auth.userId && !auth.isPublicRoute) {
        const prefix = process.env.NODE_ENV === "development" ? "http://" : "https://";
        return redirectToSignIn({ returnBackUrl: `${prefix}${hostname}/` });
      }

      return NextResponse.rewrite(new URL(`/app${path === "/" ? "" : path}`, req.url));
    }

    // special case for `vercel.pub` domain
    //   if (hostname === 'vercel.pub') {
    //     return NextResponse.redirect(
    //       'https://vercel.com/blog/platforms-starter-kit',
    //     );
    //   }

    // rewrite root application to `/home` folder
    if (hostname === "localhost:3123" || hostname === process.env.NEXT_PUBLIC_ROOT_DOMAIN) {
      return NextResponse.rewrite(new URL(`/home${path === "/" ? "" : path}`, req.url));
    }
    // console.log("here");

    // rewrite everything else to `/[domain]/[slug] dynamic route
    return NextResponse.rewrite(new URL(`/${hostname}${path}`, req.url));
  },
});

export const config = {
  matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};
Sign up to request clarification or add additional context in comments.

6 Comments

Great Work Man! Looks like you changed the whitelabel platform from next-auth to clerk auth as well. I would like to do that to. Do you have a repository for this? This also works in the case of custom domains, right?
@ZainKhalid Yes, this is from their guide for multi-tenant / platform examples. and yes this works for custom domains too, feel free to adjust for your use case. Currently Im not open sourcing this, maybe I will create example app for this
Good Work! A lot of people are struggling with this problem. If you can create an example app for this then we'll appreciate that a lot.
Would also love to see this. I'm trying doing the same. I also wanna Clerk instead of Next.auth
I get an cross-origin-referrer Error when I use this code for my middleware. Am I doing something wrong?
|
0
https://dev.to/iskurbanov/step-by-step-multi-tenant-app-with-nextjs-2mbc
https://clerk.com/docs/references/nextjs/auth-middleware

I tried these resources earlier and by following the above answer, came up with the following solution.

import { authMiddleware } from "@clerk/nextjs";
import { NextResponse } from "next/server";
import { getHostnameDataOrDefault } from "./lib/db";

export const config = {
  //ignore sign-in, sign-up from middleware re-write
  matcher: ["/((?!.+\\.[\\w]+$|_next|sign-in|sign-up).*)", "/dashboard", "/(api|trpc)(.*)", "/tenant_/:path*"],
};

export default authMiddleware({
  //define public routes for clerk
  publicRoutes: ["/", "/about-us", "/contact-us", "/join", "(/tenant_/.*/)", "(/tenant_/.+/about-us)", "(/tenant_/.+/contact-us)", "(/tenant_/.+/join)"],

  beforeAuth: async (req) => {
    const url = req.nextUrl;
    console.log("after auth _____________");

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

    const originalPathname = req.nextUrl.pathname;

    // 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(`.example.com`, "") : hostname.replace(`.localhost:3000`, "");

    const data = await getHostnameDataOrDefault(currentHost);

    // Prevent security issues – users should not be able to canonically access the pages/tenant_ folder and its respective contents.
    if (url.pathname.startsWith(`/tenant_`)) {
      url.pathname = `/404`;
    } else {
      // rewrite to the current subdomain under the pages/sites folder
      url.pathname = `/tenant_/${data?.subdomain}${url.pathname}`;
    }

    console.log(`Pathname ↩   ${originalPathname}  ⏩   ${url.pathname}`);

    return NextResponse.rewrite(url);
  },
});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.