loginos

How do I make NextAuth work with multiple domains?

loginos teamUpdated 7 min read

Short answer

Leave AUTH_URL and NEXTAUTH_URL unset and turn on trustHost, so Auth.js builds its URLs from the host of each request instead of one fixed domain. Each domain then keeps its own session cookie, and your identity provider must accept the Auth.js callback URL on every domain. With loginos, someone signed in on one domain comes back on the others without a form.

You run one Next.js app on several domains, like app.example.com, example.net and domains your customers connect. NextAuth.js, now called Auth.js, signs people in on one of them and misbehaves on the rest: it sends them back to the wrong host, or stops with an UntrustedHost error. One deployment can serve every domain. Auth.js has to build its URLs from each request, each domain keeps its own session, and your identity provider has to accept a callback on every domain.

How does Auth.js decide which URL to use?

Auth.js needs your app’s own address to build the sign-in redirect, the redirect_uri it sends to the provider, and the page people land on afterwards. Auth.js v5, which you install as next-auth@beta, finds that address in one of three ways:

Your setupAddress Auth.js usesWorks on many domains?
AUTH_URL or NEXTAUTH_URL is setThat URL, for every requestNo: every callback goes to one domain
Neither is set, and the host is trustedThe host of each request, from X-Forwarded-Host or HostYes
Neither is set, the host isn’t trusted, in productionNone: Auth.js stops with UntrustedHostNo

AUTH_URL, and its older name NEXTAUTH_URL, pin everything to one domain: next-auth rewrites the origin of each incoming request to that URL. A sign-in that starts on example.net then tries to come back to app.example.com. For several domains, remove both variables from every environment.

The host counts as trusted when you set trustHost: true in your config or AUTH_TRUST_HOST=true in the environment. Auth.js also trusts it by itself on Vercel and Cloudflare Pages, and whenever NODE_ENV isn’t production. That last rule is why a multi-domain setup can work on your laptop and fail after you deploy.

Trusting the host is safe when your hosting platform or reverse proxy sets these headers itself, instead of passing on whatever a client sends. The Auth.js deployment guide covers the setting. Your provider’s callback allow list is a second check, because a forged host only produces a callback URL that the provider refuses.

On a self-hosted server, fix the route handler URL

Server actions and auth() read the host from the headers, but the Auth.js route handlers use the request’s URL. A self-hosted Next.js server started with next start reports that URL as an internal address, like http://localhost:3000, whatever domain the visitor used. The callback then sends the wrong redirect_uri with the code, and the exchange fails. Rebuild the URL from the forwarded host before Auth.js sees it:

app/api/auth/[...nextauth]/route.ts
import { NextRequest } from "next/server"
import { handlers } from "@/auth"

// Give Auth.js the URL the visitor used, not the server's internal address.
const onPublicHost = (handler: (req: NextRequest) => Promise<Response>) => (req: NextRequest) => {
  const host = req.headers.get("x-forwarded-host") ?? req.headers.get("host")
  const proto = req.headers.get("x-forwarded-proto") ?? "https"
  const { pathname, search } = req.nextUrl
  return handler(new NextRequest(new URL(pathname + search, `${proto}://${host}`), req))
}

export const GET = onPublicHost(handlers.GET)
export const POST = onPublicHost(handlers.POST)

Where the URL is already right, the wrapper changes nothing. On Vercel, for example, X-Forwarded-Host is the same as the Host the visitor used.

Why does each domain get its own session?

Auth.js keeps the session in a cookie named authjs.session-token, or __Secure-authjs.session-token on HTTPS. Auth.js sets that cookie without a Domain attribute, so the browser sends it back only to the exact host that set it. A session on app.example.com is invisible to example.net, because browsers never send one domain’s cookies to another.

So “NextAuth on multiple domains” means that each domain signs the person in on its own. The default redirect callback also only allows redirects to the origin of the current request, so someone who starts on example.net ends on example.net.

What makes this painless is the identity provider’s own session. When the second domain sends someone to the provider, the provider recognizes them and sends them straight back with a code, without a form. The answer on sharing a login session across domains walks through that flow.

Which callback URLs must the provider accept?

Auth.js sends the provider redirect_uri=https://<current host>/api/auth/callback/<provider id>. For a provider with the id loginos, that is https://example.net/api/auth/callback/loginos on example.net. The provider must accept that exact URL on every domain, or it refuses the sign-in.

You have three ways to get there:

  • List every callback URL in the provider’s settings. That’s fine for two or three domains, and a chore once customers bring their own.
  • Use the Auth.js redirect proxy. With AUTH_REDIRECT_PROXY_URL, one stable deployment receives every callback and forwards it to the URL that started the sign-in. Auth.js built it for preview deployments and calls it an option for advanced cases. Every deployment must share the same AUTH_SECRET.
  • Use a provider built for many domains, with an API for adding them and wildcards for subdomains. Wildcards have their own rules: see wildcard redirect URIs.

How loginos does it

loginos is a hosted OpenID Connect provider for one product on many domains. A platform has no limit on domains, and they all share one user pool, so you never maintain a list of callback URLs by hand.

  1. Create a platform in the developer console, with a name and your first domain. Copy the app secret: it is shown once.
  2. In the console, set the platform’s callback path to /api/auth/callback/loginos. The path is the same on every domain, and loginos only redirects to https://<one of your domains><callback path>.
  3. Add your other domains in the console, from your server, as a verified wildcard like *.example.com, or as automatic domains that your server confirms the first time they’re used.
  4. Add loginos to Auth.js as an OpenID Connect provider, and copy roles and permissions from the ID token into the session:
auth.ts
import NextAuth, { type DefaultSession } from "next-auth"

declare module "next-auth" {
  interface Session {
    user: { roles: string[]; permissions: string[] } & DefaultSession["user"]
  }
}

export const { handlers, auth, signIn, signOut } = NextAuth({
  trustHost: true, // use each request's host; don't set AUTH_URL
  providers: [
    {
      id: "loginos", // callback: /api/auth/callback/loginos
      name: "loginos",
      type: "oidc",
      issuer: "https://loginos.io",
      clientId: process.env.LOGINOS_APP_KEY,
      clientSecret: process.env.LOGINOS_APP_SECRET,
      checks: ["pkce", "state"],
    },
  ],
  callbacks: {
    jwt({ token, account, profile }) {
      // Only at sign-in: profile holds the claims of the loginos ID token.
      if (account && profile) {
        token.sub = account.providerAccountId // the loginos sub, the same on every domain
        token.roles = profile.roles
        token.permissions = profile.permissions
      }
      return token
    },
    session({ session, token }) {
      session.user.id = token.sub as string
      session.user.roles = token.roles as string[]
      session.user.permissions = token.permissions as string[]
      return session
    },
  },
})

Auth.js reads the endpoints from https://loginos.io/.well-known/openid-configuration, sends PKCE and state, and checks the ID token. The jwt callback copies the loginos sub on purpose: without a database adapter, Auth.js gives every sign-in a new random user id. Key your own records on session.user.id, never on the email address, because loginos doesn’t verify email addresses yet. Tokens carry the roles someone had when they signed in, and roles and permissions shows how to read the current ones.

What happens on the second domain

Someone who signed in on app.example.com opens example.net for the first time. Auth.js on example.net has no session cookie for them yet.

  1. They click your sign-in button, which calls signIn("loginos") in a server action.
  2. Auth.js redirects to loginos with redirect_uri=https://example.net/api/auth/callback/loginos.
  3. loginos finds its own session and sees that the person already uses your platform, so it sends them straight back with a code. There is no form.
  4. Auth.js exchanges the code on your server and sets the session cookie for example.net.

Auth.js is optional. The quickstart has a Next.js version with two plain route handlers, one that redirects and one callback, on the default callback path. On a self-hosted server, build their callback URL from the forwarded host as well. Plain handlers also leave room for loginos features that need your own requests, like account linking.

Frequently asked questions

Do I need a separate deployment for each domain?

No. One Next.js deployment can answer on every domain you point at it. Remove AUTH_URL and NEXTAUTH_URL, trust the host, and make sure the route handlers see the public URL. Then add each domain to your hosting and to your identity provider.

Can NextAuth share one session between example.com and example.net?

No. Auth.js stores the session in a cookie for the host that set it, and browsers never send one domain’s cookies to another. Each domain gets its own session. A central identity provider turns the second sign-in into a quick redirect instead of a form.

Is trustHost: true safe in production?

Yes, if your hosting platform or reverse proxy sets the Host and X-Forwarded-Host headers itself, instead of passing on what the client sent. Your provider’s callback allow list is a second check: a forged host only produces a callback URL that the provider refuses.

Does this work with NextAuth v4?

Yes. next-auth@latest still installs v4. Leave NEXTAUTH_URL unset and set AUTH_TRUST_HOST=true, and v4 builds its origin from X-Forwarded-Host or Host on every request. The callback path is the same, but v4 writes a custom OpenID Connect provider as type: "oauth" with a wellKnown discovery URL and idToken: true.

Is NextAuth still maintained?

Yes, with a narrower focus. Since September 2025 the Better Auth team maintains Auth.js. Its announcement says existing users can keep using it and will get security patches, and it recommends Better Auth for new projects. Version 5 still ships under the beta tag on npm: 5.0.0-beta.32 came out in July 2026.

One account for every domain you run

loginos is an OpenID Connect provider with passkeys, a user pool shared by all of your domains, roles in every token, and account linking for the users you already have. Add your platform and its domains in a few minutes.

Keep reading

All answers