
Next.js Authentication Guide 2026 - Part 2
Part 2 of 2. Start with Next.js Authentication Guide 2026.
Next.js authentication gets harder after launch. Session checks turn into a per-request budget, tenancy has to exist in the schema before the first B2B customer asks for it, and the provider choice gets expensive to reverse. This part covers scaling, Clerk with Supabase Row Level Security, and a comparison of 8 providers on setup time, pricing, tenancy, and App Router support.
Scaling Next.js authentication
The failure modes below show up the week traffic triples.
Session validation becomes the request budget
If validating a session means querying a database, every authenticated request inherits that latency, and most pages make more than one such check. Signed-token verification is a local signature operation once the key set is cached, so it stays flat as traffic grows.
That makes the JWT-versus-database-session fork a scale decision, not just a security one. Clerk's short-lived tokens verify against cached keys, so the per-request cost doesn't change between a hundred users and a million.
Database connections run out before CPU does
Session storage in Postgres has a hard ceiling that's easy to miss. The PostgreSQL documentation states that for max_connections, "the default is typically 100 connections, but might be less if your kernel settings will not support it." Each Next.js instance keeps its own pool, so a handful of instances at 20 connections apiece exhausts a default-configured database. It shows up as connection errors under load.
You can solve this with a connection pooler such as PgBouncer, and plenty of teams do. Stateless token verification sidesteps the problem entirely by not touching the database on the authentication path at all.
Multi-tenancy has to be designed, not retrofitted
B2B applications need tenant isolation, per-organization roles, invitations, and eventually per-tenant SSO and audit trails. Adding that model after launch means backfilling a tenant identifier through every table and every query, and re-auditing every endpoint.
Clerk's Organizations provide the tenant primitive directly: memberships, invitations, organization switching, roles, and permissions, with the active orgId available from auth() on the server. Whatever you choose, decide early, because the cost of the decision is almost entirely in when you make it.
Revocation, rate limiting, and abuse are distributed problems
Once you run more than one instance, signing a user out everywhere means invalidating state that several processes hold. So does rate limiting a credential-stuffing attempt that spreads its requests across your fleet. Solving these yourself means shared state, cache invalidation, and race conditions in the authentication path, the worst place to debug a race condition.
A managed provider absorbs this: revocation, rate limiting, bot detection, and breached-password checks are enforced centrally, so behavior is the same no matter which instance takes the request.
Grow into custom UI
The prebuilt <SignIn /> from Part 1, props included, carries most applications. When the design finally demands a bespoke form, drop to the hook and keep the same backend:
// components/custom-sign-in.tsx
'use client'
import { useSignIn } from '@clerk/nextjs'
import { useRouter } from 'next/navigation'
export function CustomSignIn() {
const { errors, fetchStatus, signIn } = useSignIn()
const router = useRouter()
async function handleSubmit(emailAddress: string, password: string) {
const { error } = await signIn.password({ emailAddress, password })
if (error) return
if (signIn.status !== 'complete') {
// 'needs_second_factor' and 'needs_client_trust' each get their own screen
return
}
await signIn.finalize({
navigate: ({ session, decorateUrl }) => {
// Bail out and let the `taskUrls` redirect handle a pending task
if (session?.currentTask) return
const url = decorateUrl('/dashboard')
if (url.startsWith('http')) window.location.href = url
else router.push(url)
},
})
}
// SignInForm is your own presentational component
return (
<SignInForm
error={errors.fields.identifier?.message ?? errors.fields.password?.message}
isLoading={fetchStatus === 'fetching'}
onSubmit={handleSubmit}
/>
)
}Four things about that flow are easy to get wrong.
Signing in takes two steps in Core 3: signIn.password() authenticates, but the session isn't active until signIn.finalize() runs. And 'complete' is only one of the outcomes. 'needs_second_factor' and 'needs_client_trust' are just as ordinary, and Client Trust is on by default for applications created after November 14, 2025, so every branch needs a screen or the form dead-ends the first time a user without MFA signs in from a new device.
These methods report failures on an error property rather than throwing, so a try/catch around them won't catch a rejected credential.
decorateUrl() can hand back an absolute URL that refreshes the session cookie through Clerk's API, which is what keeps sessions alive under Safari's tracking prevention. Send that case through window.location.href, because a client-side push would skip the round trip.
And session tasks fire as soon as Organizations are enabled: on instances created after August 22, 2025, turning Organizations on leaves Personal Accounts off by default, so choosing an organization becomes a required task after authenticating. (reset-password is on by default for instances created after December 8, 2025, though it only fires for a password flagged as compromised.) A pending task holds the session in pending after finalize(), which the rest of Clerk treats as signed out. Point each task at a page with the taskUrls prop on <ClerkProvider>, then bail out of navigate and let that redirect win.
Prebuilt and custom share the same session, the same token lifetime, and the same security model. The markup is the only thing that changes.
Using Clerk with Supabase and Row Level Security
Choosing Clerk for authentication doesn't mean giving up Postgres Row Level Security. Clerk's native Supabase integration passes the Clerk session token to Supabase, so RLS policies evaluate against the Clerk user.
Two setup steps come first. Turning the integration on in the Clerk Dashboard adds the role: authenticated claim to your session tokens, which is what a policy scoped to authenticated keys off. Adding Clerk as a third-party auth provider in Supabase is what gets Supabase to trust those tokens.
With that done, pass a token getter to the Supabase client through the accessToken option:
// lib/supabase-server.ts
import { auth } from '@clerk/nextjs/server'
import { createClient } from '@supabase/supabase-js'
export function createServerSupabaseClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
async accessToken() {
return (await auth()).getToken()
},
},
)
}That helper reads the incoming request, so it belongs in Server Components, Server Actions, and Route Handlers. auth() carries a server-only guard, so importing it into a Client Component fails the build, well before anything reaches production. Client code builds the same client from useSession(), passing session?.getToken() ?? null to that identical accessToken option. Setting accessToken also takes Supabase's own auth namespace out of play, so supabase.auth.* calls throw once Clerk is the token source.
Two things about the table decide whether any of this works. The owning column has to store Clerk user identifiers, and Row Level Security has to be switched on. A policy on a table without that alter table is inert: Postgres ignores it, and because Supabase grants public tables to anon by default, the publishable key sitting in your browser bundle reads every row.
create table tasks (
id serial primary key,
name text not null,
user_id text not null default auth.jwt()->>'sub'
);
alter table "tasks" enable row level security;Retrofitting this onto an existing table is where it usually goes wrong: if user_id holds a Supabase UUID or an internal foreign key rather than a Clerk user_... identifier, the comparison below never matches and every query returns nothing.
Policies then key on the sub claim, which holds the Clerk user identifier. Reads and writes need one each:
create policy "User can view their own tasks"
on "public"."tasks"
for select
to authenticated
using (
((select auth.jwt()->>'sub') = (user_id)::text)
);
create policy "Users must insert their own tasks"
on "public"."tasks"
for insert
to authenticated
with check (
((select auth.jwt()->>'sub') = (user_id)::text)
);Write that with check predicate out in full rather than reaching for with check (true) when the first insert fails. The column default only fires when the insert omits user_id entirely, so a permissive check lets a caller name any owner they like, and your select policy then hides those rows from the user they were filed under.
Queries made through that client are then filtered by Postgres itself, so a missing check in application code can't leak another user's rows. You get Clerk's MFA, passkeys, Organizations, and bot protection alongside database-enforced authorization.
If every query comes back empty instead, the role: authenticated claim probably never arrived. Check the Clerk Dashboard toggle and the Supabase provider entry, because to authenticated matches a Postgres role rather than anything on the token itself.
Comparing 8 Next.js authentication providers
Read this comparison as a shortlist filter, not a scoreboard; the three questions below narrow it.
Provider comparison table
The setup ranges estimate a working sign-in and server-side session check. They exclude production readiness, branding, authorization design, migration, and enterprise connection setup. Clerk's lower bound assumes the Clerk CLI, which scaffolds the SDK, provider, Proxy, and auth pages in one command; the Dashboard path sits at the upper end. The CLI walkthrough covers it end to end.
Two pricing details are too granular for the table. Clerk's B2B Authentication add-on is included free in every plan with 100 monthly retained organizations (MROs) per app, and its paid Enhanced tier costs $85/month billed annually or $100 monthly with additional MROs at $1/month each. Enterprise Connection pricing tiers down above the first 15 connections, as does per-user pricing above 100,000 MRUs and per-organization pricing above 1,000 MROs.
Next.js integration at a glance
Two entries deserve a note. Auth0's Next.js SDK has declared Next.js 16 in its peer dependencies since v4.13.0 in November 2025, so the --legacy-peer-deps workaround that circulated right after the Next.js 16 release is no longer needed. That range now floors at 16.0.10, so it skips 16.0.0 through 16.0.9 exactly as @clerk/nextjs does.
Firebase has two React UI libraries in circulation, so check which one a tutorial is using. The original wrapper, react-firebaseui, hasn't had a release since v6.0.0 in November 2021. It was superseded by the FirebaseUI rewrite, whose v7.0.3 release shipped a first-party React package in July 2026. Firebase still installs that package from its beta tag and publishes no App Router guidance for it.
Strengths and trade-offs by provider
Clerk gives a Next.js team the most surface out of the box: embedded components, framework-native server helpers, Organizations, and managed abuse protection.
The trade-offs are real. It's fully managed with no self-hosted option, so on-premises requirements rule it out. It's a younger company than Okta or Auth0, which matters to organizations with formal vendor-review processes. And per-user pricing means teams projecting into the hundreds of thousands of users should model the cost against alternatives rather than assume the free tier scales.
Auth0 has the deepest enterprise federation catalog and more than a decade of production history. Configuration is dashboard-centric, which some teams find awkward alongside infrastructure-as-code practices, and pricing steps up sharply between the B2C and B2B tracks at the same MAU floor.
WorkOS is built around the enterprise onboarding motion: SSO, SCIM Directory Sync, and an Admin Portal that lets a customer's IT team configure their own connection. AuthKit's sign-in UI is hosted and redirect-based, with no embeddable inline sign-in components, though WorkOS Widgets do cover post-sign-in management screens such as user profile and organization switching. Per-connection pricing means cost tracks enterprise customer count rather than end users.
Auth.js is the answer when data ownership is non-negotiable. The costs land in places that are easy to underestimate: MFA isn't built in, token rotation is manual through callbacks, the credentials provider forces the JWT strategy and doesn't persist users automatically, and not every database adapter is edge-compatible. The v5 rewrite has been in beta for an extended period while v4 remains the latest tag, so plan for a migration.
Supabase Auth is the natural choice inside a Supabase stack, and Row Level Security is genuinely excellent, since authorization enforced by the database eliminates a whole category of bug. The auth layer itself is thinner than dedicated platforms. If you want both, Clerk's native Supabase integration keeps RLS while adding a dedicated auth layer, at the cost of a second vendor.
Firebase Auth is a good fit for client-rendered React and React Native applications, especially alongside Firestore and Cloud Functions. It wasn't designed for server rendering: there are no official Server Component helpers, the recommended SSR path uses FirebaseServerApp with a service worker to move tokens to the server, and the common App Router path is the community next-firebase-auth-edge library instead. ID tokens are fixed at one hour and can't be tuned.
Amazon Cognito makes sense when you're already deep in AWS and want IAM integration and one bill. The friction is developer experience: configuration is extensive, some user pool decisions can't be reversed after creation, and the Next.js path runs through Amplify rather than a purpose-built SDK.
Okta is an enterprise identity platform first. Universal Directory, lifecycle management, and SCIM are its strengths, and if your organization already runs on Okta the integration question is mostly settled. For a Next.js application specifically, there's no first-party App Router SDK, so you go through Auth0's SDK or a generic OIDC library, and pricing starts at a level that assumes an enterprise contract.
Migration and lock-in
All of these providers issue standard JWTs, so token verification patterns port reasonably well. The coupling is in the framework SDK, not the token format: swapping @clerk/nextjs for @auth0/nextjs-auth0 means rewriting every session read, every route policy, and every UI component.
Treat a provider change as a data and session project rather than an SDK swap. Profile data is usually portable. Active sessions never transfer, so plan for everyone to sign in again.
Password preservation is the question to pin down with every vendor on your shortlist before you commit: it depends on whether the source exports hashes and the destination supports that algorithm, and the answer decides whether leaving forces a reset on all of your users. Clerk documents the path out: an admin (or anyone working in their personal workspace) can download a CSV of the application's users that includes their hashed passwords, from the instance's Settings page under User Exports. User records are also available programmatically through the getUserList() Backend API wrapper, though only the CSV carries the hashes. Read the destination's own import guidance before promising a reset-free migration: Clerk documents both directions, and Auth0 and WorkOS publish theirs.
Pricing sources: Clerk, Auth0, WorkOS, Supabase, Firebase, Amazon Cognito, and Okta. Auth.js is free and open source, so it has no pricing page; its release status is read from the npm registry. Product sources: Clerk Organizations, Auth0 enterprise connections, WorkOS AuthKit for Next.js, Supabase SSR, Firebase server-side rendering, and Cognito authentication flows. The prebuilt-UI column draws on WorkOS Widgets, the Amplify UI Authenticator, and the Okta Sign-In Widget.
Choosing the right auth provider for your team
Three questions decide the shortlist, and the table below turns the answers into a pick.
1. How much authentication work can your team own? A small product team should avoid maintaining recovery, MFA enrollment, session revocation, organization invitations, abuse prevention, and provider upgrades unless that control is strategically important. Clerk is the strongest default when it isn't; an open-source library is the answer when self-hosting is a deliberate requirement.
2. Is the product B2B or consumer? B2B products should start with the tenant model, because retrofitting one costs what the section above describes.
3. Is enterprise SSO a current requirement? A live requirement narrows the field to Clerk, WorkOS, Auth0, and Okta. A future one leaves the choice open.
Decision shortcuts
Ready to build it? Start from Clerk's Next.js quickstart, or scaffold the whole integration with a single Clerk CLI command, walked through step by step in the CLI guide.
Authentication decisions get expensive to reverse once traffic and tenant count climb. Three are worth settling before launch, whichever provider you pick: what a session check costs per request, whether tenancy lives in the auth layer or your schema, and what the path out looks like if you switch.
Frequently asked questions
In this series
- Next.js Authentication Guide 2026
- Next.js Authentication Guide 2026 - Part 2 (you are here)