<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Esimit Karlgusta</title>
    <description>The latest articles on Forem by Esimit Karlgusta (@thekarlesi).</description>
    <link>https://forem.com/thekarlesi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F358200%2F2d8413ff-d550-4565-ab48-d3dc075aa5b1.png</url>
      <title>Forem: Esimit Karlgusta</title>
      <link>https://forem.com/thekarlesi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/thekarlesi"/>
    <language>en</language>
    <item>
      <title>Stop Learning, Start Building: The Fastest Way to Become a Developer in 2026</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 12 May 2026 10:28:20 +0000</pubDate>
      <link>https://forem.com/thekarlesi/stop-learning-start-building-the-fastest-way-to-become-a-developer-in-2026-21fj</link>
      <guid>https://forem.com/thekarlesi/stop-learning-start-building-the-fastest-way-to-become-a-developer-in-2026-21fj</guid>
      <description>&lt;p&gt;If you’ve been learning programming for a while and still feel stuck, you are not alone.&lt;/p&gt;

&lt;p&gt;Most beginners don’t fail because coding is too hard.&lt;/p&gt;

&lt;p&gt;They fail because they spend too much time learning and not enough time building.&lt;/p&gt;

&lt;p&gt;At some point, you need to switch.&lt;/p&gt;

&lt;p&gt;From consuming information to creating software.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tutorial Trap
&lt;/h2&gt;

&lt;p&gt;Tutorials feel productive.&lt;/p&gt;

&lt;p&gt;You:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;watch a video
&lt;/li&gt;
&lt;li&gt;follow along
&lt;/li&gt;
&lt;li&gt;everything works
&lt;/li&gt;
&lt;li&gt;you feel like you understand it
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But a few hours later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can’t build anything alone
&lt;/li&gt;
&lt;li&gt;you forget most of it
&lt;/li&gt;
&lt;li&gt;you need the tutorial again
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not learning.&lt;/p&gt;

&lt;p&gt;This is repetition without understanding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Building Is Different
&lt;/h2&gt;

&lt;p&gt;Building forces your brain to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;make decisions
&lt;/li&gt;
&lt;li&gt;solve problems
&lt;/li&gt;
&lt;li&gt;handle errors
&lt;/li&gt;
&lt;li&gt;connect concepts
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where real learning happens.&lt;/p&gt;

&lt;p&gt;When you build something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you get stuck
&lt;/li&gt;
&lt;li&gt;you debug
&lt;/li&gt;
&lt;li&gt;you search
&lt;/li&gt;
&lt;li&gt;you try again
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That struggle is what creates skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  You Don’t Need More Tutorials
&lt;/h2&gt;

&lt;p&gt;Most beginners already have enough information.&lt;/p&gt;

&lt;p&gt;The real issue is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no practice
&lt;/li&gt;
&lt;li&gt;no projects
&lt;/li&gt;
&lt;li&gt;no repetition
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At some point, more tutorials stop helping.&lt;/p&gt;

&lt;p&gt;You need experience, not more explanations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With Small Projects
&lt;/h2&gt;

&lt;p&gt;You don’t need big ideas.&lt;/p&gt;

&lt;p&gt;Start simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;calculator
&lt;/li&gt;
&lt;li&gt;to-do app
&lt;/li&gt;
&lt;li&gt;notes app
&lt;/li&gt;
&lt;li&gt;weather app
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These teach you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;logic
&lt;/li&gt;
&lt;li&gt;structure
&lt;/li&gt;
&lt;li&gt;debugging
&lt;/li&gt;
&lt;li&gt;basic UI
&lt;/li&gt;
&lt;li&gt;data handling
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small projects build confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 80/20 Rule of Learning to Code
&lt;/h2&gt;

&lt;p&gt;You only need a few core concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;variables
&lt;/li&gt;
&lt;li&gt;functions
&lt;/li&gt;
&lt;li&gt;loops
&lt;/li&gt;
&lt;li&gt;conditions
&lt;/li&gt;
&lt;li&gt;arrays
&lt;/li&gt;
&lt;li&gt;objects
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most beginner projects are built with just this.&lt;/p&gt;

&lt;p&gt;Instead of learning everything, focus on using what you already know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Skill Is Problem Solving
&lt;/h2&gt;

&lt;p&gt;Programming is not memorization.&lt;/p&gt;

&lt;p&gt;It is problem solving.&lt;/p&gt;

&lt;p&gt;When you build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you break problems into smaller parts
&lt;/li&gt;
&lt;li&gt;you test ideas
&lt;/li&gt;
&lt;li&gt;you fix mistakes
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That process is the actual skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Feel Stuck
&lt;/h2&gt;

&lt;p&gt;If you understand tutorials but can’t build alone, it usually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you only practiced with guidance
&lt;/li&gt;
&lt;li&gt;you never built from scratch
&lt;/li&gt;
&lt;li&gt;you never struggled through problems alone
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gap is normal.&lt;/p&gt;

&lt;p&gt;It closes with practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Fix It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Watch less
&lt;/h3&gt;

&lt;p&gt;Limit tutorials.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Build more
&lt;/h3&gt;

&lt;p&gt;Start small projects immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Remove guidance slowly
&lt;/h3&gt;

&lt;p&gt;Try building without step-by-step instructions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Embrace errors
&lt;/h3&gt;

&lt;p&gt;Every error is part of learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The “Blank Page” Test
&lt;/h2&gt;

&lt;p&gt;Open your editor and try building something without a tutorial.&lt;/p&gt;

&lt;p&gt;Even something simple.&lt;/p&gt;

&lt;p&gt;If you struggle, that is not failure.&lt;/p&gt;

&lt;p&gt;That is where growth starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Developers Don’t Follow Tutorials Forever
&lt;/h2&gt;

&lt;p&gt;Professional developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read documentation
&lt;/li&gt;
&lt;li&gt;build from requirements
&lt;/li&gt;
&lt;li&gt;debug constantly
&lt;/li&gt;
&lt;li&gt;learn on demand
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They don’t wait for step-by-step instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop Waiting to Feel Ready
&lt;/h2&gt;

&lt;p&gt;You will never feel fully ready.&lt;/p&gt;

&lt;p&gt;If you wait for that moment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you will keep learning without progress
&lt;/li&gt;
&lt;li&gt;you will keep switching resources
&lt;/li&gt;
&lt;li&gt;you will stay stuck
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start before you feel ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build, Break, Fix, Repeat
&lt;/h2&gt;

&lt;p&gt;This is the real learning loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build something
&lt;/li&gt;
&lt;li&gt;break it
&lt;/li&gt;
&lt;li&gt;figure out why
&lt;/li&gt;
&lt;li&gt;fix it
&lt;/li&gt;
&lt;li&gt;improve it
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That cycle builds real skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;If you want to become a developer in 2026, stop focusing only on learning.&lt;/p&gt;

&lt;p&gt;Start focusing on building.&lt;/p&gt;

&lt;p&gt;Because in programming, understanding comes from doing, not watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learn How Real SaaS Products Are Built
&lt;/h2&gt;

&lt;p&gt;Most beginners only build small practice projects.&lt;/p&gt;

&lt;p&gt;But real software involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication systems
&lt;/li&gt;
&lt;li&gt;APIs
&lt;/li&gt;
&lt;li&gt;databases
&lt;/li&gt;
&lt;li&gt;payments
&lt;/li&gt;
&lt;li&gt;deployment
&lt;/li&gt;
&lt;li&gt;system design
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding how these pieces work together is what turns beginners into real builders.&lt;/p&gt;

&lt;p&gt;If you want to learn how real SaaS products are built and shipped in modern development environments, check out ZeroToSaaS at &lt;a href="https://zero-to-saas.collabtower.com" rel="noopener noreferrer"&gt;https://zero-to-saas.collabtower.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It’s a practical execution-focused blueprint designed to help developers move from tutorials to real product building faster.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>What Is Solana? A Beginner-Friendly Introduction</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Fri, 08 May 2026 18:58:41 +0000</pubDate>
      <link>https://forem.com/thekarlesi/what-is-solana-a-beginner-friendly-introduction-5bjk</link>
      <guid>https://forem.com/thekarlesi/what-is-solana-a-beginner-friendly-introduction-5bjk</guid>
      <description>&lt;p&gt;You've probably heard the name Solana thrown around in developer circles, crypto communities, or tech Twitter. But what actually is it, and why should you care?&lt;/p&gt;

&lt;p&gt;This article breaks it down from the ground up, no prior blockchain knowledge required.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Simple Version
&lt;/h2&gt;

&lt;p&gt;Solana is a global network of computers that work together to process digital transactions quickly, cheaply, and reliably.&lt;/p&gt;

&lt;p&gt;Think of it like the internet, but instead of sharing information, you're transferring value. Send money, trade assets, run applications, all without going through a bank or any middleman.&lt;/p&gt;

&lt;p&gt;It's been live since 2020 and has processed billions of transactions since launch.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes Solana Different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  It's Fast
&lt;/h3&gt;

&lt;p&gt;Traditional bank transfers take days. Credit card payments feel instant but take days to fully settle on the backend. Solana transactions confirm in under a second. That's fast enough to power real-time apps, games, and payments that feel no different from what you're used to in Web2.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's Cheap
&lt;/h3&gt;

&lt;p&gt;The average transaction on Solana costs around &lt;strong&gt;$0.00025&lt;/strong&gt;. One dollar gets you roughly 4,000 transactions. That makes things like tipping creators a few cents, trading frequently, or building apps with lots of user interactions genuinely practical. Fees won't eat into profits or scare off users.&lt;/p&gt;

&lt;h3&gt;
  
  
  It Scales
&lt;/h3&gt;

&lt;p&gt;Some blockchains slow down and get expensive when traffic picks up. Solana is built to handle thousands of transactions per second, similar to major payment networks. Your transaction gets processed just as fast at peak hours as it does at quiet ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's Open to Everyone
&lt;/h3&gt;

&lt;p&gt;No bank account. No credit check. No geographic restrictions. If you have an internet connection, you can use Solana. A developer in Lagos has access to the same tools as one in San Francisco.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Can You Do on Solana?
&lt;/h2&gt;

&lt;p&gt;Solana isn't just for sending cryptocurrency. Here's a snapshot of what's possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Send money globally&lt;/strong&gt; — Any amount, to anyone, instantly, for a fraction of a cent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trade digital assets&lt;/strong&gt; — Swap tokens directly from your wallet, around the clock&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collect NFTs&lt;/strong&gt; — Own unique digital items that travel with you across platforms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Play blockchain games&lt;/strong&gt; — Truly own your in-game items and trade them freely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Earn yield&lt;/strong&gt; — Lend assets or provide liquidity with transparent rates and returns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Join communities&lt;/strong&gt; — Use tokens as membership cards or voting rights in decentralized organizations&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Terms Worth Knowing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SOL&lt;/strong&gt; is Solana's native currency. You need a small amount to pay transaction fees, similar to buying stamps before mailing a letter. A dollar's worth covers hundreds of transactions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wallets&lt;/strong&gt; are the software you use to store assets and interact with apps. One wallet works across thousands of Solana applications, no separate accounts needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tokens&lt;/strong&gt; are digital assets that live on Solana. Some are stable currencies, others represent ownership in a project or access to a service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validators&lt;/strong&gt; are the computers that verify and record transactions. Over 1,000 validators operate worldwide, keeping the network decentralized and resilient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smart contracts&lt;/strong&gt; are programs that run on Solana automatically, powering everything from token swaps to games. They run exactly as written, with no downtime or interference. Think of them as vending machines for digital services.&lt;/p&gt;




&lt;h2&gt;
  
  
  How a Transaction Works
&lt;/h2&gt;

&lt;p&gt;Here is the simple version of what happens when you send SOL:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You initiate the action in your wallet&lt;/li&gt;
&lt;li&gt;The transaction is sent to validators on the network&lt;/li&gt;
&lt;li&gt;Validators confirm it is legitimate and reach agreement&lt;/li&gt;
&lt;li&gt;The transaction is permanently recorded in under a second&lt;/li&gt;
&lt;li&gt;The recipient sees the result immediately&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This happens thousands of times per second, all around the world.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Developers Are Building on Solana
&lt;/h2&gt;

&lt;p&gt;If you are coming from Web2 development, a few things stand out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; — Build apps that feel instant, not laggy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low user costs&lt;/strong&gt; — Your users will not abandon your app over high fees&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composability&lt;/strong&gt; — Existing programs can be combined like building blocks, so you are not starting from zero&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active ecosystem&lt;/strong&gt; — Strong community, growing tooling, and solid developer resources&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Your First Three Steps
&lt;/h2&gt;

&lt;p&gt;Getting started on Solana is simpler than it sounds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set up a wallet&lt;/strong&gt; — Free, and takes just a few minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get a small amount of SOL&lt;/strong&gt; — Enough to cover fees while you explore&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make your first transaction&lt;/strong&gt; — Send SOL or try an app&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You do not need to understand how validators work or what cryptography is happening under the hood. Start using it and the understanding will follow.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to Learn Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set up your first wallet&lt;/strong&gt; — Phantom and Solflare are great starting points&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understand fees and transactions&lt;/strong&gt; — Learn what you are paying for and why it matters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore a Solana app&lt;/strong&gt; — The fastest way to understand it is to use it&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Found this helpful? Follow along for more beginner-friendly Solana content. Next up: setting up your first Solana wallet.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How to Build a SaaS App Faster Without Rebuilding the Same Boilerplate Twice</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Mon, 04 May 2026 17:59:19 +0000</pubDate>
      <link>https://forem.com/thekarlesi/how-to-build-a-saas-app-faster-without-rebuilding-the-same-boilerplate-twice-1nbg</link>
      <guid>https://forem.com/thekarlesi/how-to-build-a-saas-app-faster-without-rebuilding-the-same-boilerplate-twice-1nbg</guid>
      <description>&lt;h2&gt;
  
  
  The Setup That Never Ends
&lt;/h2&gt;

&lt;p&gt;You have the idea. You've validated it — at least enough to start building. You open your terminal, scaffold a new project, and start with what seems obvious: authentication. Three days later, you're knee-deep in JWT configuration, session management edge cases, and an email verification flow that still doesn't work right in Safari. The actual product? Not a single line written.&lt;/p&gt;

&lt;p&gt;This is the trap most developers fall into. Not because they're unproductive — they're often extremely capable — but because building a SaaS product from scratch means solving the same solved problems that thousands of other developers have already solved before you. Every hour spent wiring up Stripe webhooks or configuring protected routes is an hour not spent on the feature that makes your product worth paying for.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Cost of Starting from Zero
&lt;/h2&gt;

&lt;p&gt;The standard "build it yourself" path for a SaaS application looks something like this: spin up a Node/Express backend, configure MongoDB or PostgreSQL, set up a React or Next.js frontend, implement auth (and realize halfway through that you need magic links too), integrate Stripe, build a dashboard, set up role-based access, write deployment configs, and add environment management for multiple environments.&lt;/p&gt;

&lt;p&gt;On paper, none of these tasks are insurmountable. In practice, stringing them together correctly — with security, scalability, and maintainability in mind — takes four to eight weeks for most solo developers. That's eight weeks before you have written a single line of code for the actual value your SaaS delivers.&lt;/p&gt;

&lt;p&gt;The hidden cost isn't just time. It's decision fatigue. Every architectural choice — how to structure routes, where to store tokens, how to scope roles, which payment provider to support — pulls you away from product thinking and into infrastructure thinking. By the time the boilerplate is done, many founders have lost the momentum that made them want to build in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Starter Kits Changed the Game
&lt;/h2&gt;

&lt;p&gt;The shift toward SaaS starter kits wasn't driven by laziness. It was driven by a mature recognition that infrastructure is not differentiation. The same way modern developers don't hand-roll their own HTTP parsers or authentication cryptography, they shouldn't spend weeks reinventing subscription management and onboarding flows.&lt;/p&gt;

&lt;p&gt;SaaS starter kits emerged as a category that packages the non-negotiable plumbing — auth, payments, routing, dashboards, deployment — into a coherent, production-ready base. The best ones are opinionated enough to give you a real head start without being so rigid that you can't extend them for your specific use case.&lt;/p&gt;

&lt;p&gt;The MERN stack (MongoDB, Express, React, Node.js) and Next.js became two dominant foundations for this category, and for good reason. They're fast, well-documented, and supported by massive ecosystems. More importantly, they're what most full-stack SaaS teams are already building with.&lt;/p&gt;




&lt;h2&gt;
  
  
  Breaking Down the Real Bottlenecks
&lt;/h2&gt;

&lt;p&gt;Understanding where time actually gets lost is the first step to recovering it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;Auth feels simple until it isn't. Email/password login is straightforward. But production SaaS apps need more: OAuth (Google, GitHub), magic link flows, JWT refresh logic, session persistence, and secure logout across devices. Add role-based access — free users vs. pro users vs. admins — and auth alone becomes a multi-day project with multiple failure vectors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routing and Protected Pages
&lt;/h3&gt;

&lt;p&gt;In a full-stack SaaS app, not every route should be accessible to every user. Middleware that checks auth state before serving pages, redirects for unauthenticated users, and route guards for premium-only features all need to be architected carefully. Getting this wrong means security gaps or broken user experiences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payments and Billing
&lt;/h3&gt;

&lt;p&gt;Stripe is powerful, but raw Stripe integration is deceptively complex. You need to handle checkout sessions, webhook verification, subscription state sync, plan upgrades and downgrades, trial periods, and failed payment recovery. Miss any one of these and you either lose revenue or break the user's access state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboards and Data Display
&lt;/h3&gt;

&lt;p&gt;The dashboard is usually the first thing a paying user sees after onboarding. It needs to surface meaningful data, be fast, and scale gracefully. Building a clean, reusable dashboard component set from scratch — with charts, tables, and stat cards — takes time that most founders don't have.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment and Environment Management
&lt;/h3&gt;

&lt;p&gt;A SaaS product that can't ship reliably is a prototype, not a product. Vercel deployments, environment-specific config, CI/CD pipelines, and zero-downtime deploys are all part of shipping correctly. Setting this up once is fine. Doing it for every new project is a tax on your time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Onboarding and Roles
&lt;/h3&gt;

&lt;p&gt;Getting a user from signup to their first "aha moment" is a product design problem. But enabling that flow technically — welcome emails, onboarding steps, plan selection at registration, role assignment — is an engineering problem that often gets deprioritized and shipped late.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes When Building SaaS from Scratch
&lt;/h2&gt;

&lt;p&gt;Developers who build everything themselves often make the same category of mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Underestimating auth complexity.&lt;/strong&gt; Starting with a "simple" email/password flow and planning to add OAuth later always costs more than doing it right the first time. The refactor is painful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring webhook reliability.&lt;/strong&gt; Stripe webhooks fail. They get retried. If your handler isn't idempotent, you'll charge users twice or fail to upgrade their access. This is usually discovered in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coupling auth and billing logic.&lt;/strong&gt; When subscription state and user roles are managed in two different places without a clean sync layer, edge cases multiply. Users on cancelled plans who still have admin access. Free users who somehow unlocked paid features. These are embarrassing bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skipping environment separation.&lt;/strong&gt; Development, staging, and production need separate environment configs. Developers who skip staging ship more bugs to production and have no safe place to test Stripe webhooks or third-party integrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building the dashboard last.&lt;/strong&gt; The user-facing dashboard is often treated as a "nice to have" while backend logic gets prioritized. But investors and early users judge the product on what they can see. Shipping the dashboard earlier gives you faster feedback.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pro Tips for Shipping SaaS Faster
&lt;/h2&gt;

&lt;p&gt;A few senior-level patterns that compress the timeline significantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with a working auth implementation, not a minimal one.&lt;/strong&gt; Adding OAuth and magic links later is a major refactor. Implement the full auth surface area you'll need on day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a single source of truth for subscription state.&lt;/strong&gt; Sync Stripe subscription status to your database on every webhook event. Query your database, not Stripe's API, when checking access. This is faster, cheaper, and more reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build role-based access as middleware from the start.&lt;/strong&gt; Route-level authorization that checks roles before business logic runs is far easier to maintain than scattered conditional checks throughout your codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat deployment as a product requirement.&lt;/strong&gt; If you can't ship in one command or one push, fix that before you add features. Deployment friction compounds.&lt;/p&gt;

&lt;p&gt;If you're using Next.js and considering how to structure your subscription billing logic, the approach to &lt;a href="https://sassypack.collabtower.com/blog/how-to-add-new-payment-plans-in-sassypack" rel="noopener noreferrer"&gt;adding payment plans to your SaaS&lt;/a&gt; matters enormously for long-term maintainability — designing for plan extensibility from the start prevents painful refactors later.&lt;/p&gt;




&lt;h2&gt;
  
  
  How SassyPack Removes the Bottlenecks
&lt;/h2&gt;

&lt;p&gt;SassyPack is a full MERN and Next.js SaaS starter kit that ships with authentication, payments, routing, dashboards, and deployment already configured. It's built specifically to eliminate the boilerplate phase and get developers to product work on day one.&lt;/p&gt;

&lt;p&gt;Instead of spending week one on JWT setup and week two on Stripe webhooks, you clone the kit, configure your environment variables, and deploy. Auth — including OAuth and magic links — is wired in. Stripe and Paystack billing are preconfigured, with webhook handling, subscription sync, and plan management ready to extend. Role-based access control is implemented at the middleware level. A clean, modular dashboard is included. Vercel deployment is configured out of the box.&lt;/p&gt;

&lt;p&gt;The result is a realistic compression from four to eight weeks of setup down to hours. Developers who've used well-structured SaaS starter kits report shipping their first paying customer 10× faster than when building from scratch — not because the kit does their product work for them, but because it stops them from doing the same infrastructure work twice.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Real-World Build Scenario
&lt;/h2&gt;

&lt;p&gt;Consider a solo developer building a fitness SaaS — a tool for personal trainers to manage clients, assign workout plans, and collect recurring payments. Without a starter kit, the path looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Week 1–2: Auth setup, email verification, OAuth&lt;/li&gt;
&lt;li&gt;Week 3: Stripe subscription integration&lt;/li&gt;
&lt;li&gt;Week 4: Dashboard, protected routes, role system (trainer vs. client)&lt;/li&gt;
&lt;li&gt;Week 5: Deployment and environment config&lt;/li&gt;
&lt;li&gt;Week 6+: Actual product features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a solid foundation already in place, that same developer can skip directly to the product work. The trainer/client role system maps cleanly onto an existing RBAC layer. Payment plans for monthly or per-session billing extend an already-wired Stripe integration. The dashboard becomes a canvas for workout tracking data rather than a component built from scratch.&lt;/p&gt;

&lt;p&gt;The go-to-market timeline compresses from months to weeks. That's not a minor efficiency — it's the difference between launching and stalling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Action Plan: From Idea to Deployed SaaS
&lt;/h2&gt;

&lt;p&gt;If you want to compress your path to a live, billing product, here's what to do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Choose your stack deliberately.&lt;/strong&gt; MERN with Next.js gives you SSR, file-based routing, and a single unified codebase. Pick it if you want maximum ecosystem support and deployment flexibility.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with auth and payments configured, not planned.&lt;/strong&gt; If your base doesn't have working auth and Stripe integration on day one, you will spend your first sprint on infrastructure instead of product.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Design your role system before you need it.&lt;/strong&gt; Even if you only have one user type at launch, build the access layer to support multiple roles. Retrofitting RBAC into a live codebase is a painful, risky process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deploy early and deploy often.&lt;/strong&gt; Get your app live on a real URL before you've written product features. This forces good deployment hygiene and gives you a URL to share for early feedback.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat the dashboard as a first-class feature.&lt;/strong&gt; Users judge their experience by what they see. A clean, functional dashboard increases activation rates and reduces churn even before you've built all the core features.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use a starter kit that matches your production stack.&lt;/strong&gt; A kit built on a different framework or with a different database than your intended stack will slow you down, not speed you up.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Understanding &lt;a href="https://sassypack.collabtower.com/blog/why-devs-waste-weeks-building-boilerplate" rel="noopener noreferrer"&gt;why so many developers waste weeks on boilerplate&lt;/a&gt; is the first step to not repeating that pattern on your next project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stop Rebuilding. Start Shipping.
&lt;/h2&gt;

&lt;p&gt;The developers who ship fastest aren't the ones who write the most code — they're the ones who write the least unnecessary code. Every hour spent on auth middleware, Stripe webhook handlers, or dashboard scaffolding is an hour taken from the product differentiation that actually drives growth.&lt;/p&gt;

&lt;p&gt;If you're ready to skip the boilerplate phase and get to building what actually matters, &lt;a href="https://sassypack.collabtower.com/" rel="noopener noreferrer"&gt;SassyPack&lt;/a&gt; gives you a production-ready MERN and Next.js foundation with auth, payments, dashboards, and deployment already wired in — so your first commit can be a product feature, not a config file.&lt;/p&gt;

</description>
      <category>react</category>
      <category>mongodb</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Google OAuth + Email Auth in Next.js — The Complete SaaS Authentication Guide (2026)</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 24 Mar 2026 14:32:00 +0000</pubDate>
      <link>https://forem.com/thekarlesi/google-oauth-email-auth-in-nextjs-the-complete-saas-authentication-guide-2026-1ja</link>
      <guid>https://forem.com/thekarlesi/google-oauth-email-auth-in-nextjs-the-complete-saas-authentication-guide-2026-1ja</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Authentication and Security&lt;br&gt;
&lt;strong&gt;Primary Keyword:&lt;/strong&gt; Google OAuth email authentication Next.js SaaS&lt;br&gt;
&lt;strong&gt;Level:&lt;/strong&gt; Beginner to Intermediate&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Authentication is the front door of your SaaS. Get it wrong and users can't get in — or worse, the wrong users get in. Get it right and it becomes invisible: a smooth, trusted experience that sets the tone for everything that follows.&lt;/p&gt;

&lt;p&gt;This guide covers the full authentication setup for a Next.js App Router SaaS: email and password login, Google OAuth, session management, protected routes, and secure API access. By the end, you'll have a production-ready auth layer you can drop into any project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F55frseqx3n1w6a8tzwo3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F55frseqx3n1w6a8tzwo3.jpg" alt="Login and signup forms for web app authentication" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Authentication Is More Than Just a Login Form
&lt;/h2&gt;

&lt;p&gt;Most tutorials show you how to render a login form. But in a real SaaS, authentication touches nearly every layer of your app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signup&lt;/strong&gt; — creating a user record in your database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Login&lt;/strong&gt; — verifying credentials and issuing a session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OAuth&lt;/strong&gt; — delegating identity verification to Google&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session&lt;/strong&gt; — persisting who is logged in across requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route protection&lt;/strong&gt; — blocking unauthenticated access to the dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API protection&lt;/strong&gt; — securing your backend routes from anonymous requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role handling&lt;/strong&gt; — distinguishing free users from paid subscribers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NextAuth.js (now Auth.js) handles most of this out of the box. Your job is to configure it correctly and wire it to your database and UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up NextAuth.js in the App Router
&lt;/h2&gt;

&lt;p&gt;Start by installing the required packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;next-auth@beta @auth/mongodb-adapter mongoose bcryptjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Use &lt;code&gt;next-auth@beta&lt;/code&gt; for full App Router support. The stable v4 has limited support for the App Router's server components and route handlers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Create your auth configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/authOptions.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;NextAuth&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;GoogleProvider&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next-auth/providers/google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;CredentialsProvider&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next-auth/providers/credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MongoDBAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@auth/mongodb-adapter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;clientPromise&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/mongodbClient&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;connectDB&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/mongodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/models/User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bcryptjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MongoDBAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientPromise&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;GoogleProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GOOGLE_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nc"&gt;CredentialsProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connectDB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jwt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXTAUTH_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signOut&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NextAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then expose the route handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/auth/[...nextauth]/route.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/authOptions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single file handles every auth request — login, logout, OAuth callbacks, session checks — automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fdeveloper-coding-laptop.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fdeveloper-coding-laptop.jpg" alt="Developer coding on laptop with code editor open" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuring Environment Variables
&lt;/h2&gt;

&lt;p&gt;Add these to your &lt;code&gt;.env.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_random_secret_here

GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret

MONGODB_URI=mongodb+srv://...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To generate a secure &lt;code&gt;NEXTAUTH_SECRET&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;GOOGLE_CLIENT_ID&lt;/code&gt; and &lt;code&gt;GOOGLE_CLIENT_SECRET&lt;/code&gt;, head to &lt;a href="https://console.cloud.google.com" rel="noopener noreferrer"&gt;console.cloud.google.com&lt;/a&gt;, create a project, enable the Google OAuth API, and add &lt;code&gt;http://localhost:3000/api/auth/callback/google&lt;/code&gt; as an authorized redirect URI.&lt;/p&gt;




&lt;h2&gt;
  
  
  The User Model: Bridging Auth and Your Database
&lt;/h2&gt;

&lt;p&gt;NextAuth's MongoDB adapter automatically creates &lt;code&gt;accounts&lt;/code&gt;, &lt;code&gt;sessions&lt;/code&gt;, and &lt;code&gt;verification_tokens&lt;/code&gt; collections. But you also need a &lt;code&gt;users&lt;/code&gt; collection that your app controls — for subscription status, preferences, and other SaaS-specific data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// models/User.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mongoose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// null for OAuth users&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;stripeCustomerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;subscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;past_due&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canceled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trialing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key design decision:&lt;/strong&gt; OAuth users have no &lt;code&gt;password&lt;/code&gt; field. Email users have a hashed password. Your &lt;code&gt;authorize&lt;/code&gt; function in CredentialsProvider checks for &lt;code&gt;user.password&lt;/code&gt; before attempting bcrypt comparison — this prevents OAuth-only accounts from logging in via the credentials form.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Signup Flow for Email Users
&lt;/h2&gt;

&lt;p&gt;OAuth handles its own user creation automatically. For email/password, you need a signup API route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/auth/signup/route.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;connectDB&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/mongodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/models/User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bcryptjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connectDB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email already registered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hashed&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth noting here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always hash passwords&lt;/strong&gt; with bcrypt before storing. Never store plaintext.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salt rounds of 12&lt;/strong&gt; is the current recommended minimum for production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return generic errors&lt;/strong&gt; on failure — don't confirm whether an email exists to prevent enumeration attacks.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Building the Login and Signup UI
&lt;/h2&gt;

&lt;p&gt;With NextAuth configured, your login page just needs to call &lt;code&gt;signIn&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/auth/login/page.jsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signIn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next-auth/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/navigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LoginPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPassword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleEmailLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid email or password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"min-h-screen flex items-center justify-center bg-base-200"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card w-full max-w-sm shadow-xl bg-base-100"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card-body"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card-title text-2xl font-bold"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Sign in&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"alert alert-error text-sm"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleEmailLogin&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col gap-3"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
              &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt;
              &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"input input-bordered"&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;required&lt;/span&gt;
            &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
              &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Password"&lt;/span&gt;
              &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"input input-bordered"&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;required&lt;/span&gt;
            &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-primary"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              Sign in with Email
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"divider"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;OR&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
            &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;callbackUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-outline gap-2"&lt;/span&gt;
          &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"w-5 h-5"&lt;/span&gt; &lt;span class="na"&gt;viewBox&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#4285F4"&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"&lt;/span&gt;&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#34A853"&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"&lt;/span&gt;&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#FBBC05"&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"&lt;/span&gt;&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#EA4335"&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"&lt;/span&gt;&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            Continue with Google
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-center text-sm mt-2"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            Don't have an account?&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/auth/signup"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"link link-primary"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Sign up&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a clean DaisyUI login card with both authentication methods, error handling, and a redirect to the dashboard on success.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fnextjs-tailwind-code-snippet.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fnextjs-tailwind-code-snippet.jpg" alt="Code snippet of Next.js and Tailwind project" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Protecting Routes with Middleware
&lt;/h2&gt;

&lt;p&gt;The cleanest way to protect your dashboard is with Next.js middleware. It runs before any page renders and can redirect unauthenticated users server-side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.js (root of project)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/authOptions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isLoggedIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isOnDashboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isOnDashboard&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard/:path*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a single file that silently redirects any unauthenticated request to &lt;code&gt;/dashboard&lt;/code&gt; to the login page — before a single byte of the dashboard renders. No client-side flicker, no layout shift.&lt;/p&gt;




&lt;h2&gt;
  
  
  Securing API Routes
&lt;/h2&gt;

&lt;p&gt;For API routes in your SaaS backend, check the session at the top of every handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/user/profile/route.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getServerSession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authOptions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/authOptions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;connectDB&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/mongodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/models/User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getServerSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connectDB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always exclude the &lt;code&gt;password&lt;/code&gt; field when returning user data. The &lt;code&gt;.select('-password')&lt;/code&gt; Mongoose modifier ensures it never leaves your server.&lt;/p&gt;




&lt;h2&gt;
  
  
  How This Fits Into the Zero to SaaS Journey
&lt;/h2&gt;

&lt;p&gt;Authentication is the second major milestone in building a SaaS — right after project setup and right before connecting your database and billing layer. Without it, you can't identify who your users are, protect their data, or gate features behind a subscription.&lt;/p&gt;

&lt;p&gt;If you're working through the &lt;a href="https://zero-to-saas.collabtower.com/" rel="noopener noreferrer"&gt;Zero to SaaS&lt;/a&gt; course, this is where things start feeling real. You have a login page, a Google OAuth button, and a dashboard that only authenticated users can see. That's a real product foundation.&lt;/p&gt;

&lt;p&gt;Once auth is working, the next step is wiring up Stripe subscriptions so you can start charging for access. Check out &lt;a href="https://zero-to-saas.collabtower.com/blog/build-saas-with-nextjs-and-stripe" rel="noopener noreferrer"&gt;Build SaaS with Next.js and Stripe&lt;/a&gt; to continue the journey, or start from the beginning with the &lt;a href="https://zero-to-saas.collabtower.com/blog/nextjs-saas-tutorial-complete-guide" rel="noopener noreferrer"&gt;full Next.js SaaS tutorial&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes to Avoid
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Using &lt;code&gt;strategy: 'database'&lt;/code&gt; with the App Router&lt;/strong&gt;&lt;br&gt;
Database sessions require the adapter on every request, which adds latency. Use &lt;code&gt;strategy: 'jwt'&lt;/code&gt; for serverless environments like Vercel — it's faster and scales better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Not hashing passwords&lt;/strong&gt;&lt;br&gt;
This one is obvious but still happens. Always use bcrypt. Never MD5, never SHA-1, never plaintext.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Allowing credential login for OAuth-only accounts&lt;/strong&gt;&lt;br&gt;
If a user signed up with Google, they have no password. Your &lt;code&gt;authorize&lt;/code&gt; function must check for &lt;code&gt;user.password&lt;/code&gt; before calling &lt;code&gt;bcrypt.compare&lt;/code&gt;, or it will throw a runtime error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Forgetting to add the callback URL to Google Cloud Console&lt;/strong&gt;&lt;br&gt;
For production, you must add &lt;code&gt;https://yourdomain.com/api/auth/callback/google&lt;/code&gt; to the authorized redirect URIs in Google Cloud. Missing this is the number one reason OAuth fails after deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Exposing session data on the client without filtering&lt;/strong&gt;&lt;br&gt;
The JWT callback adds &lt;code&gt;token.id&lt;/code&gt; to the session. Be deliberate about what ends up in the session object — don't forward sensitive fields like subscription details or internal IDs that clients don't need.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pro Tips for Production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add email verification&lt;/strong&gt; for new email signups using NextAuth's built-in email provider or a service like Resend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit your signup and login routes&lt;/strong&gt; to prevent brute force attacks. Upstash Redis with &lt;code&gt;@upstash/ratelimit&lt;/code&gt; is a clean serverless solution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;httpOnly&lt;/code&gt; and &lt;code&gt;secure&lt;/code&gt; cookie flags&lt;/strong&gt; — NextAuth does this by default in production when &lt;code&gt;NEXTAUTH_URL&lt;/code&gt; uses HTTPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log auth events&lt;/strong&gt; — failed logins and new signups are worth tracking. Wire &lt;code&gt;events.signIn&lt;/code&gt; and &lt;code&gt;events.createUser&lt;/code&gt; in your NextAuth config to a logging service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test OAuth locally with ngrok&lt;/strong&gt; if you need a real HTTPS callback URL for development.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real-World Example: DevTrack SaaS
&lt;/h2&gt;

&lt;p&gt;You're building DevTrack — a time tracking SaaS for freelance developers. Here's how auth plays out for two different users:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User A — Sarah (Google OAuth):&lt;/strong&gt;&lt;br&gt;
Sarah clicks "Continue with Google," approves the OAuth consent screen, and lands on the dashboard. NextAuth creates her user record automatically via the MongoDB adapter. No password stored. Her &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;image&lt;/code&gt; are pulled from her Google profile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User B — James (Email/Password):&lt;/strong&gt;&lt;br&gt;
James fills out the signup form with his email and a password. Your API route hashes it with bcrypt and saves it to MongoDB. He then logs in with those credentials. NextAuth's CredentialsProvider verifies the hash and issues a JWT session.&lt;/p&gt;

&lt;p&gt;Both users end up with a session that contains &lt;code&gt;{ id, email, name }&lt;/code&gt;. Your dashboard doesn't need to care how they authenticated — it just checks &lt;code&gt;session.user&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Action Plan: What to Build Next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;✅ Install &lt;code&gt;next-auth@beta&lt;/code&gt;, &lt;code&gt;@auth/mongodb-adapter&lt;/code&gt;, and &lt;code&gt;bcryptjs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ Create &lt;code&gt;lib/authOptions.js&lt;/code&gt; with Google and Credentials providers&lt;/li&gt;
&lt;li&gt;✅ Add all required environment variables to &lt;code&gt;.env.local&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ Set up Google OAuth credentials in Google Cloud Console&lt;/li&gt;
&lt;li&gt;✅ Build the signup API route with bcrypt hashing&lt;/li&gt;
&lt;li&gt;✅ Create the login page with email form and Google button&lt;/li&gt;
&lt;li&gt;✅ Add middleware to protect &lt;code&gt;/dashboard&lt;/code&gt; routes&lt;/li&gt;
&lt;li&gt;✅ Secure all API routes with &lt;code&gt;getServerSession&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🔜 Add email verification for new signups&lt;/li&gt;
&lt;li&gt;🔜 Connect auth to your subscription status check&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Authentication in a Next.js SaaS isn't complicated — but it has a lot of moving parts that need to work together. You now have both email/password and Google OAuth wired up, a MongoDB-backed user model, protected routes via middleware, and secure API handlers.&lt;/p&gt;

&lt;p&gt;The pattern is consistent across every route and component: get the session, check it exists, use it to identify the user. Everything else — subscriptions, dashboards, roles — builds on top of this foundation.&lt;/p&gt;

&lt;p&gt;If you want to build this complete authentication layer as part of a full SaaS app — including Stripe billing, a Tailwind dashboard, and deployment to Vercel — the &lt;a href="https://zero-to-saas.collabtower.com/" rel="noopener noreferrer"&gt;Zero to SaaS course&lt;/a&gt; walks you through it all, step by step.&lt;/p&gt;

&lt;p&gt;The door is open. Let your users in.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>authentication</category>
      <category>oauth</category>
      <category>saas</category>
    </item>
    <item>
      <title>Stripe Subscription Lifecycle in Next.js — The Complete Developer Guide (2026)</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 17 Mar 2026 13:26:46 +0000</pubDate>
      <link>https://forem.com/thekarlesi/stripe-subscription-lifecycle-in-nextjs-the-complete-developer-guide-2026-4l9d</link>
      <guid>https://forem.com/thekarlesi/stripe-subscription-lifecycle-in-nextjs-the-complete-developer-guide-2026-4l9d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Category:&lt;/strong&gt; Stripe Payments and Billing&lt;br&gt;
&lt;strong&gt;Primary Keyword:&lt;/strong&gt; Stripe subscription lifecycle Next.js&lt;br&gt;
&lt;strong&gt;Level:&lt;/strong&gt; Intermediate&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Most tutorials show you how to &lt;em&gt;add&lt;/em&gt; Stripe to a Next.js app. Few show you what happens &lt;em&gt;after&lt;/em&gt; the user subscribes — the renewals, failures, cancellations, and recovery flows that make or break a real SaaS business.&lt;/p&gt;

&lt;p&gt;This guide walks you through the complete Stripe subscription lifecycle: from checkout session to webhook handling, customer portal integration, and automated churn recovery. If you're building a production SaaS, this is the article you wish you had on day one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fstripe-payments-checkout.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fstripe-payments-checkout.jpg" alt="Stripe payment integration checkout page illustration" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the Lifecycle Matters More Than the Checkout
&lt;/h2&gt;

&lt;p&gt;When developers first integrate Stripe, they focus on the checkout form. Makes sense — you want to see money coming in. But a SaaS business lives and dies by what happens &lt;em&gt;between&lt;/em&gt; payments.&lt;/p&gt;

&lt;p&gt;Here's the reality of a subscription over 12 months:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A user subscribes (checkout)&lt;/li&gt;
&lt;li&gt;Stripe charges them monthly (renewals)&lt;/li&gt;
&lt;li&gt;A card expires or gets declined (payment failure)&lt;/li&gt;
&lt;li&gt;Stripe retries and sends dunning emails (recovery)&lt;/li&gt;
&lt;li&gt;The user wants to upgrade or downgrade (plan change)&lt;/li&gt;
&lt;li&gt;The user cancels (churn)&lt;/li&gt;
&lt;li&gt;The subscription ends at period end (access revocation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app doesn't handle each of these events, you'll have users with broken access, missed revenue, and no way to debug any of it. Let's fix that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up: What You Need Before Writing Code
&lt;/h2&gt;

&lt;p&gt;Before diving into webhooks and lifecycle events, make sure your Next.js project has these pieces in place:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stripe account&lt;/strong&gt; with at least one Product and Price created in the dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe Node SDK&lt;/strong&gt; installed: &lt;code&gt;npm install stripe&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt; configured:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A &lt;code&gt;users&lt;/code&gt; collection in MongoDB&lt;/strong&gt; with a field for &lt;code&gt;stripeCustomerId&lt;/code&gt; and &lt;code&gt;subscriptionStatus&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your user schema in Mongoose might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// models/User.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mongoose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;stripeCustomerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;subscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;past_due&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canceled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trialing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;incomplete&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;currentPeriodEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;mongoose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema is the source of truth your app reads from. Stripe is the source of truth Stripe reads from. Your webhooks keep them in sync.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fdeveloper-coding-laptop.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fdeveloper-coding-laptop.jpg" alt="Developer coding on laptop with code editor open" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Creating the Checkout Session
&lt;/h2&gt;

&lt;p&gt;When a user clicks "Subscribe," you create a Stripe Checkout Session via a Server Action or API route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/stripe/checkout/route.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getServerSession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authOptions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/authOptions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/models/User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;connectDB&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/mongodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connectDB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getServerSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Create or retrieve the Stripe customer&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stripeCustomerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;customerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stripeCustomerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkoutSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;payment_method_types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_PRICE_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;success_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/dashboard?success=true`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cancel_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/pricing`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checkoutSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key point:&lt;/strong&gt; Always attach a &lt;code&gt;customer&lt;/code&gt; ID to the session. This links the Stripe customer to your user record and makes every downstream webhook event traceable back to a user in your database.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Handling Webhooks: The Heart of the Lifecycle
&lt;/h2&gt;

&lt;p&gt;Webhooks are how Stripe tells your app what happened. You don't poll Stripe — Stripe calls you. This means your webhook endpoint must be fast, idempotent, and reliable.&lt;/p&gt;

&lt;p&gt;Here's the webhook handler that covers the full subscription lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/stripe/webhook/route.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/headers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;connectDB&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/mongodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/models/User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Webhook error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connectDB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout.session.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stripeCustomerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice.paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stripeCustomerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;subscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;currentPeriodEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_period_end&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice.payment_failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stripeCustomerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;subscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;past_due&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Optionally: trigger email notification here&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer.subscription.deleted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stripeCustomerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;subscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canceled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer.subscription.updated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stripeCustomerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;subscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;currentPeriodEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current_period_end&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Five Events You Must Handle
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;What It Means&lt;/th&gt;
&lt;th&gt;What You Do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checkout.session.completed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User subscribed&lt;/td&gt;
&lt;td&gt;Activate account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;invoice.paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Renewal succeeded&lt;/td&gt;
&lt;td&gt;Extend &lt;code&gt;currentPeriodEnd&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;invoice.payment_failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Card declined&lt;/td&gt;
&lt;td&gt;Set status to &lt;code&gt;past_due&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customer.subscription.deleted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sub canceled or expired&lt;/td&gt;
&lt;td&gt;Revoke access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customer.subscription.updated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Plan changed or paused&lt;/td&gt;
&lt;td&gt;Sync status and dates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Missing any of these means your database will drift out of sync with Stripe — and users will either have access they shouldn't, or lose access they paid for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fsaas-dashboard-analytics.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fsaas-dashboard-analytics.jpg" alt="SaaS dashboard showing analytics and user stats" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Protecting Routes Based on Subscription Status
&lt;/h2&gt;

&lt;p&gt;Once your webhook is syncing subscription status to MongoDB, protecting routes is straightforward. In your middleware or layout server component, check the user's &lt;code&gt;subscriptionStatus&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/getSubscriptionStatus.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getServerSession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authOptions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/authOptions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/models/User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;connectDB&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/mongodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSubscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connectDB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getServerSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;subscriptionStatus&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your dashboard layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/dashboard/layout.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;redirect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/navigation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getSubscriptionStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/getSubscriptionStatus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DashboardLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSubscriptionStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;trialing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/pricing?reason=inactive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is clean, server-side, and requires zero client JavaScript. The redirect happens before the page renders.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — The Customer Portal: Self-Service Billing
&lt;/h2&gt;

&lt;p&gt;Instead of building a custom cancel or upgrade flow, use Stripe's hosted Customer Portal. It handles plan changes, payment method updates, and cancellations out of the box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/stripe/portal/route.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getServerSession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next-auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authOptions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/authOptions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/models/User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;connectDB&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/mongodb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connectDB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getServerSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;portalSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;billingPortal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stripeCustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;return_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_APP_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/dashboard/billing`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;portalSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a "Manage Billing" button in your dashboard that calls this endpoint and redirects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;openPortal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/stripe/portal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;openPortal&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-outline"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Manage Billing
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a user cancels through the portal, Stripe fires &lt;code&gt;customer.subscription.updated&lt;/code&gt; (with &lt;code&gt;cancel_at_period_end: true&lt;/code&gt;) and eventually &lt;code&gt;customer.subscription.deleted&lt;/code&gt; — both of which your webhook already handles.&lt;/p&gt;




&lt;h2&gt;
  
  
  How This Fits Into the Zero to SaaS Journey
&lt;/h2&gt;

&lt;p&gt;The subscription lifecycle is where your SaaS becomes a real business. Authentication gets users in the door. The dashboard keeps them engaged. But billing is where value exchange happens — and where most indie SaaS apps break down in subtle, expensive ways.&lt;/p&gt;

&lt;p&gt;If you're following the &lt;a href="https://zero-to-saas.collabtower.com/" rel="noopener noreferrer"&gt;Zero to SaaS&lt;/a&gt; course, this lesson sits at the midpoint: after authentication and database setup, before deployment. Getting this right before you ship means you won't be debugging ghost subscriptions at 2am after your first launch.&lt;/p&gt;

&lt;p&gt;For a deeper look at setting up subscriptions from scratch, check out &lt;a href="https://zero-to-saas.collabtower.com/blog/stripe-subscriptions-in-nextjs" rel="noopener noreferrer"&gt;Stripe Subscriptions in Next.js&lt;/a&gt; and &lt;a href="https://zero-to-saas.collabtower.com/blog/build-saas-with-nextjs-and-stripe" rel="noopener noreferrer"&gt;Build SaaS with Next.js and Stripe&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fdeveloper-building-saas.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzero-to-saas.collabtower.com%2Fimages%2Farticles%2Fdeveloper-building-saas.jpg" alt="Developer building a SaaS app using modern web technologies" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes to Avoid
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Not verifying webhook signatures&lt;/strong&gt;&lt;br&gt;
Always use &lt;code&gt;stripe.webhooks.constructEvent()&lt;/code&gt;. Without this, anyone can POST to your webhook endpoint and fake events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Using &lt;code&gt;checkout.session.completed&lt;/code&gt; as the only activation trigger&lt;/strong&gt;&lt;br&gt;
Checkout fires once. &lt;code&gt;invoice.paid&lt;/code&gt; fires every renewal. If you only listen to checkout, your users lose access after the first billing cycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Storing subscription data only in Stripe&lt;/strong&gt;&lt;br&gt;
Your app should have a local copy of subscription status. Querying Stripe on every request adds latency and rate limit risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Not handling &lt;code&gt;cancel_at_period_end&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
When a user cancels, Stripe sets this flag to &lt;code&gt;true&lt;/code&gt; but keeps the subscription &lt;code&gt;active&lt;/code&gt; until the period ends. If you immediately revoke access on cancel, you're giving a worse experience than Stripe's own dashboard promises the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Forgetting to test with the Stripe CLI&lt;/strong&gt;&lt;br&gt;
Run &lt;code&gt;stripe listen --forward-to localhost:3000/api/stripe/webhook&lt;/code&gt; locally. Without this, you'll be deploying blind.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pro Tips for Production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency&lt;/strong&gt;: Stripe can send the same event more than once. Use &lt;code&gt;event.id&lt;/code&gt; to deduplicate if needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry tolerance&lt;/strong&gt;: Your webhook handler must return &lt;code&gt;200&lt;/code&gt; quickly. Move heavy work (emails, database jobs) to a background queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor failed webhooks&lt;/strong&gt;: Stripe Dashboard &amp;gt; Developers &amp;gt; Webhooks shows every delivery attempt and failure reason. Check it after every deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grace period logic&lt;/strong&gt;: Give &lt;code&gt;past_due&lt;/code&gt; users 3–5 days before revoking access. Stripe's Smart Retries often recover these automatically.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real-World Example: TaskFlow SaaS
&lt;/h2&gt;

&lt;p&gt;Imagine you're building TaskFlow — a project management SaaS with a $19/month Pro plan.&lt;/p&gt;

&lt;p&gt;A user named Marcus subscribes on March 1st. Here's what your system processes over the next 60 days:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;March 1&lt;/strong&gt; — &lt;code&gt;checkout.session.completed&lt;/code&gt; → Marcus's status set to &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;currentPeriodEnd&lt;/code&gt; = April 1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;April 1&lt;/strong&gt; — &lt;code&gt;invoice.paid&lt;/code&gt; → Status remains &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;currentPeriodEnd&lt;/code&gt; updated to May 1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;May 1&lt;/strong&gt; — &lt;code&gt;invoice.payment_failed&lt;/code&gt; → Card expired. Status → &lt;code&gt;past_due&lt;/code&gt;. Stripe retries on May 4, May 8&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;May 8&lt;/strong&gt; — &lt;code&gt;invoice.paid&lt;/code&gt; (retry success) → Status back to &lt;code&gt;active&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;May 20&lt;/strong&gt; — Marcus opens portal, cancels. &lt;code&gt;customer.subscription.updated&lt;/code&gt; fires with &lt;code&gt;cancel_at_period_end: true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;June 1&lt;/strong&gt; — &lt;code&gt;customer.subscription.deleted&lt;/code&gt; → Status → &lt;code&gt;canceled&lt;/code&gt;. Dashboard access removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every step is automatic. Zero manual intervention. That's the power of a properly wired lifecycle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Action Plan: What to Build Next
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;✅ Create a Stripe Product and Price in your dashboard&lt;/li&gt;
&lt;li&gt;✅ Add &lt;code&gt;stripeCustomerId&lt;/code&gt; and &lt;code&gt;subscriptionStatus&lt;/code&gt; fields to your User model&lt;/li&gt;
&lt;li&gt;✅ Build the &lt;code&gt;/api/stripe/checkout&lt;/code&gt; route&lt;/li&gt;
&lt;li&gt;✅ Deploy the webhook handler and verify with Stripe CLI&lt;/li&gt;
&lt;li&gt;✅ Add subscription status check to your dashboard layout&lt;/li&gt;
&lt;li&gt;✅ Wire up the Customer Portal endpoint&lt;/li&gt;
&lt;li&gt;🔜 Add an email notification on &lt;code&gt;invoice.payment_failed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🔜 Build a usage dashboard showing &lt;code&gt;currentPeriodEnd&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The Stripe subscription lifecycle isn't just a technical concern — it's your revenue pipeline. Every event maps to a moment in your customer's journey, and how you handle each one determines whether your SaaS feels professional or broken.&lt;/p&gt;

&lt;p&gt;You now have the complete picture: checkout, renewals, failures, recovery, plan changes, and cancellations — all wired to your Next.js App Router app through a single, robust webhook handler.&lt;/p&gt;

&lt;p&gt;If you want to go deeper and build this end-to-end alongside a full SaaS app — with authentication, a MongoDB backend, Tailwind UI, and Vercel deployment — the &lt;a href="https://zero-to-saas.collabtower.com/" rel="noopener noreferrer"&gt;Zero to SaaS course&lt;/a&gt; walks you through every step in sequence, with real code and real decisions.&lt;/p&gt;

&lt;p&gt;The billing layer is ready. Time to ship.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>stripe</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Beyond find(): Mastering MongoDB Aggregations for Real-Time SaaS Analytics</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 24 Feb 2026 09:04:22 +0000</pubDate>
      <link>https://forem.com/thekarlesi/beyond-find-mastering-mongodb-aggregations-for-real-time-saas-analytics-2lgc</link>
      <guid>https://forem.com/thekarlesi/beyond-find-mastering-mongodb-aggregations-for-real-time-saas-analytics-2lgc</guid>
      <description>&lt;p&gt;Every successful SaaS reaches a point where simple CRUD operations are no longer enough. Your users want to see data: revenue growth, active user trends, or usage heatmaps. If you are fetching thousands of documents just to calculate a total in JavaScript, you are killing your application's performance.&lt;/p&gt;

&lt;p&gt;In 2026, the senior-level approach is to push the computation to the database. By mastering &lt;strong&gt;MongoDB Aggregation Pipelines&lt;/strong&gt;, you can transform millions of raw data points into actionable insights in milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Client-Side Logic
&lt;/h2&gt;

&lt;p&gt;Imagine you have 50,000 transaction records. You want to display a chart of "Monthly Recurring Revenue (MRR) per Region." &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Junior Way:&lt;/strong&gt; Fetch all 50,000 docs to the frontend, loop through them, and group by region. This will crash the user's browser and eat your bandwidth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Senior Way:&lt;/strong&gt; Use an aggregation pipeline to filter, group, and sum the data directly on the database server, returning only the final 5-10 rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deep Dive: Anatomy of a SaaS Analytics Pipeline
&lt;/h2&gt;

&lt;p&gt;A powerful aggregation pipeline is built in stages. Here is how you should structure your queries for maximum efficiency.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The $match Stage (The Filter)
&lt;/h3&gt;

&lt;p&gt;Always filter as early as possible. If you only need data from the last 30 days, discard everything else first to reduce the memory footprint of the remaining stages.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The $group Stage (The Engine)
&lt;/h3&gt;

&lt;p&gt;This is where the magic happens. You can group by a specific field (like &lt;code&gt;planId\&lt;/code&gt;) and use accumulators like &lt;code&gt;$sum\&lt;/code&gt;, &lt;code&gt;$avg\&lt;/code&gt;, or &lt;code&gt;$max\&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The $project Stage (The Refinement)
&lt;/h3&gt;

&lt;p&gt;Don't return the raw MongoDB structure. Use &lt;code&gt;$project\&lt;/code&gt; to rename fields and format data so it is ready for your frontend charting library (like Recharts or Chart.js) without further manipulation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Benefits of Aggregation
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server-Side Compute&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low Latency&lt;/td&gt;
&lt;td&gt;Users get data-heavy dashboards instantly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reduced Payload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Saved Bandwidth&lt;/td&gt;
&lt;td&gt;Mobile users won't drain data loading your app.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Complex Logic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clean Code&lt;/td&gt;
&lt;td&gt;Keep your Next.js API routes small and readable.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  3 Pro-Level Aggregation Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A. Calculating Churn Rate in One Query
&lt;/h3&gt;

&lt;p&gt;You can use a &lt;code&gt;$facet\&lt;/code&gt; stage to run multiple pipelines simultaneously—one counting total users and another counting users with a &lt;code&gt;canceled\&lt;/code&gt; status—to calculate your churn percentage in a single trip to the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  B. Time-Series Bucketization
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;$bucket\&lt;/code&gt; or &lt;code&gt;$dateTrunc\&lt;/code&gt; operators to group events into days, weeks, or months. This is essential for building "Growth Over Time" line graphs.&lt;/p&gt;

&lt;h3&gt;
  
  
  C. Join Logic with $lookup
&lt;/h3&gt;

&lt;p&gt;While MongoDB is NoSQL, you often need to join collections (e.g., joining &lt;code&gt;Transactions\&lt;/code&gt; with &lt;code&gt;UserProfiles\&lt;/code&gt;). The &lt;code&gt;$lookup\&lt;/code&gt; stage acts as your "Left Outer Join," allowing you to pull in related data seamlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SassyPack Helps
&lt;/h2&gt;

&lt;p&gt;SassyPack doesn't just store data; it teaches you how to use it. Our &lt;a href="https://sassypack.collabtower.com/blog/advanced-mern-performance-optimization-scaling-guide" rel="noopener noreferrer"&gt;advanced MERN performance optimization and scaling guide&lt;/a&gt; includes a library of battle-tested aggregation snippets for common SaaS metrics.&lt;/p&gt;

&lt;p&gt;By using the &lt;a href="https://sassypack.collabtower.com/blog/saas-architecture-patterns-scalable-workflows" rel="noopener noreferrer"&gt;SassyPack architecture for scalable workflows&lt;/a&gt;, you ensure that your analytics dashboard remains performant even as your &lt;code&gt;events\&lt;/code&gt; collection grows into the millions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Action Plan: Optimize Your Dashboard Today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify Slow Queries:&lt;/strong&gt; Use the MongoDB Atlas Profiler to find any query taking longer than 100ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert Loops to Aggregations:&lt;/strong&gt; Find one place where you are &lt;code&gt;.map()\&lt;/code&gt;ing over a large array to calculate a total and move it to a &lt;code&gt;$group\&lt;/code&gt; pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Indexes:&lt;/strong&gt; Ensure every field used in a &lt;code&gt;$match\&lt;/code&gt; or &lt;code&gt;$sort\&lt;/code&gt; stage is properly indexed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Your database is more than a bucket for JSON; it is a powerful computational engine. When you master aggregations, you unlock the ability to provide the "Big Data" insights that enterprise clients demand.&lt;/p&gt;

&lt;p&gt;Stop wasting CPU cycles. Build a smarter, faster SaaS with &lt;a href="https://sassypack.collabtower.com/" rel="noopener noreferrer"&gt;SassyPack&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Stop Building Your Own Auth and Billing: Why You Are Actually Losing Money</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 24 Feb 2026 08:59:07 +0000</pubDate>
      <link>https://forem.com/thekarlesi/stop-building-your-own-auth-and-billing-why-you-are-actually-losing-money-2e7i</link>
      <guid>https://forem.com/thekarlesi/stop-building-your-own-auth-and-billing-why-you-are-actually-losing-money-2e7i</guid>
      <description>&lt;h1&gt;
  
  
  Stop Building Your Own Auth and Billing: Why You Are Actually Losing Money
&lt;/h1&gt;

&lt;p&gt;It happens to the best of us. You have a killer SaaS idea. You open your IDE, initialize a new git repo, and start coding. But instead of building the feature that solves your customer's problem, you spend the next three nights configuring &lt;strong&gt;NextAuth.js&lt;/strong&gt;, setting up &lt;strong&gt;Stripe webhooks&lt;/strong&gt;, and fighting with &lt;strong&gt;PostgreSQL types&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fast forward three weeks: you have a perfect login screen and a working "Manage Subscription" button, but &lt;strong&gt;zero users&lt;/strong&gt; and &lt;strong&gt;zero core features&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In 2026, building your own boilerplate isn't "engineering excellence"—it is a form of procrastination.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architect's Debt
&lt;/h2&gt;

&lt;p&gt;Every hour you spend on infrastructure that is identical across 99% of all SaaS apps is an hour you are not spending on your &lt;strong&gt;Unique Value Proposition (UVP)&lt;/strong&gt;. This is what I call "Architect's Debt." &lt;/p&gt;

&lt;p&gt;When you build from scratch, you aren't just writing code; you are signing up for a lifetime of maintenance.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who is monitoring your JWT rotation?&lt;/li&gt;
&lt;li&gt;Who is updating your Stripe API version next year?&lt;/li&gt;
&lt;li&gt;Who is fixing the hydration error in your mobile sidebar?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The "Ship First" Stack: MERN + Next.js
&lt;/h2&gt;

&lt;p&gt;If you want to move from "Developer" to "Founder," you need a stack that favors velocity. The MERN stack (MongoDB, Express, React, Node.js) combined with Next.js is currently the most powerful engine for this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this specific stack?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;NoSQL Flexibility:&lt;/strong&gt; MongoDB allows you to evolve your data schema as you find product-market fit without painful migrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server Actions:&lt;/strong&gt; Next.js eliminates the need for redundant boilerplate between your frontend and backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge Middleware:&lt;/strong&gt; Security and redirects happen before the user even hits your server.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The 80/20 Rule of SaaS
&lt;/h2&gt;

&lt;p&gt;80% of your SaaS code is "The Boring Stuff" (Auth, Billing, SEO, Emails).&lt;br&gt;
20% is "The Secret Sauce" (Your actual product).&lt;/p&gt;

&lt;p&gt;By using a starter kit like &lt;strong&gt;SassyPack&lt;/strong&gt;, you start your project at the 80% mark. You aren't "cheating"; you are leveraging professional-grade scaffolding to ensure your foundation doesn't crumble when you hit your first 1,000 users.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Pivot Your Workflow Today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stop Bikeshedding:&lt;/strong&gt; It doesn't matter which CSS-in-JS library you use. Pick Tailwind and move on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Managed Services:&lt;/strong&gt; Don't host your own database. Use MongoDB Atlas. Don't build auth. Use a proven wrapper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus on the "Aha!" Moment:&lt;/strong&gt; What is the one thing your app does that makes a user say "Wow"? Build that first. Everything else should be a pre-built commodity.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The market doesn't care how beautiful your internal authentication middleware is. The market cares if you can solve its problems. &lt;/p&gt;

&lt;p&gt;If you're ready to stop fighting your infrastructure and start shipping, check out &lt;a href="https://sassypack.collabtower.com/" rel="noopener noreferrer"&gt;SassyPack&lt;/a&gt;. It’s the boilerplate I built to ensure I never have to write a login form ever again.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What’s your take? Do you prefer building every layer yourself, or do you use a starter kit to get to market faster? Let's discuss in the comments.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>From Todo App to Real SaaS in 2026: The RSC-First Playbook</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 17 Feb 2026 18:04:28 +0000</pubDate>
      <link>https://forem.com/thekarlesi/from-todo-app-to-real-saas-in-2026-the-rsc-first-playbook-bcd</link>
      <guid>https://forem.com/thekarlesi/from-todo-app-to-real-saas-in-2026-the-rsc-first-playbook-bcd</guid>
      <description>&lt;h3&gt;
  
  
  A Production-Ready Architecture Guide for Modern Next.js SaaS Builders
&lt;/h3&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;You spend weeks building authentication and connecting your database. Then you realize your architecture cannot securely handle Stripe webhooks or scale MongoDB queries. You are exhausted and have not even built your core feature.&lt;/p&gt;

&lt;p&gt;If you want a structured system instead of random tutorials, explore &lt;strong&gt;&lt;a href="https://zero-to-saas.collabtower.com/" rel="noopener noreferrer"&gt;Zero to SaaS&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  1. The Real Problem With Most SaaS Tutorials
&lt;/h1&gt;

&lt;p&gt;Most tutorials teach Todo apps.&lt;/p&gt;

&lt;p&gt;They skip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-tenant security
&lt;/li&gt;
&lt;li&gt;Subscription lifecycle management
&lt;/li&gt;
&lt;li&gt;Usage-based billing
&lt;/li&gt;
&lt;li&gt;Core Web Vitals optimization
&lt;/li&gt;
&lt;li&gt;Production deployment
&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  2. The 2026 Shift: RSC-First Development
&lt;/h1&gt;

&lt;p&gt;Modern SaaS products are built RSC-first using the Next.js App Router.&lt;/p&gt;

&lt;p&gt;With this architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server handles sensitive logic
&lt;/li&gt;
&lt;li&gt;The client handles interaction
&lt;/li&gt;
&lt;li&gt;JavaScript bundles shrink
&lt;/li&gt;
&lt;li&gt;SEO improves automatically
&lt;/li&gt;
&lt;li&gt;Secrets never reach the browser
&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  3. The Modern SaaS Stack
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Next.js (App Router)
&lt;/li&gt;
&lt;li&gt;Tailwind CSS + DaisyUI
&lt;/li&gt;
&lt;li&gt;MongoDB
&lt;/li&gt;
&lt;li&gt;Stripe
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a guided technical walkthrough, see &lt;strong&gt;&lt;a href="https://zero-to-saas.collabtower.com/blog/build-saas-with-nextjs-and-stripe" rel="noopener noreferrer"&gt;Build SaaS with Next.js and Stripe&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  4. High-Intent Architecture With App Router
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Static Landing Pages
&lt;/h2&gt;

&lt;p&gt;Use Server Components for marketing pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistent Dashboard Layout
&lt;/h2&gt;

&lt;p&gt;Create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dashboard/layout.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your sidebar persists. Only inner content updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming UI
&lt;/h2&gt;

&lt;p&gt;Add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;loading.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Show instant skeleton states while data loads.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. Database Layer: MongoDB + Server Actions
&lt;/h1&gt;

&lt;p&gt;Use Server Actions instead of unnecessary API routes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/dashboard/actions.ts&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dbConnect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Project&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/models/Project&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidatePath&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;dbConnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newProject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
    &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="na"&gt;ownerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; 
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;revalidatePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  6. Stripe Payment Lifecycle
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;Redirect users to Stripe Checkout
&lt;/li&gt;
&lt;li&gt;Create webhook at &lt;code&gt;/api/webhook/stripe&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Update subscription and usage in MongoDB
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a structured 14-day build roadmap, check &lt;strong&gt;&lt;a href="https://zero-to-saas.collabtower.com/blog/from-zero-to-saas-nextjs-14-day-course" rel="noopener noreferrer"&gt;Zero to SaaS 14 Day Course&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  7. Deployment
&lt;/h1&gt;

&lt;p&gt;Deploy using Vercel for seamless scaling.&lt;/p&gt;




&lt;h1&gt;
  
  
  8. Common Mistakes
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Storing secrets in client components
&lt;/li&gt;
&lt;li&gt;Overcomplicating authentication
&lt;/li&gt;
&lt;li&gt;Ignoring hydration errors
&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  9. Weekly Action Plan
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;Initialize Next.js project
&lt;/li&gt;
&lt;li&gt;Implement authentication
&lt;/li&gt;
&lt;li&gt;Build one core feature
&lt;/li&gt;
&lt;li&gt;Connect Stripe and handle webhooks
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ship clean. Ship scalable. Ship production-ready.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Why Zero to SaaS is the Best Alternative to CodeFast in 2026</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Wed, 11 Feb 2026 03:07:28 +0000</pubDate>
      <link>https://forem.com/thekarlesi/why-zero-to-saas-is-the-best-alternative-to-codefast-in-2026-k7e</link>
      <guid>https://forem.com/thekarlesi/why-zero-to-saas-is-the-best-alternative-to-codefast-in-2026-k7e</guid>
      <description>&lt;p&gt;If you have spent any time in the indie hacker community lately, you have likely seen &lt;strong&gt;CodeFast&lt;/strong&gt;—the "learn to code in 14 days" course that promise to turn anyone into a shipping machine. It is flashy, it is fast, and it is built by some of the most visible makers in the space.&lt;/p&gt;

&lt;p&gt;But after helping hundreds of developers move from "tutorial hell" to "production reality," I have noticed a recurring pattern: &lt;strong&gt;Speed without structure leads to technical debt.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While CodeFast is great for a quick dopamine hit, if you are serious about building a sustainable, scalable business, there is a better way. Here is why &lt;strong&gt;Zero to SaaS&lt;/strong&gt; has emerged as the premier alternative for developers who want to build products that last.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Philosophy: Shipping vs. Scaling
&lt;/h2&gt;

&lt;p&gt;The core difference between these two paths lies in their fundamental goal.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CodeFast&lt;/strong&gt; is designed for the absolute sprint. It uses an "AI-first" approach that leans heavily on prompts to generate code. It is fantastic for moving fast, but it often leaves students with a "black box" application—they have a working product, but they don't truly understand &lt;em&gt;why&lt;/em&gt; it works or how to fix it when the AI hallucinates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero to SaaS&lt;/strong&gt; is designed for the &lt;strong&gt;Full Lifecycle&lt;/strong&gt;. It is workflow-driven. We don't just teach you how to prompt an AI to make a button; we teach you the &lt;strong&gt;Next.js App Router SaaS architecture&lt;/strong&gt; required to handle complex data, multi-tenant security, and real-world performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Deep Dive vs. Surface Level
&lt;/h2&gt;

&lt;p&gt;CodeFast’s 12-hour curriculum is impressive for its density, but it often skims over the "boring" parts that actually matter for a business: database indexing, secure middleware patterns, and subscription lifecycle management.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Zero to SaaS&lt;/strong&gt;, we take a more surgical approach to the stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;CodeFast Approach&lt;/th&gt;
&lt;th&gt;Zero to SaaS Approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic API routes&lt;/td&gt;
&lt;td&gt;RSC-first (Server Components)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple CRUD&lt;/td&gt;
&lt;td&gt;Optimized MongoDB Schemas + Indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Payments&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One-time/Basic Sub&lt;/td&gt;
&lt;td&gt;Full &lt;a href="https://zero-to-saas.collabtower.com/blog/stripe-subscriptions-in-nextjs" rel="noopener noreferrer"&gt;Stripe Subscriptions in Next.js&lt;/a&gt; lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI-generated snippets&lt;/td&gt;
&lt;td&gt;Hand-crafted, modular Server Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  2. The "AI Crutch" Problem
&lt;/h2&gt;

&lt;p&gt;In 2026, AI is a requirement, not a feature. However, CodeFast treats AI as the primary developer. This works until you hit a bug that the AI can't solve. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero to SaaS&lt;/strong&gt; teaches you to be the &lt;strong&gt;Architect&lt;/strong&gt;. We use AI to accelerate our workflow, but the course is built around understanding the &lt;a href="https://zero-to-saas.collabtower.com/blog/nextjs-course-for-beginners" rel="noopener noreferrer"&gt;Next.js Course for Beginners&lt;/a&gt; fundamentals first. When your Stripe webhook fails at 3:00 AM, you won't be praying for a better prompt—you will know exactly which line in your route handler is causing the issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Real Production Workflows
&lt;/h2&gt;

&lt;p&gt;CodeFast focuses on the "First 14 Days." But what happens on Day 15? &lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Zero to SaaS&lt;/strong&gt; curriculum includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Advanced Deployment:&lt;/strong&gt; Going beyond "click to deploy" to understanding environment variables, build logs, and Vercel optimization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance:&lt;/strong&gt; How to achieve 100 Lighthouse scores so your landing page actually converts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; Multi-role access and protecting sensitive API routes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Zero to SaaS is the Better Choice for 2026
&lt;/h2&gt;

&lt;p&gt;The "Vertical SaaS" wave is here. Developers are no longer building generic tools; they are building industry-specific CRMs, AI wrappers, and compliance tools. These require more than just a "shippable" script—they require a robust &lt;a href="https://zero-to-saas.collabtower.com/blog/build-saas-dashboard-nextjs-tailwind" rel="noopener noreferrer"&gt;Build SaaS Dashboard Next.js Tailwind&lt;/a&gt; that can grow with the customer's needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Benefits of Choosing Zero to SaaS:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Future-Proofing:&lt;/strong&gt; We focus exclusively on the latest Next.js App Router patterns, ensuring your code isn't legacy by the time you launch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability:&lt;/strong&gt; Our MongoDB and Stripe patterns are designed for high-concurrency and complex billing models (like usage-based pricing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clarity:&lt;/strong&gt; We replace "copy-paste" with "reason-and-implement."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Mistakes Beginners Make When Choosing a Course
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Chasing the shortest timeline:&lt;/strong&gt; A 14-day launch sounds great, but if the app breaks on Day 16, you haven't saved any time.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Ignoring the "Middle of the Funnel":&lt;/strong&gt; Most courses teach you how to build a landing page and a login. They skip the actual dashboard logic where the value is created.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Stack Lock-in:&lt;/strong&gt; Choosing a course that forces you into a proprietary boilerplate you don't own.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final Verdict
&lt;/h2&gt;

&lt;p&gt;If you want to build a "toy" or a very simple MVP to test an idea in a weekend, CodeFast is a solid choice. But if you want to &lt;a href="https://zero-to-saas.collabtower.com/blog/learn-full-stack-saas-development" rel="noopener noreferrer"&gt;Learn Full Stack SaaS Development&lt;/a&gt; and build a professional, secure, and scalable business, &lt;strong&gt;Zero to SaaS&lt;/strong&gt; is the best alternative on the market today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your Action Plan:&lt;/strong&gt;&lt;br&gt;
Stop watching and start building. If you are ready to move from zero to a production-ready application with a mentor-led approach, check out the &lt;a href="https://zero-to-saas.collabtower.com/blog/from-zero-to-saas-nextjs-14-day-course" rel="noopener noreferrer"&gt;Zero to SaaS 14 Day Course&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Would you like me to help you map out a custom curriculum based on your specific SaaS idea?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Handle Stripe and Paystack Webhooks in Next.js (The App Router Way)</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 13 Jan 2026 04:01:08 +0000</pubDate>
      <link>https://forem.com/thekarlesi/how-to-handle-stripe-and-paystack-webhooks-in-nextjs-the-app-router-way-5bgi</link>
      <guid>https://forem.com/thekarlesi/how-to-handle-stripe-and-paystack-webhooks-in-nextjs-the-app-router-way-5bgi</guid>
      <description>&lt;p&gt;The #1 reason developers struggle with SaaS payments is Webhook Signature Verification. You set everything up, the test payment goes through, but your server returns a &lt;code&gt;400 Bad Request&lt;/code&gt; or a &lt;code&gt;Signature Verification Failed&lt;/code&gt; error.&lt;/p&gt;

&lt;p&gt;In the Next.js App Router, the problem usually stems from how the request body is parsed. Stripe and Paystack require the raw request body to verify the signature, but Next.js often tries to be helpful by parsing it as JSON before you can get to it.&lt;/p&gt;

&lt;p&gt;Here is the "Golden Pattern" for handling this in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Route Handler Setup
&lt;/h2&gt;

&lt;p&gt;Create a file at &lt;code&gt;app/api/webhooks/route.ts&lt;/code&gt;. You must export a &lt;code&gt;config&lt;/code&gt; object (if using older versions) or use the &lt;code&gt;req.text()&lt;/code&gt; method in the App Router to prevent automatic parsing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Get the raw body as text&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Grab the signature from headers&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-paystack-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; 
                    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Verify the signature (Example for Paystack)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha512&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAYSTACK_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Now parse the body and handle the event&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;charge.success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Handle successful payment in your database&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment successful for:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. The Middleware Trap
&lt;/h2&gt;

&lt;p&gt;If you have global middleware protecting your routes, ensure your webhook path is excluded. Otherwise, the payment provider will hit your login page instead of your API.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Why this matters for your SaaS
&lt;/h2&gt;

&lt;p&gt;If your webhooks fail, your users won't get their "Pro" access, and your churn will skyrocket. Handling this correctly is the difference between a side project and a real business.&lt;/p&gt;

&lt;p&gt;I have spent a lot of time documenting these "Gotchas" while building my MERN stack projects. If you want to see a full implementation of this including Stripe, Paystack, and database logic, check out my deep dive here: &lt;a href="https://sassypack.collabtower.com/blog/how-to-add-stripe-or-paystack-payments-to-your-saas" rel="noopener noreferrer"&gt;How to add Stripe or Paystack payments to your SaaS&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Digging Deeper
&lt;/h2&gt;

&lt;p&gt;If you are tired of debugging the same boilerplate over and over, you might find my &lt;a href="https://sassypack.collabtower.com/blog/sassypack-overview" rel="noopener noreferrer"&gt;SassyPack overview&lt;/a&gt; helpful. I built it specifically to solve these "Day 1" technical headaches for other founders.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>api</category>
      <category>nextjs</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Secure Authentication in Next.js: Building a Production-Ready Login System</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Sun, 04 Jan 2026 11:16:35 +0000</pubDate>
      <link>https://forem.com/thekarlesi/secure-authentication-in-nextjs-building-a-production-ready-login-system-4m7</link>
      <guid>https://forem.com/thekarlesi/secure-authentication-in-nextjs-building-a-production-ready-login-system-4m7</guid>
      <description>&lt;h1&gt;
  
  
  Secure Authentication in Next.js: Building a Production-Ready Login System
&lt;/h1&gt;

&lt;p&gt;Every great SaaS product begins at the same point: the login page. It is the gatekeeper of your user data and the first interaction your customers have with your professional application. Yet, for many developers, setting up authentication feels like a high-stakes puzzle where a single mistake can lead to security vulnerabilities or a frustrated user base.&lt;/p&gt;

&lt;p&gt;If you have ever struggled with session management, wondered how to securely store user credentials, or felt overwhelmed by the complexity of OAuth providers, you are in the right place. In this lesson, we are going to strip away the confusion and build a robust, secure authentication system using Auth.js (NextAuth v5) within the Next.js App Router framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: The "Homegrown" Auth Trap
&lt;/h2&gt;

&lt;p&gt;Many developers start by trying to build their own authentication logic. They create a users table in MongoDB, hash passwords with bcrypt, and try to manage JWTs (JSON Web Tokens) manually in cookies. While this is a great academic exercise, it is often a recipe for disaster in a production SaaS environment.&lt;/p&gt;

&lt;p&gt;Manual auth systems frequently suffer from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security Gaps:&lt;/strong&gt; Improperly configured cookies or CSRF (Cross-Site Request Forgery) vulnerabilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance Burden:&lt;/strong&gt; Keeping up with changing security standards and API updates from providers like Google or GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UX Friction:&lt;/strong&gt; Hard-to-implement features like "Forgot Password," "Magic Links," or social logins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0dy6ofma79tbskuqatzz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0dy6ofma79tbskuqatzz.jpg" alt="Login and signup forms for web app authentication" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shift: Moving to Auth.js
&lt;/h2&gt;

&lt;p&gt;The professional way to handle this in 2026 is by using a library that does the heavy lifting for you. Auth.js is the standard for anyone wanting to &lt;a href="https://zero-to-saas.collabtower.com/blog/learn-nextjs-for-saas" rel="noopener noreferrer"&gt;Learn Next.js for SaaS&lt;/a&gt;. It handles session management, multi-provider support, and database integration out of the box, allowing you to focus on your core product features instead of reinventing the security wheel.&lt;/p&gt;

&lt;p&gt;By shifting to an established library, you gain the confidence that your sessions are handled via encrypted, server-only cookies. You also get an easy path to adding "Login with Google," which significantly increases conversion rates for modern SaaS products.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep Dive: Setting Up Your Auth Workflow
&lt;/h2&gt;

&lt;p&gt;To build a complete SaaS, we need a flexible system. We will implement two main strategies: &lt;strong&gt;Email/Password (Credentials)&lt;/strong&gt; for traditional users and &lt;strong&gt;Google OAuth&lt;/strong&gt; for a frictionless experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Architecture of Auth.js in the App Router
&lt;/h3&gt;

&lt;p&gt;In the Next.js App Router, authentication happens primarily on the server. We use a combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Auth Configuration File:&lt;/strong&gt; Where we define our providers and callbacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middleware:&lt;/strong&gt; To protect routes before they even hit the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server Actions:&lt;/strong&gt; To handle login and signup logic securely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="/images/articles/developer-building-saas.jpg" class="article-body-image-wrapper"&gt;&lt;img src="/images/articles/developer-building-saas.jpg" alt="Developer building a SaaS app using modern web technologies"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Initial Setup and Environment Variables
&lt;/h3&gt;

&lt;p&gt;First, we need to install the necessary packages. In your terminal, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;next-auth@beta mongodb @auth/mongodb-adapter bcryptjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before writing code, we must define our environment variables. These are secrets that should never be committed to GitHub. Create a &lt;code&gt;.env.local\&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AUTH_SECRET=your_super_secret_random_string
NEXT_PUBLIC_APP_URL=http://localhost:3000

AUTH_GOOGLE_ID=your_google_client_id
AUTH_GOOGLE_SECRET=your_google_client_secret

MONGODB_URI=your_mongodb_connection_string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Configuring the Auth Library
&lt;/h3&gt;

&lt;p&gt;We will create a central configuration file. This is the heart of your security system. It tells Next.js how to talk to your database and how to verify users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File: &lt;code&gt;auth.ts&lt;/code&gt; (Root directory)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;NextAuth&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Google&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-auth/providers/google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Credentials&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next-auth/providers/credentials&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MongoDBAdapter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@auth/mongodb-adapter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;clientPromise&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/mongodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bcryptjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signOut&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NextAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MongoDBAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientPromise&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;Google&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;credentials&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;clientPromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dbClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
          &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; 
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
          &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;isValid&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jwt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Creating the Login UI with Tailwind and DaisyUI
&lt;/h3&gt;

&lt;p&gt;A SaaS needs a professional-looking login page. Using Tailwind CSS and DaisyUI, we can build a clean, responsive form that works on any device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File: &lt;code&gt;app/(auth)/login/page.tsx&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signIn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LoginPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center justify-center min-h-screen bg-base-200"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card w-full max-w-md shadow-2xl bg-base-100"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"card-body"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-3xl font-bold text-center mb-6"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Welcome Back&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;
            &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
              &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;redirectTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-outline w-full flex items-center gap-2"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              Continue with Google
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"divider text-xs uppercase text-base-content/50"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;or&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"space-y-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"form-control"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"label-text"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email@example.com"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"input input-bordered"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"form-control"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"label-text"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Password&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"••••••••"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"input input-bordered"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-primary w-full"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Sign In&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-center mt-4 text-sm"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            Don't have an account? &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/signup"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"link link-primary"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Sign up&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Protecting Routes with Middleware
&lt;/h3&gt;

&lt;p&gt;In a SaaS application, you don't want unauthorized users accessing the &lt;code&gt;dashboard&lt;/code&gt; or &lt;code&gt;settings&lt;/code&gt; pages. Instead of checking for a session on every single page, we use Next.js Middleware to handle this globally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File: &lt;code&gt;middleware.ts&lt;/code&gt; (Root directory)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isLoggedIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nextUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isAuthPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; 
                     &lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/signup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDashboardPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isDashboardPage&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isAuthPage&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/((?!api|_next/static|_next/image|favicon.ico).*)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Benefits and Learning Outcomes
&lt;/h2&gt;

&lt;p&gt;By following this workflow, you achieve several critical milestones in your development journey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Centralized Security:&lt;/strong&gt; You have a single source of truth for your authentication logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Synchronization:&lt;/strong&gt; Your user accounts are automatically saved to MongoDB whenever someone logs in via Google.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved Conversions:&lt;/strong&gt; Providing OAuth options reduces the friction of creating an account, which is vital for any &lt;a href="https://zero-to-saas.collabtower.com/blog/build-saas-with-nextjs" rel="noopener noreferrer"&gt;Build SaaS with Next.js&lt;/a&gt; project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type Safety:&lt;/strong&gt; Using TypeScript ensures that your session data is predictable throughout your components.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common Mistakes to Avoid
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exposing the Secret:&lt;/strong&gt; Never leave your AUTH_SECRET empty or use a simple string in production. Use a tool like openssl rand -base64 32 to generate a strong key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-Side Protection Only:&lt;/strong&gt; Never rely solely on hiding UI elements to secure your app. Always verify the session on the server or through middleware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting Secure Cookies:&lt;/strong&gt; In production, ensure your AUTH_URL uses HTTPS, otherwise Auth.js will not set secure cookies, and your login will fail.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Pro Tips and Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Server Components for Auth Checks:&lt;/strong&gt; Whenever possible, check the session in a Server Component using the auth() function. It is faster and more secure than checking on the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Session Data:&lt;/strong&gt; If you need to store extra info (like a user's subscription status), extend the session callback in auth.ts to include those fields from your MongoDB database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful Error Handling:&lt;/strong&gt; Redirect users to a custom error page if Google login fails, rather than letting the app crash or show a generic error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="/images/articles/nextjs-tailwind-code-snippet.jpg" class="article-body-image-wrapper"&gt;&lt;img src="/images/articles/nextjs-tailwind-code-snippet.jpg" alt="Code snippet of Next.js and Tailwind project"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How This Fits Into the Zero to SaaS Journey
&lt;/h2&gt;

&lt;p&gt;Authentication is the foundation of the user experience. Once you have established who the user is, you can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Store their specific data in MongoDB.&lt;/li&gt;
&lt;li&gt;Link their account to a Stripe Customer ID for billing.&lt;/li&gt;
&lt;li&gt;Provide a personalized &lt;a href="https://zero-to-saas.collabtower.com/blog/build-saas-dashboard-nextjs-tailwind" rel="noopener noreferrer"&gt;Build SaaS Dashboard Next.js Tailwind&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Without a secure auth system, your SaaS cannot function because you cannot identify who to charge or whose data to display.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Use Case: The Productivity Tool
&lt;/h2&gt;

&lt;p&gt;Imagine you are building a SaaS called TaskFlow. A user arrives at your landing page and clicks Get Started. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They click Continue with Google.&lt;/li&gt;
&lt;li&gt;Auth.js redirects them to Google's secure portal.&lt;/li&gt;
&lt;li&gt;After they approve, Google sends a token back to your auth.ts handler.&lt;/li&gt;
&lt;li&gt;Auth.js checks your MongoDB. Since this is a new user, it automatically creates a new record in your users collection.&lt;/li&gt;
&lt;li&gt;The user is redirected to /dashboard, where your server component greets them: "Welcome!"&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Action Plan: What to Build Next
&lt;/h2&gt;

&lt;p&gt;To master this lesson, I want you to complete these four tasks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initialize the Project:&lt;/strong&gt; Set up a fresh Next.js project and install the dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure Google Cloud:&lt;/strong&gt; Go to the Google Cloud Console, create a project, and get your OAuth credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the Login Page:&lt;/strong&gt; Use the Tailwind/DaisyUI code provided to create your own branded login screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the Middleware:&lt;/strong&gt; Create a protected /dashboard page and try to access it while logged out to ensure you are redirected.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Take Your SaaS to the Next Level
&lt;/h3&gt;

&lt;p&gt;Building a secure login system is just the beginning. If you want to skip the trial and error and follow a proven path to a launched product, check out our comprehensive &lt;a href="https://zero-to-saas.collabtower.com/blog/zero-to-saas-nextjs-course" rel="noopener noreferrer"&gt;Zero to SaaS Next.js Course&lt;/a&gt;. We dive deep into advanced patterns, multi-tenant security, and production-ready deployments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>nextjs</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The SaaS Billing Nightmare: Why Integration Is More Than Just a 'Pay' Button</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Sun, 04 Jan 2026 10:52:11 +0000</pubDate>
      <link>https://forem.com/thekarlesi/the-saas-billing-nightmare-why-integration-is-more-than-just-a-pay-button-1mjp</link>
      <guid>https://forem.com/thekarlesi/the-saas-billing-nightmare-why-integration-is-more-than-just-a-pay-button-1mjp</guid>
      <description>&lt;h3&gt;
  
  
  The "Simple" Button Illusion
&lt;/h3&gt;

&lt;p&gt;You’ve finally finished your MVP. The logic works, the UI is clean, and you’re ready to take your first dollar. You open the Stripe documentation thinking, "I’ll just drop in a Checkout button and be done by lunch." &lt;/p&gt;

&lt;p&gt;Fast forward forty-eight hours: you are buried in webhook logs, trying to figure out why a "successful" payment didn't actually update the user's subscription status in your database. &lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: Billing is a State Machine, Not a Transaction
&lt;/h3&gt;

&lt;p&gt;Billing is the most deceptive part of building a SaaS. It looks like a simple transaction, but in reality, it is a complex, multi-state distributed system. When you build billing from scratch, you aren't just adding a button; you are building a system that must handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subscription States:&lt;/strong&gt; Trialing, active, past_due, canceled, and incomplete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proration:&lt;/strong&gt; What happens when a user switches from a $20/month plan to a $100/month plan mid-cycle?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook Reliability:&lt;/strong&gt; If your server blips when Stripe sends a "payment succeeded" event, does your user lose access to the product they just paid for?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Compliance:&lt;/strong&gt; Handling VAT, GST, and diverse payment methods like Paystack for African markets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building this manually drains weeks of development time and introduces "silent failures" that cost you money and customer trust.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Shift: Using Subscription Engines
&lt;/h3&gt;

&lt;p&gt;Sophisticated founders no longer write custom billing logic. They use &lt;strong&gt;Subscription Engines&lt;/strong&gt;. The shift is moving away from "How do I call the Stripe API?" toward "How do I sync my app state with my billing provider?" &lt;/p&gt;

&lt;p&gt;By using a pre-configured billing architecture, you treat payments as an infrastructure layer rather than a coding challenge. This allows you to focus on the actual value that makes users want to pay in the first place.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep Dive: The Hidden Logic of SaaS Payments
&lt;/h3&gt;

&lt;p&gt;When you peel back the curtain, "simple" billing involves several layers of engineering that developers often underestimate.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The Source of Truth Paradox
&lt;/h4&gt;

&lt;p&gt;Should your database be the source of truth for a subscription, or should Stripe? If you rely solely on your database, you might miss a cancellation made through the Stripe portal. If you rely solely on Stripe's API, your app will be slow due to network overhead. The solution is a robust sync layer using webhooks, which is notoriously difficult to test locally.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Regional Payment Diversity
&lt;/h4&gt;

&lt;p&gt;If you are targeting a global audience, you can't rely on credit cards alone. In regions like Africa, Paystack is the gold standard. Implementing &lt;a href="https://sassypack.collabtower.com/blog/how-to-add-stripe-or-paystack-payments-to-your-saas" rel="noopener noreferrer"&gt;how to add Stripe or Paystack payments to your SaaS&lt;/a&gt; requires a unified abstraction layer so your frontend doesn't care which provider is being used.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. The Grace Period Logic
&lt;/h4&gt;

&lt;p&gt;What happens if a user's card is declined? You shouldn't lock them out instantly. You need "dunning" logic: a window where the app remains active while the system automatically retries the card. Coding this state machine from scratch is a massive time sink.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Managing Plan Upgrades
&lt;/h4&gt;

&lt;p&gt;Upgrading a plan isn't just a new transaction. You have to calculate the unused portion of the current plan and apply it as a credit. If you don't have a system that handles &lt;a href="https://sassypack.collabtower.com/blog/how-to-add-new-payment-plans-in-sassypack" rel="noopener noreferrer"&gt;how to add new payment plans in SassyPack&lt;/a&gt; automatically, you'll be doing manual math in support tickets every week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Mistakes to Avoid
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Hard-coding Plan IDs:&lt;/strong&gt; Never put your Stripe Price IDs directly in your frontend code. Keep them in environment variables.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Trusting the Client-Side:&lt;/strong&gt; Never update a user's status based on a frontend callback. Only trust a verified, signed webhook from the payment provider.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Ignoring the "Tax" Problem:&lt;/strong&gt; Forgetting to collect addresses for tax purposes can lead to a legal nightmare once you scale.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Pro Tips for Billing Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency Keys:&lt;/strong&gt; Always use idempotency keys when creating charges to ensure customers aren't charged twice during network retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulate Webhooks:&lt;/strong&gt; Use the Stripe CLI during development. Do not wait until production to see if your handlers work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified Pricing Table:&lt;/strong&gt; Create a single JSON configuration that maps your plan names to their respective provider IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How SassyPack Helps
&lt;/h3&gt;

&lt;p&gt;SassyPack takes the nightmare out of billing by providing a dual-integration system out of the box. Whether you need to &lt;a href="https://sassypack.collabtower.com/blog/how-to-add-stripe-payments-to-your-sassypack-app" rel="noopener noreferrer"&gt;add Stripe payments to your SassyPack app&lt;/a&gt; or &lt;a href="https://sassypack.collabtower.com/blog/how-to-add-paystack-payments-to-your-sassypack-app" rel="noopener noreferrer"&gt;add Paystack payments to your SassyPack app&lt;/a&gt;, the infrastructure is already built.&lt;/p&gt;

&lt;p&gt;It features secure webhook handlers, subscription middleware to protect routes, and a unified checkout UI that works for both providers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Action Plan and Takeaways
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Stop Coding Billing Logic:&lt;/strong&gt; It is a high-risk, low-reward activity for a founder.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Verify Your Webhooks:&lt;/strong&gt; Ensure your handlers are cryptographically verified to prevent spoofing.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Automate Subscriptions:&lt;/strong&gt; Use a system that handles the "past_due" and "canceled" states without manual intervention.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Start Collecting Revenue Today
&lt;/h3&gt;

&lt;p&gt;Your goal isn't to be a "billing expert"; it is to be a successful founder. Every day you spend debugging payment logic is a day you aren't growing your MRR. Stop fighting the API and start shipping. Use the &lt;a href="https://sassypack.collabtower.com/blog/sassypack-pricing-and-setup-assist" rel="noopener noreferrer"&gt;SassyPack Pricing and Setup Assist&lt;/a&gt; to get your billing system live by the end of the day.&lt;br&gt;
`&lt;br&gt;
},&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
