For most Next.js apps, pick a library rather than writing auth yourself. Clerk is hosted and quickest to set up, Supabase Auth suits apps already on Supabase, and Better Auth or Auth.js keep users in your own database. Whichever you choose, check the session again in server code, because proxy checks alone do not protect data.
| Option | Users stored | Setup in Next.js |
|---|---|---|
| Clerk | Clerk's servers | @clerk/nextjs, ClerkProvider, clerkMiddleware in proxy.ts |
| Supabase Auth | Your Supabase project | @supabase/ssr clients plus a proxy that refreshes the session |
| Better Auth | Your own database | auth instance plus app/api/auth/[...all]/route.ts |
| Auth.js (next-auth v5 beta) | Your database or a JWT cookie | auth.ts plus app/api/auth/[...nextauth]/route.ts |
| Roll your own | Your own database | jose or iron-session, httpOnly cookies, a data access layer |
Updated
The official Next.js guide lists twelve compatible auth libraries, which is more choice than most people want. This comparison narrows it to the five routes most teams actually weigh, shows the minimum code for each in the App Router, and explains the checks you still owe your app no matter which library signs people in.
Tick the requirements that matter and the list keeps only the options that meet all of them.
Install @clerk/nextjs and set NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY. On Next.js 16 the middleware file is proxy.ts.
// proxy.ts
import { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware();
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };
// app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body><ClerkProvider>{children}</ClerkProvider></body>
</html>
);
}
// any Server Component or Route Handler
import { auth } from "@clerk/nextjs/server";
const { userId } = await auth();A server client built per request from the cookie store, using the publishable key. getClaims() verifies the token before you trust it.
// lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (list) => {
try {
list.forEach(({ name, value, options }) => cookieStore.set(name, value, options));
} catch {
// Called from a Server Component; the proxy refreshes the session instead.
}
},
},
},
);
}
// app/account/page.tsx
const supabase = await createClient();
const { data } = await supabase.auth.getClaims();
if (!data?.claims) redirect("/login");Mount the handler on a catch-all route, then read the session on the server with the request headers.
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);
// app/dashboard/page.tsx
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
export default async function Dashboard() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) redirect("/sign-in");
return <h1>Hello {session.user.name}</h1>;
}auth.ts exports handlers, auth, signIn and signOut. Set AUTH_SECRET (npx auth secret creates one).
// auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [GitHub] });
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
// app/page.tsx
import { auth } from "@/auth";
const session = await auth();A short version of the pattern in the Next.js guide: jose signs the payload and the cookie is HttpOnly, Secure and SameSite=Lax.
// lib/session.ts
import "server-only";
import { SignJWT, jwtVerify } from "jose";
import { cookies } from "next/headers";
const key = new TextEncoder().encode(process.env.SESSION_SECRET);
export async function createSession(userId: string) {
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const token = await new SignJWT({ userId })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("7d")
.sign(key);
(await cookies()).set("session", token, {
httpOnly: true, secure: true, sameSite: "lax", path: "/", expires,
});
}
export async function readSession() {
const token = (await cookies()).get("session")?.value;
if (!token) return null;
try {
const { payload } = await jwtVerify(token, key, { algorithms: ["HS256"] });
return payload as { userId: string };
} catch {
return null;
}
}| Place | Check type | Why |
|---|---|---|
| proxy.ts | Optimistic, cookie only | Fast redirects; runs before rendering, also on prefetches |
| Data access layer | Secure | One verifySession() every data read goes through |
| Server Actions | Secure | Treated like public endpoints; check role before mutating |
| Route Handlers | Secure | 401 when signed out, 403 when the role is wrong |
| Layouts | Avoid | Not re-rendered on navigation, so checks can be skipped |
Clerk runs the user store, sign-in pages and session handling for you, and you drop its components into your layout. You trade some control and a monthly bill at scale for speed and a polished UI on day one.
Better Auth and Auth.js run inside your app and write to your database. You own the data and the screens, and you also own email delivery, password reset flows and upgrades.
Supabase Auth ties sign-in directly to Postgres row level security, so a policy like auth.uid() = user_id protects data at the database layer. The @supabase/ssr package stores the session in cookies so Server Components can read it.
Supabase's docs warn that getSession() returns unverified data. Use getClaims(), which checks the token signature, or getUser(), which asks the auth server, before trusting who the user is.
The Next.js guide walks through it with a Server Action for the form, Zod for validation, bcrypt for hashing and jose to sign a seven day session cookie. It is a good way to learn the moving parts.
For production, the same guide recommends a library, because rate limiting, email verification, account linking and multi-factor sign-in add up quickly.
Auth.js, the project formerly called NextAuth, says on its own site that it is now part of Better Auth. Its Next.js install command is still npm install next-auth@beta. New projects often start on Better Auth directly, while existing Auth.js apps keep working.
No. The Next.js docs call proxy checks optimistic: good for fast redirects based on a cookie, but not a security boundary. Verify the session again close to the data, because Server Functions and routes outside the matcher never pass through proxy.
Libraries that store users in your own database, like Better Auth or Auth.js, have no per-user fee; you pay for your database and email sending. Supabase Auth includes 50,000 monthly active users on the free plan as of October 2026. Check Clerk's pricing page for its current free allowance.
Yes. Clerk and Supabase both document proxy.ts for Next.js 16 and middleware.ts for older versions, and Better Auth provides a cookie helper for proxy plus a full session check for server code.
A signed cookie (stateless) is simpler and needs no lookup, but you cannot revoke it before it expires. A database session costs a query per check and lets users sign out of every device. Many libraries let you choose.
Related: Next.js proxy (middleware) examples, Next.js Route Handler examples, Supabase vs Firebase, Secret and API key generator, React Server Components.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.