loginos

What is the best authentication solution for Next.js?

loginos teamUpdated 8 min read

Short answer

There is no single best authentication solution for Next.js; the right one depends on your situation. Clerk and Supabase Auth get a prototype or a social-login app running quickly. WorkOS, Auth0 and Clerk suit B2B apps that need organizations and SAML. Better Auth and Auth.js keep users in your own database, and loginos is a hosted OpenID Connect option for many domains that share one user pool.

You are starting a Next.js app, or moving one to the App Router, and every list you find names a different winner. There isn’t one. The right choice depends on who signs in, what they sign in with, whether business customers need SAML, how many domains you run, and who should hold the user records.

Below is a map of the options, a guide by situation, and the App Router details that apply to all of them.

What are the main options?

Next.js authentication comes in two kinds. A library runs inside your app and stores users in your database. A hosted service runs the sign-in pages and stores the accounts for you, and your app talks to it through an SDK or OpenID Connect.

OptionKindWhere accounts liveKnown for
Auth.js (NextAuth.js)Open-source libraryYour database, if you add oneMany OAuth providers, sessions without a database
Better AuthOpen-source library (MIT)Your databasePlugins for organizations, two-factor, passkeys and SSO
ClerkHostedClerkPrebuilt UI components, organizations
Supabase AuthHosted, or self-hostedYour Supabase Postgres databaseRow Level Security, magic links, phone sign-in
Firebase AuthHosted by GoogleFirebaseMobile SDKs, anonymous sign-in
Auth0Hosted by OktaAuth0Enterprise connections, Organizations
WorkOS AuthKitHostedWorkOSSAML and OIDC single sign-on, Directory Sync
KindeHostedKindeOrganizations, permissions, enterprise connections
Your own codeSmall librariesYour databaseFull control and full responsibility

Auth.js, formerly NextAuth.js, became part of Better Auth in September 2025, and its maintainers now recommend Better Auth for new projects. If you write your own, pick maintained pieces: the Next.js docs point to jose and iron-session for sessions, and openid-client handles OpenID Connect. Lucia and the Arctic OAuth library are deprecated.

Which one fits your situation?

A quick prototype

Start with what you already use. Supabase Auth comes with Supabase. Firebase Auth comes with Firebase, but it signs people in in the browser, so server components need Admin SDK session cookies or initializeServerApp. For prebuilt screens on a fresh app, Clerk’s npx -y clerk@latest init writes your keys and the proxy file. With no vendor at all, Better Auth or Auth.js with one GitHub provider needs a short config file.

B2C with social logins

Social logins are buttons like “Sign in with Google”, “Apple” or “GitHub”. Clerk, Supabase Auth, Firebase Auth, Auth0 and Kinde have them built in, and you turn each provider on in a dashboard. With Better Auth or Auth.js, you register an OAuth app with each provider and keep its secret in your environment. Also check what happens when two accounts share an email address. Auth.js, for example, doesn’t merge them unless you set allowDangerousEmailAccountLinking.

B2B with organizations and SAML

Business customers want their staff to sign in through the company’s identity provider, such as Okta or Microsoft Entra ID. That takes SAML (Security Assertion Markup Language), the XML-based single sign-on standard that corporate identity systems speak. Larger customers also ask for SCIM (System for Cross-domain Identity Management), which adds and removes users from their directory.

WorkOS is built around single sign-on and Directory Sync, which keeps users in step with the customer’s directory. Auth0, Clerk and Kinde pair organizations with SAML enterprise connections. Better Auth does the same inside your app with its organization and SSO plugins, and Supabase Auth supports SAML 2.0 on its Pro plan and above. Enterprise features often depend on the plan, so check each vendor’s pricing page.

Many domains or brands

Cookies belong to one domain, so brand-a.com and brand-b.com can’t share a session. Each domain needs its own session, created after one central sign-in. Clerk does this with satellite domains, which need a paid plan in production, and its docs advise against passkeys when sessions are shared across different domains. Auth0 lets one tenant serve several branded login domains on Enterprise plans. With Auth.js, every domain runs the same setup, as NextAuth with multiple domains explains. loginos is built for this case, as shown below.

Owning your user data

If user records must stay in your own database, use a library: Better Auth, or Auth.js with a database adapter. Supabase Auth keeps users in your project’s Postgres database, and Supabase is open source, so you can self-host it. For a complete identity server on your own machines, see simpler alternatives to Keycloak. Hosted services keep the accounts on their side, and your app keeps its own user table keyed by their stable user id.

What changes with the App Router?

The App Router gives authentication three places to run:

  • Route handlers (app/**/route.ts) receive the OAuth callback. They can read the query string, call the token endpoint and set cookies.
  • Proxy (proxy.ts) runs before matching requests. Next.js 16 renamed middleware.ts to proxy.ts and runs it on Node.js by default. Use it for optimistic checks, like redirecting visitors without a session cookie.
  • Server components read the session with await cookies(), which is async since Next.js 15, but they can’t set or delete cookies. Sign-in and sign-out belong in a route handler or a server function.

Check the session again close to the data, for example in a verifySession() function that every query calls, because layouts don’t re-render on every navigation. The Next.js authentication guide shows the pattern. Each option fills the three places differently:

OptionRoute handlerProxySession in a server component
Auth.jsapp/api/auth/[...nextauth]/route.tsexport { auth as proxy }, optionalawait auth()
Better Authapp/api/auth/[...all]/route.tsgetSessionCookie() for a quick checkauth.api.getSession({ headers: await headers() })
ClerkNone neededclerkMiddleware(), requiredawait auth()
Supabase Authapp/auth/callback/route.tsRefreshes tokens with getClaims()supabase.auth.getClaims()
Auth0Mounted by the proxy under /auth/*auth0.middleware(request)auth0.getSession()
WorkOSapp/callback/route.ts with handleAuth()authkitProxy()withAuth()
Kindeapp/api/auth/[kindeAuth]/route.tswithAuthgetKindeServerSession()

Where loginos fits

loginos is a hosted OpenID Connect provider for products on many domains. OpenID Connect (OIDC) is the standard layer on top of OAuth 2.0 that tells your app who signed in. You create a platform in the developer console and add your domains, and all of them share one pool of people.

loginos fits when several domains or brands should share users, with no limit on domains. Passkeys are built in and belong to loginos.io, so they work on every domain without WebAuthn code. Roles and permissions come in every token, and existing users can link their accounts instead of migrating. It is standard OIDC, and loginos has no paid plans today.

loginos doesn’t fit when you need social logins like “Sign in with Google”, or SAML and organizations for B2B customers. It also has no SMS or TOTP codes, no login page on your own domain and no self-hosting, and it doesn’t verify email addresses yet.

Browser
Your server
loginos
1/6Your site sends the browser to loginos with your app key, a state and a PKCE challenge.

In the App Router, the whole integration is two route handlers. The first sends people to loginos with a state and a PKCE challenge. PKCE (Proof Key for Code Exchange) is a one-time secret your server keeps, so a stolen code can’t be redeemed without it. The second route checks state and exchanges the code on your server.

// app/api/auth/login/single/route.ts
import { createHash, randomBytes } from "node:crypto";
import { NextResponse, type NextRequest } from "next/server";

const LOGINOS = "https://loginos.io";
const b64url = (b: Buffer) => b.toString("base64url");

export function GET(req: NextRequest) {
  const state = b64url(randomBytes(16));
  const verifier = b64url(randomBytes(32));
  const url = new URL("/oauth/authorize", LOGINOS);
  url.search = new URLSearchParams({
    client_id: process.env.LOGINOS_APP_KEY!,
    redirect_uri: `${req.nextUrl.origin}/api/auth/login/single/callback`,
    response_type: "code",
    scope: "openid profile email",
    state,
    code_challenge: b64url(createHash("sha256").update(verifier).digest()),
    code_challenge_method: "S256",
  }).toString();
  const res = NextResponse.redirect(url);
  res.cookies.set("lo_flow", `${state}.${verifier}`, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 600 });
  return res;
}

The callback path is the same on every domain, so these two files work on all of them. Your server components then read your own session cookie with await cookies() and never call loginos. The quickstart adds sign-out, and the OpenID Connect reference lists every claim in user.

Auth.js works with loginos too, as a standard OIDC provider next to the usual app/api/auth/[...nextauth]/route.ts:

auth.ts
import NextAuth from "next-auth";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    {
      id: "loginos",
      name: "loginos",
      type: "oidc",
      issuer: "https://loginos.io",
      clientId: process.env.LOGINOS_APP_KEY,
      clientSecret: process.env.LOGINOS_APP_SECRET,
      checks: ["pkce", "state"],
    },
  ],
});

Set the Callback path in the console’s Domains tab to /api/auth/callback/loginos, the path Auth.js uses for a provider with the id loginos. The ID token’s roles and permissions reach the jwt callback as profile. Better Auth’s generic OAuth plugin uses the same callback path and takes https://loginos.io/.well-known/openid-configuration as its discovery URL.

Frequently asked questions

Is NextAuth.js still maintained?

Yes, as Auth.js. The Better Auth team has looked after it since September 2025, with security patches and urgent fixes for existing apps, and the Next.js package still installs as next-auth@beta. For new projects, the maintainers recommend Better Auth.

Does Next.js have built-in authentication?

No. Next.js gives you the building blocks, such as route handlers, server functions, cookies and Proxy, but no sign-in system. Its own docs walk through a password flow for learning, then recommend an auth library for security and simplicity.

Should I check authentication in Next.js middleware?

Only as a first, optimistic check. Proxy, called middleware before Next.js 16, is fine for redirecting visitors without a session cookie, but the Next.js docs say it shouldn’t be your only line of defense. A matcher change can silently remove it from a route, and in March 2025 CVE-2025-29927 let requests with a crafted header skip middleware. Verify the session again in server components, server functions and route handlers.

Do I need a hosted service or a library?

A library like Better Auth or Auth.js keeps everything in your code and database, and you secure and operate it. A hosted service runs the sign-in pages, password storage and enterprise features for you, and you depend on its uptime and plans. A practical rule is to follow your data: Supabase Auth with Supabase, Firebase Auth with Firebase, and a library when the data is in your own Postgres.

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