Skip to main content
Articles

Next.js Authentication Guide 2026 - Part 2

Authors: Roy AngerJeff EscalanteBrian Morrison II
Published: (last updated )

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.

Note

Pricing and product details were verified on July 29, 2026. Vendor pricing changes frequently; confirm it before purchasing.

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

OptionBest forBasic setup estimatePricing model, verified July 29, 2026Organizations and multi-tenancyMFASession management
ClerkNext.js and B2B SaaS teams that want embedded UI and organization management without assembling those layers5–15 minutes with the CLI; longer via the DashboardHobby is free up to 50,000 monthly retained users (MRUs) per app, then you upgrade. Pro starts at $20/month billed annually or $25 monthly and also includes 50,000 MRUs per app, with additional users at $0.02/month each and tiering down above that. Pro and Business each include one Enterprise Connection per app, with connections 2 through 15 at $75/month each.Organizations, invitations, switching, Admin/Member roles, and custom permissions are included. The Enhanced B2B Authentication tier adds unlimited members, Verified Domains, custom Roles and Rolesets, and linking Enterprise connections with Organizations. SAML/OIDC is available through Enterprise Connections.Built in; production MFA is included on Pro and aboveManaged sessions, short-lived JWTs, background refresh, server auth(), and client hooks
Auth0Teams that need mature customer identity and broad enterprise federation, including SAML, OIDC, AD/LDAP, ADFS, Azure AD, Google Workspace, and PingFederate15–30 minutesFree covers 25,000 MAUs and includes self-service SSO, SCIM, and one enterprise connection. Paid plans split into two tracks priced from a 500-MAU floor: B2C Essentials starts at $35/month and B2B (Organizations) Essentials at $150/month, which includes unlimited Organizations and three enterprise SSO connections.Organizations, organization roles, branded federated login, and enterprise connections; availability and limits depend on planPro MFA on Essentials and above; capabilities vary by planThe Next.js SDK manages an encrypted session cookie and exposes server session and token APIs
WorkOSB2B products that want SSO, Directory Sync, Admin Portal, and user management as modular enterprise services15–30 minutesAuthKit is free for the first 1 million MAUs, then $2,500 per additional million. SSO and Directory Sync start at $125 per connection per month for the first 15, dropping to $100, $80, and $65 at higher counts, with anything above 100 connections quoted by sales.First-class Organizations and RBAC; SSO, SCIM Directory Sync, and Admin Portal support customer IT onboardingBuilt into AuthKitThe Next.js SDK uses Proxy and server helpers such as withAuth() for cookie-backed sessions and access tokens
Auth.js (NextAuth.js)Existing Auth.js deployments and teams willing to own UI, persistence, authorization, operations, and security maintenance15–45 minutes for basic OAuth; longer with a database, email, custom UI, or B2B featuresFree, open-source software. You still pay for hosting, database and email infrastructure, monitoring, and engineering. The stable release is the v4 line (npm's latest tag, currently 4.24.15); the v5 rewrite is still a pre-release, published only under the beta tag.No turnkey organization product; you model tenants, memberships, invitations, roles, and admin workflowsWebAuthn and provider primitives exist, but complete MFA policy and recovery are application workJWT or database sessions with server-side auth() and adapters for persistence
Supabase AuthTeams already using Supabase Postgres, APIs, and Row Level Security10–30 minutes with the starter; longer for manual SSRFree includes 50,000 MAUs. Pro starts at $25/month with 100,000 MAUs, then $0.00325 per additional MAU. SAML SSO requires Pro and includes 50 SSO users, then $0.015 per MAU.Model tenants in Postgres and enforce isolation with Row Level Security; no turnkey organization-management UITOTP MFA on every plan; phone MFA is a flat-fee add-on from Pro upAccess and refresh tokens in cookies; @supabase/ssr uses browser/server clients and Proxy for refresh
Firebase AuthClient-rendered React apps and teams already inside the Firebase and Google Cloud ecosystem30–60 minutes for App Router, which needs a community libraryThe Spark plan includes 50,000 MAUs at no cost. SAML and OIDC are capped at 50 MAUs before Google Cloud Identity Platform pricing applies, and phone authentication needs the paid Blaze plan, billed per SMS.No organization product; tenants are yours to modelAvailable through Identity PlatformClient-first. ID tokens have a fixed one-hour TTL. Server rendering uses FirebaseServerApp with a service worker, or the community next-firebase-auth-edge library
Amazon CognitoAWS-native architectures where IAM integration and consolidated billing outweigh developer experienceHours, and some choices are irreversible after pool creationLite and Essentials both include 10,000 MAUs free. Lite starts at $0.0055 per MAU and tiers down to $0.0025; Essentials is a flat $0.015 per MAU; Plus is $0.020 per MAU with no free tier. SAML/OIDC federation is $0.015 per MAU above 50. Machine-to-machine tokens start at $0.00225 per request and tier down. Rates are us-east-1.User pools with groups mapped to IAM roles; no turnkey organization or invitation modelTOTP and SMS for MFA; passkeys and email OTP are first factors on Essentials and above; threat protection is gated to PlusAmplify Gen 2 with @aws-amplify/adapter-nextjs for server contexts
OktaEnterprises standardizing workforce or customer identity on Okta, with existing directory and lifecycle investmentsHours to daysPublished Workforce Identity suites run $6 to $17 per user per month billed annually, with a $1,500 minimum annual contract; higher tiers are quoted. Customer Identity requires a $3,000 per month Enterprise base platform plus MAU-based suite pricing. The Integrator Free plan supports up to 10 active users for development.Universal Directory, groups, lifecycle management, and SCIM provisioningAdaptive MFANo first-party Next.js App Router SDK; teams integrate through Auth0's Next.js SDK or a generic OIDC library

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

OptionServer Component helperproxy.ts supportEmbeddable prebuilt UISession token TTLFramework React hooks
Clerkauth()60 seconds, refreshed at 50
Auth0auth0.getSession()Universal Login is redirect-based; Auth0 recommends it over embedded login for most casesAccess tokens default to 24 hours, configurable per APIuseUser(), client only
WorkOSwithAuth()Widgets cover management UI; sign-in itself is hostedConfigurableuseAuth(), useAccessToken(), useTokenClaims(), useRecentAuth()
Auth.js (NextAuth.js)auth()ConfigurableSession helpers, no component library
Supabase AuthcreateServerClientUI Library auth blocks are copy-into-your-repo source, not a component packageConfigurable
Firebase AuthCommunity next-firebase-auth-edgeFirebaseUI v7 React, no App Router guidanceID token 1 hour fixed; session cookie 5 minutes to 2 weeks
Amazon Cognito@aws-amplify/adapter-nextjsManual wiringAmplify UI Authenticator, client sideConfigurableAmplify UI hooks
OktaSign-In Widget, React SPA guidance onlyConfigurable

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

If you need…Consider
Fastest path to embedded sign-in UI in the App RouterClerk: prebuilt <SignIn /> and <UserButton />, server auth()
A tenant model with roles, invitations, and per-org SSOClerk Organizations, or WorkOS if IT onboarding drives the roadmap
Enterprise SSO with self-service customer configurationWorkOS: Admin Portal and SCIM Directory Sync
An established enterprise CIAM platform with broad federationAuth0, or Okta if already standardized there
Auth bundled with Postgres and Row Level SecuritySupabase Auth, or Clerk with the native Supabase integration
Complete data ownership and no per-user feesAuth.js, with the maintenance cost budgeted
Deep Google Cloud or React Native ecosystem integrationFirebase Auth, accepting third-party libraries for SSR
AWS-native infrastructure with IAM and consolidated billingAmazon Cognito

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

  1. Next.js Authentication Guide 2026
  2. Next.js Authentication Guide 2026 - Part 2 (you are here)