How to Add Auth to a Next.js App (And Why You Should Do It First)

01 Mar 2026 · Bank K.

A practical guide to adding authentication to your Next.js app. Compare NextAuth, Clerk, Supabase Auth, Firebase Auth, and all-in-one solutions — with code examples and honest trade-offs for indie hackers shipping MVPs.

You just shipped your MVP. It looks great. You showed it to someone on Twitter. They said “nice, can I sign up?”

And that’s where the weekend disappears.

Adding auth to a Next.js app is the most common thing every developer needs to do, and somehow it still takes way longer than it should. You start with “I just need a login page” and end up three days deep in JWT refresh token rotation strategies, wondering where your life went wrong.

This guide is for indie hackers and solo developers who want to add auth to a Next.js app without losing a week of shipping time. We will cover every major option, show real code, and be honest about the trade-offs.

Why Auth Is the First Thing Every App Needs

Here is the uncomfortable truth: you cannot build anything useful without authentication. Not a dashboard. Not a SaaS tool. Not even a simple CRUD app that stores user data.

Auth is not a feature. It is infrastructure. And every day you delay it is a day you are building on sand.

What auth actually unlocks:

  • User identity — knowing who is using your app
  • Data isolation — making sure users only see their own stuff
  • Payments — you cannot charge someone you cannot identify
  • Analytics — real usage data instead of anonymous page views
  • Growth — email lists, onboarding flows, retention tracking

If you are building an MVP, auth should be the first thing you wire up. Everything else depends on it.

The Major Ways to Add Auth to a Next.js App

Let’s walk through every realistic option in 2026. For each one, I will show you what the setup actually looks like, what it costs, and where it falls apart.

1. NextAuth.js (Auth.js)

NextAuth — now rebranded as Auth.js — is the community standard for Next.js authentication. It is open source, flexible, and well-documented.

Basic setup:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import GithubProvider from "next-auth/providers/github";

const handler = NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    GithubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
  ],
  callbacks: {
    async session({ session, token }) {
      session.user.id = token.sub;
      return session;
    },
  },
});

export { handler as GET, handler as POST };

Then wrap your app with the session provider:

// app/layout.tsx
import { SessionProvider } from "next-auth/react";

export default function RootLayout({ children }) {
  return (
    <SessionProvider>
      {children}
    </SessionProvider>
  );
}

And use it in your components:

"use client";
import { useSession, signIn, signOut } from "next-auth/react";

export default function LoginButton() {
  const { data: session } = useSession();

  if (session) {
    return <button onClick={() => signOut()}>Sign out</button>;
  }
  return <button onClick={() => signIn()}>Sign in</button>;
}

Pros:

  • Free and open source
  • Huge provider list (Google, GitHub, Discord, email, etc.)
  • Full control over your data
  • Active community and ecosystem

Cons:

  • You own the complexity. Database adapters, token management, session handling — all on you
  • The v4 to v5 migration was painful. Breaking changes happen
  • No built-in user management UI
  • You still need to set up a database, configure OAuth apps, handle edge cases
  • Adding payments means integrating Stripe separately

Best for: Developers who want full control and don’t mind spending 2-3 days on setup.

Realistic time to production: 1-3 days

2. Clerk

Clerk is a managed authentication service built specifically for React and Next.js. It gives you pre-built UI components, user management, and multi-factor auth out of the box.

Basic setup:

// middleware.ts
import { clerkMiddleware } from "@clerk/nextjs/server";

export default clerkMiddleware();

export const config = {
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};
// app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";

export default function RootLayout({ children }) {
  return (
    <ClerkProvider>
      {children}
    </ClerkProvider>
  );
}
// Any component
import { SignInButton, UserButton, useUser } from "@clerk/nextjs";

export default function Header() {
  const { isSignedIn, user } = useUser();

  return (
    <header>
      {isSignedIn ? (
        <UserButton afterSignOutUrl="/" />
      ) : (
        <SignInButton mode="modal" />
      )}
    </header>
  );
}

Pros:

  • Beautiful pre-built components (sign-in, sign-up, user profile)
  • Excellent Next.js integration with middleware
  • Built-in MFA, organization management, and webhooks
  • Great developer experience

Cons:

  • Pricing scales with monthly active users (free up to 10,000 MAU, then $0.02/MAU)
  • Vendor lock-in. Your users live on their platform
  • No built-in payments
  • At scale, costs add up fast for a bootstrapped product

Best for: Developers who want polished auth UI quickly and are okay with per-user pricing.

Realistic time to production: 30 minutes to 2 hours

3. Supabase Auth

Supabase is the open-source Firebase alternative, and its auth module is solid. If you are already using Supabase for your database, adding auth is almost free.

Basic setup:

// lib/supabase.ts
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// Sign in with OAuth
async function signInWithGoogle() {
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: "google",
    options: {
      redirectTo: `${window.location.origin}/auth/callback`,
    },
  });
}

// Sign in with email/password
async function signInWithEmail(email: string, password: string) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password,
  });
}

Pros:

  • Generous free tier (50,000 MAU)
  • Tight integration with Supabase database and Row Level Security
  • Open source — you can self-host
  • Built-in email templates and magic links

Cons:

  • Tightly coupled to the Supabase ecosystem
  • Auth UI components are less polished than Clerk
  • Self-hosting adds operational complexity
  • Still no built-in payments

Best for: Developers already using Supabase for their database layer.

Realistic time to production: 1-2 hours (with existing Supabase project)

4. Firebase Auth

Firebase Auth is Google’s authentication solution. It handles the basics well and has been around forever.

Basic setup:

// lib/firebase.ts
import { initializeApp } from "firebase/app";
import { getAuth, GoogleAuthProvider, signInWithPopup } from "firebase/auth";

const app = initializeApp({
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
});

export const auth = getAuth(app);

export async function signInWithGoogle() {
  const provider = new GoogleAuthProvider();
  const result = await signInWithPopup(auth, provider);
  return result.user;
}

Pros:

  • Very generous free tier
  • Battle-tested at massive scale
  • Good documentation
  • Works with any framework

Cons:

  • Firebase SDK is large (adds bundle size)
  • Google ecosystem lock-in
  • Firestore pricing can surprise you at scale
  • Server-side auth in Next.js App Router requires extra work
  • No payments integration

Best for: Developers already in the Google/Firebase ecosystem.

Realistic time to production: 1-3 hours

5. Custom Auth (Roll Your Own)

Building auth from scratch with bcrypt, JWTs, and a database. The “how hard can it be?” approach.

// This is simplified. Real auth has 10x more edge cases.
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";

async function register(email: string, password: string) {
  const hashedPassword = await bcrypt.hash(password, 12);
  const user = await db.user.create({
    data: { email, password: hashedPassword },
  });
  return jwt.sign({ userId: user.id }, process.env.JWT_SECRET!);
}

async function login(email: string, password: string) {
  const user = await db.user.findUnique({ where: { email } });
  if (!user) throw new Error("Invalid credentials");

  const valid = await bcrypt.compare(password, user.password);
  if (!valid) throw new Error("Invalid credentials");

  return jwt.sign({ userId: user.id }, process.env.JWT_SECRET!);
}

Pros:

  • Total control
  • No vendor dependencies
  • Free (besides your time)

Cons:

  • You will get something wrong. Password reset flows, token rotation, CSRF protection, rate limiting, session invalidation — auth has a massive surface area for security bugs
  • Takes 1-2 weeks to do properly
  • You become your own security team
  • Every hour spent on auth is an hour not spent on your actual product

Best for: Honestly? Almost nobody building an MVP. Save this for when you have a security team.

Realistic time to production: 1-2 weeks (if you want it secure)

The Comparison: Which Auth Solution Should You Use?

Here is how these options stack up for an indie hacker shipping an MVP:

FeatureNextAuthClerkSupabaseFirebaseCustom
Setup time1-3 days30 min1-2 hrs1-3 hrs1-2 weeks
Free tierUnlimited10K MAU50K MAU10K MAUN/A
Pre-built UINoYesBasicNoNo
PaymentsNoNoNoNoNo
Vendor lock-inLowHighMediumHighNone
Self-hostableYesNoYesNoYes

Notice something? None of them include payments. That is a problem.

The Auth + Payments Problem

Every SaaS app needs two things on day one: know who your users are, and let them pay you.

Auth and payments are not separate problems. They are the same problem. You need to:

  1. Authenticate the user
  2. Associate a Stripe customer ID with their account
  3. Gate features based on their subscription plan
  4. Handle upgrades, downgrades, and cancellations
  5. Keep auth state and payment state in sync

When you add auth to a Next.js app using any of the options above, you still need to separately integrate Stripe. That means:

  • Setting up Stripe Checkout or Payment Links
  • Building webhook handlers for subscription events
  • Creating a customer portal for plan management
  • Syncing user records between your auth provider and Stripe
  • Writing middleware to check subscription status on protected routes

Here is what a basic Stripe integration looks like on top of your auth:

// api/create-checkout/route.ts
import { stripe } from "@/lib/stripe";
import { getServerSession } from "next-auth";

export async function POST(req: Request) {
  const session = await getServerSession();
  if (!session?.user?.email) {
    return new Response("Unauthorized", { status: 401 });
  }

  // Find or create Stripe customer
  const customers = await stripe.customers.list({
    email: session.user.email,
    limit: 1,
  });

  let customerId = customers.data[0]?.id;
  if (!customerId) {
    const customer = await stripe.customers.create({
      email: session.user.email,
    });
    customerId = customer.id;
  }

  const checkoutSession = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: "subscription",
    line_items: [{ price: process.env.STRIPE_PRICE_ID!, quantity: 1 }],
    success_url: `${process.env.NEXT_URL}/dashboard?success=true`,
    cancel_url: `${process.env.NEXT_URL}/pricing`,
  });

  return Response.json({ url: checkoutSession.url });
}

Then you need webhook handlers:

// api/webhooks/stripe/route.ts
import { stripe } from "@/lib/stripe";

export async function POST(req: Request) {
  const body = await req.text();
  const sig = req.headers.get("stripe-signature")!;

  const event = stripe.webhooks.constructEvent(
    body,
    sig,
    process.env.STRIPE_WEBHOOK_SECRET!
  );

  switch (event.type) {
    case "checkout.session.completed":
      // Update user's subscription status in your database
      break;
    case "customer.subscription.deleted":
      // Revoke access
      break;
    case "invoice.payment_failed":
      // Handle failed payment
      break;
  }

  return new Response("OK");
}

That is a lot of code for something every SaaS needs. And we have not even covered plan gating, trial periods, or the customer portal.

DIY Auth vs Auth-as-a-Service vs All-in-One

Let’s be real about the three paths:

DIY (NextAuth + custom Stripe): Maximum flexibility, maximum time investment. You will spend 3-5 days getting auth and payments working together properly. Great if you enjoy the plumbing. Bad if you want to ship this week.

Auth-as-a-service (Clerk/Supabase + separate Stripe): Faster auth setup, but you still need to wire up payments yourself. You are managing two vendor relationships and keeping them in sync. Typical setup time: 1-2 days.

All-in-one (auth + payments bundled): This is the approach tools like Beag take. Instead of stitching together auth and payments from different providers, you add a single script to your app and get both. Auth is handled server-side (not client-only JWTs), and Stripe payments are pre-wired so users can subscribe to plans immediately.

The Beag approach works particularly well for indie hackers and vibe coders because:

  • There is no backend to build. It works with any frontend — React, Vue, plain HTML, or whatever your AI tool generated
  • User data (email, plan, subscription status) is available in localStorage after login
  • Feature gating is just checking the user’s plan in JavaScript
  • You can go from zero to “accepting payments” in about 5 minutes

Here is what feature gating looks like with an all-in-one approach:

// Check if user has access to a premium feature
function canAccessFeature(feature: string): boolean {
  const userData = JSON.parse(localStorage.getItem("beag_user") || "{}");

  if (userData.plan === "pro" || userData.plan === "enterprise") {
    return true;
  }

  // Free users get limited access
  const freeFeatures = ["dashboard", "basic-reports"];
  return freeFeatures.includes(feature);
}

No webhook handlers. No customer sync logic. No database adapter configuration.

How to Choose the Right Approach

Ask yourself three questions:

1. How much time do you have? If you need to ship this week, an all-in-one solution or managed service saves days. If you have a month, DIY gives you more control.

2. Do you need payments on day one? If yes, pick a solution that bundles both. Wiring Stripe into a separate auth system always takes longer than you expect.

3. How technical is your stack? If you are using AI tools like Cursor, v0, or Bolt to generate your frontend, you want something that does not require a backend. Adding a script tag is always easier than configuring a Node.js API layer.

For more detailed integration guides and setup walkthroughs, check out the Beag documentation.

FAQ

Is it safe to add auth to a Next.js app using client-side only methods?

Client-side only auth (storing JWTs in localStorage without server validation) is a security risk. Tokens can be tampered with, and there is no server-side verification that the user is who they claim to be. Any serious auth solution should validate sessions on the server. Solutions like NextAuth, Clerk, and Beag all handle server-side validation — make sure whatever you choose does too.

Can I add auth to a Next.js app that was generated by AI (Cursor, v0, Bolt)?

Yes. AI-generated apps are just regular web apps. The easiest integration path is a solution that requires minimal code changes — ideally just a script tag or a wrapper component. Solutions that need you to restructure your API routes or add database adapters are harder to retrofit into AI-generated code without breaking things.

How much does it cost to add auth and payments to an MVP?

It ranges from free to a few hundred dollars per month depending on your approach. NextAuth is free but costs you time. Clerk is free up to 10K MAU. Supabase Auth is free up to 50K MAU. All-in-one solutions like Beag start at $19/month and include both auth and Stripe payments, which is cheaper than the combined cost of separate auth and payment tools at scale. The real cost is always your time — a week of development time on auth plumbing is worth far more than $19/month.

Should I add auth before or after building my core features?

Before. Always before. Auth determines your data model (every record needs a user ID), your API design (every endpoint needs authentication), and your UI flow (signed in vs. signed out states). Bolting auth onto an existing app means refactoring almost everything. Starting with auth means every feature you build afterward works correctly from day one.

Wrapping Up

Adding auth to a Next.js app is not optional — it is the foundation everything else sits on. The question is how much time you want to spend on it.

If you are an indie hacker or solo developer trying to validate an idea quickly, every day spent on auth infrastructure is a day not spent talking to users or building features they actually care about. Pick the fastest path that does not compromise on security.

For most MVPs, that means either a managed service like Clerk (for auth only) or an all-in-one solution like Beag (for auth + payments together). You can always migrate to a custom setup later when you have revenue, users, and a reason to invest the time.

Add auth + payments to your app in 5 minutes. Try Beag free for 7 days — no credit card required.

For more developer guides and tutorials, check out the Beag blog.

About the Author
Bank K.

Bank K.

Serial entrepreneur & Co-founder of Beag.io

Founder of Beag.io. Indie hacker building tools to help developers ship faster. Previously built multiple micro SaaS products.

Ready to Make Money From Your SaaS?

Turn your SaaS into cash with Beag.io. Get started now!

Start 7-day free trial →