<?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>DEV Community: Esimit Karlgusta</title>
    <description>The latest articles on DEV Community by Esimit Karlgusta (@thekarlesi).</description>
    <link>https://dev.to/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>DEV Community: Esimit Karlgusta</title>
      <link>https://dev.to/thekarlesi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thekarlesi"/>
    <language>en</language>
    <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://dev.to/thekarlesi/google-oauth-email-auth-in-nextjs-the-complete-saas-authentication-guide-2026-1ja</link>
      <guid>https://dev.to/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="800" height="533"&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="800" height="534"&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://dev.to/thekarlesi/stripe-subscription-lifecycle-in-nextjs-the-complete-developer-guide-2026-4l9d</link>
      <guid>https://dev.to/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="800" height="533"&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="800" height="533"&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://dev.to/thekarlesi/beyond-find-mastering-mongodb-aggregations-for-real-time-saas-analytics-2lgc</link>
      <guid>https://dev.to/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://dev.to/thekarlesi/stop-building-your-own-auth-and-billing-why-you-are-actually-losing-money-2e7i</link>
      <guid>https://dev.to/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://dev.to/thekarlesi/from-todo-app-to-real-saas-in-2026-the-rsc-first-playbook-bcd</link>
      <guid>https://dev.to/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://dev.to/thekarlesi/why-zero-to-saas-is-the-best-alternative-to-codefast-in-2026-k7e</link>
      <guid>https://dev.to/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://dev.to/thekarlesi/how-to-handle-stripe-and-paystack-webhooks-in-nextjs-the-app-router-way-5bgi</link>
      <guid>https://dev.to/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://dev.to/thekarlesi/secure-authentication-in-nextjs-building-a-production-ready-login-system-4m7</link>
      <guid>https://dev.to/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="800" height="533"&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://dev.to/thekarlesi/the-saas-billing-nightmare-why-integration-is-more-than-just-a-pay-button-1mjp</link>
      <guid>https://dev.to/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>
    <item>
      <title>Stop Coding Login Screens: A Senior Developer’s Guide to Building SaaS That Actually Ships</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Sun, 04 Jan 2026 10:37:56 +0000</pubDate>
      <link>https://dev.to/thekarlesi/stop-coding-login-screens-a-senior-developers-guide-to-building-saas-that-actually-ships-3han</link>
      <guid>https://dev.to/thekarlesi/stop-coding-login-screens-a-senior-developers-guide-to-building-saas-that-actually-ships-3han</guid>
      <description>&lt;h3&gt;
  
  
  The 2:00 AM Realization
&lt;/h3&gt;

&lt;p&gt;You have the perfect idea. The domain is bought. The database schema is mapped out in your head. You sit down, crack your knuckles, and... spend the next eight hours configuring protected routes and password reset emails. By the time you get to the actual "logic" of your app, the weekend is over, and your motivation is dead. &lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: The Founder Momentum Killer
&lt;/h3&gt;

&lt;p&gt;As developers, we are often our own worst enemies. We suffer from "Not Invented Here" syndrome. We think that if we don't hand-roll our own authentication or write our own Stripe webhook handlers, we aren't "really" coding.&lt;/p&gt;

&lt;p&gt;But here is the hard truth: Your users do not care about your elegant JWT rotation logic. They care about the problem your software solves. Every hour you spend building boilerplate is an hour you aren't talking to users or refining your unique value proposition. Building SaaS from scratch manually drains your most finite resource: &lt;strong&gt;founder momentum&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Shift: From Coder to Integrator
&lt;/h3&gt;

&lt;p&gt;The era of the "everything from scratch" developer is ending. In its place is the "Product-Minded Engineer." These developers recognize that a SaaS foundation is a commodity. Whether you are building a Fintech tool or an AI wrapper, the login, the billing, and the dashboard sidebar are 90% the same across every project.&lt;/p&gt;

&lt;p&gt;Starter kits have risen in popularity because they provide a "standard library" for business. They allow you to skip the plumbing and go straight to the architecture. This isn't "cheating"; it is strategic efficiency.&lt;/p&gt;

&lt;p&gt;&lt;a href="/images/articles/mern-architecture-diagram.jpg" class="article-body-image-wrapper"&gt;&lt;img src="/images/articles/mern-architecture-diagram.jpg" alt="High-level architecture diagram of a MERN SaaS application"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep Dive: The Parts That Will Kill Your Speed
&lt;/h3&gt;

&lt;p&gt;If you choose to ignore a kit and build manually, these are the four horsemen of development delay:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The Auth Rabbit Hole
&lt;/h4&gt;

&lt;p&gt;You start with simple email/password. Then you realize you need Google Login. Then you need Magic Links for better UX. Then you need to handle session expiration across multiple tabs. Implementing the &lt;a href="https://sassypack.collabtower.com/blog/best-authentication-setup-for-saas" rel="noopener noreferrer"&gt;best authentication setup for SaaS&lt;/a&gt; can take a seasoned dev a full week of edge-case testing and security auditing.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. The Subscription Logic Nightmare
&lt;/h4&gt;

&lt;p&gt;Writing a checkout session is easy. Handling a &lt;code&gt;customer.subscription.deleted&lt;/code&gt; event in a way that gracefully revokes access in your database without crashing your app is hard. If you are building for a global audience, you might even need to manage multiple providers. Learning &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; isn't just about reading docs; it is about building a bulletproof state machine.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Environment Parity and Deployment
&lt;/h4&gt;

&lt;p&gt;Setting up a CI/CD pipeline that handles your Next.js frontend, your Node backend, and your MongoDB connection strings without leaking secrets is a tedious process. Most devs waste days on deployment debugging before they ever hit production.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. The "Boring" UI
&lt;/h4&gt;

&lt;p&gt;Don't forget the maintenance screens. Profile photo uploads, changing passwords, updating credit card info, and managing team members. This is 20% of the code but 0% of the market value of your product.&lt;/p&gt;

&lt;p&gt;&lt;a href="/images/articles/code-editor-mern-stack-setup.jpg" class="article-body-image-wrapper"&gt;&lt;img src="/images/articles/code-editor-mern-stack-setup.jpg" alt="Code editor showing MERN stack setup with Next.js and MongoDB"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Benefits and Real Results
&lt;/h3&gt;

&lt;p&gt;When you use a professional foundation, your timeline compresses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 1:&lt;/strong&gt; Deployment to production with a landing page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 2:&lt;/strong&gt; Your first core feature is live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 7:&lt;/strong&gt; You are running ads or cold emails to get users.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By comparison, the "manual" dev is usually still trying to figure out why their Tailwind CSS isn't purging correctly on Vercel by Day 10. The result of using a kit isn't just a faster launch; it is a higher probability of success. A project that launches in 48 hours has a much lower abandonment rate than one that takes three months to reach MVP.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Customizing the Foundation Early:&lt;/strong&gt; Don't spend three days redesigning the login page. Use the default. Get to the dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard-Coding Plans:&lt;/strong&gt; Never hard-code your pricing tiers. Use a kit that lets you manage plans via your payment provider so you can A/B test prices without a code deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual Deployment:&lt;/strong&gt; If you are still FTP-ing files or manually running &lt;code&gt;npm build&lt;/code&gt; on a VPS, you are losing time. Use a modern Vercel-based workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pro Tips for Senior Developer Speed
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Use Server Components for Data Fetching:&lt;/strong&gt; If you are on Next.js, leverage React Server Components to keep your client-side bundles small and your SEO high.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Schema First:&lt;/strong&gt; Define your database models in Mongoose before you touch the UI. This acts as the source of truth for your entire app.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Global Middleware:&lt;/strong&gt; Use middleware to handle authentication checks once, rather than checking session state on every single page or component.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="/images/articles/saas-app-onboarding-screen.jpg" class="article-body-image-wrapper"&gt;&lt;img src="/images/articles/saas-app-onboarding-screen.jpg" alt="SaaS app onboarding screen with modern dashboard UI"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;SassyPack was built to be the engine under the hood of your SaaS. It uses the MERN stack with Next.js to provide a seamless developer experience. It includes everything mentioned above: pre-built auth, integrated payments with Stripe and Paystack, and a gorgeous dashboard UI.&lt;/p&gt;

&lt;p&gt;Instead of fighting with infrastructure, you can &lt;a href="https://sassypack.collabtower.com/blog/build-saas-with-sassypack" rel="noopener noreferrer"&gt;build SaaS with SassyPack&lt;/a&gt; and focus on the 10% of your code that is actually unique. It is the difference between being a mechanic and being a driver.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-World Use Case: The AI Content Tool
&lt;/h3&gt;

&lt;p&gt;Suppose you want to build a tool that generates LinkedIn posts using OpenAI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Without SassyPack:&lt;/strong&gt; You spend 2 weeks on the login, the Stripe integration, and the user profile. You spend 1 day on the OpenAI prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With SassyPack:&lt;/strong&gt; You spend 1 hour on setup. You spend 2 days on the OpenAI prompt, the UI for the generator, and refining the output quality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You launch 12 days earlier. In the SaaS world, 12 days is an eternity of feedback you could have been receiving.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Stop Coding Boilerplate:&lt;/strong&gt; If a library or kit exists for it, use it.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Focus on the Core Loop:&lt;/strong&gt; What is the one thing your user comes to your site to do? Code that first.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Use a Proven Starter:&lt;/strong&gt; Get the MERN stack advantage without the setup headache.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Closing CTA
&lt;/h3&gt;

&lt;p&gt;Stop letting the "boring stuff" hold your ideas hostage. You have the skills to build something great, so don't waste them on login forms and billing webhooks. Use a professional &lt;a href="https://sassypack.collabtower.com/blog/nextjs-saas-starter-kit-2" rel="noopener noreferrer"&gt;Next.js SaaS Starter Kit&lt;/a&gt; to handle the heavy lifting while you focus on building the next big thing.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Zero to SaaS vs ShipFast, Which One Actually Helps You Build a Real SaaS?</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Fri, 05 Dec 2025 17:45:45 +0000</pubDate>
      <link>https://dev.to/thekarlesi/zero-to-saas-vs-shipfast-which-one-actually-helps-you-build-a-real-saas-3jng</link>
      <guid>https://dev.to/thekarlesi/zero-to-saas-vs-shipfast-which-one-actually-helps-you-build-a-real-saas-3jng</guid>
      <description>&lt;p&gt;If you are trying to launch your first SaaS product, two paths usually show up. You can buy a template like ShipFast or you can follow a full step by step program like &lt;a href="https://zero-to-saas.collabtower.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Zero to SaaS&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Both solutions help you move faster, but they help in completely different ways. One gives you a ready made codebase, the other teaches you how to build a complete SaaS from scratch. In this article, you will see the advantages of each approach, the limitations, and which one actually helps you become a real builder.&lt;/p&gt;




&lt;h2&gt;
  
  
  What ShipFast Really Offers
&lt;/h2&gt;

&lt;p&gt;ShipFast is a production grade starter template built with Next.js. You get a full SaaS application that is ready to deploy with all the typical pieces already included.&lt;/p&gt;

&lt;h3&gt;
  
  
  ShipFast strengths:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A completed codebase with routing, auth, payments, and dashboard
&lt;/li&gt;
&lt;li&gt;Stripe subscriptions already integrated
&lt;/li&gt;
&lt;li&gt;Prebuilt landing page and onboarding
&lt;/li&gt;
&lt;li&gt;Helps experienced developers launch very quickly
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ShipFast limitations for beginners:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Customization becomes difficult if you do not understand the code
&lt;/li&gt;
&lt;li&gt;You are modifying someone else’s architecture
&lt;/li&gt;
&lt;li&gt;Debugging takes longer because you did not build it
&lt;/li&gt;
&lt;li&gt;You are fast, but you do not learn core SaaS fundamentals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ShipFast makes sense if you already know what you are doing. If you are a beginner, the speed benefit is real, but the learning gap becomes a long term disadvantage.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Zero to SaaS Actually Teaches
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://zero-to-saas.collabtower.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Zero to SaaS&lt;/strong&gt;&lt;/a&gt; is a complete training program that shows beginners how to build a full SaaS product from scratch using Next.js, Stripe, MongoDB, and Vercel. You follow a clear path and write the code yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero to SaaS benefits:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You build each part yourself, so you learn everything
&lt;/li&gt;
&lt;li&gt;Covers auth, protected routes, subscriptions, dashboards, and API design
&lt;/li&gt;
&lt;li&gt;Perfect for beginners who want structure and clarity
&lt;/li&gt;
&lt;li&gt;Helps you understand real world SaaS architecture
&lt;/li&gt;
&lt;li&gt;Gives you confidence to build future products on your own
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where Zero to SaaS can feel slower:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You are writing all the code manually
&lt;/li&gt;
&lt;li&gt;You will debug your own mistakes
&lt;/li&gt;
&lt;li&gt;It requires commitment and practice
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal of Zero to SaaS is not speed at the beginning. The goal is to turn you into someone who understands how SaaS is built, so you can build anything later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Zero to SaaS vs ShipFast, Side by Side
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Speed
&lt;/h3&gt;

&lt;p&gt;ShipFast is faster at the start because everything is already built.&lt;br&gt;&lt;br&gt;
Zero to SaaS is slower because you are learning fundamentals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Learning
&lt;/h3&gt;

&lt;p&gt;Zero to SaaS wins because you build the system step by step.&lt;br&gt;&lt;br&gt;
ShipFast gives you a completed solution without explaining how it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customization
&lt;/h3&gt;

&lt;p&gt;Zero to SaaS wins because you understand every component you create.&lt;br&gt;&lt;br&gt;
ShipFast becomes harder to modify if you do not know the underlying patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Long term skills
&lt;/h3&gt;

&lt;p&gt;Zero to SaaS wins easily. Skills compound and unlock future products.&lt;/p&gt;

&lt;h3&gt;
  
  
  Launching an actual SaaS
&lt;/h3&gt;

&lt;p&gt;Both work, but the path depends on your background.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who Should Use ShipFast?
&lt;/h2&gt;

&lt;p&gt;ShipFast is a better choice if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Already understand Next.js
&lt;/li&gt;
&lt;li&gt;Can read and modify complex codebases
&lt;/li&gt;
&lt;li&gt;Want speed more than education
&lt;/li&gt;
&lt;li&gt;Are an experienced developer or repeat founder
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have shipped apps before and you only need acceleration, a ready made template makes sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who Should Choose Zero to SaaS?
&lt;/h2&gt;

&lt;p&gt;Zero to SaaS is the right choice if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are a beginner or intermediate developer
&lt;/li&gt;
&lt;li&gt;Want to understand authentication, billing, database models, and dashboard logic
&lt;/li&gt;
&lt;li&gt;Want to build multiple SaaS products in the future
&lt;/li&gt;
&lt;li&gt;Prefer clarity and structured guidance
&lt;/li&gt;
&lt;li&gt;Need hands on practice, not just a template
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You might move slower in the first week, but you become ten times faster later because you understand what you are doing.&lt;/p&gt;




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

&lt;p&gt;If you want instant acceleration, ShipFast gives you a head start.&lt;br&gt;&lt;br&gt;
If you want long term skill, the ability to build any SaaS you imagine, and full understanding of Next.js architecture, &lt;a href="https://zero-to-saas.collabtower.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Zero to SaaS&lt;/strong&gt;&lt;/a&gt; is the stronger long term path.&lt;/p&gt;

&lt;p&gt;Templates are shortcuts, skills compound. Zero to SaaS gives you the foundation that keeps paying off with every project you build.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Developer’s Paradox: Why You Need a Next.js SaaS Starter Kit to Stop Coding and Start Selling</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Thu, 20 Nov 2025 12:24:34 +0000</pubDate>
      <link>https://dev.to/thekarlesi/the-developers-paradox-why-you-need-a-nextjs-saas-starter-kit-to-stop-coding-and-start-selling-4a9k</link>
      <guid>https://dev.to/thekarlesi/the-developers-paradox-why-you-need-a-nextjs-saas-starter-kit-to-stop-coding-and-start-selling-4a9k</guid>
      <description>&lt;p&gt;You have a brilliant idea. It came to you in the shower or during a commute, a SaaS concept that solves a specific pain point, has a clear target audience, and potential for recurring revenue. You rush to your computer, fire up your terminal, and type &lt;code&gt;npx create-next-app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The adrenaline is pumping. You are ready to build the next unicorn.&lt;/p&gt;

&lt;p&gt;But then, reality hits. Before you can write a single line of logic that makes your app unique, you have to set up authentication. Then you need to configure the database connection. Then comes the Stripe integration, webhook listeners, protected routes, email transaction providers, and responsive dashboard layouts.&lt;/p&gt;

&lt;p&gt;Three weeks later, you are still debugging a JWT token issue. Your enthusiasm has waned, and your "brilliant idea" is gathering dust in a folder named &lt;code&gt;project-final-v2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the "Developer’s Paradox": &lt;strong&gt;The more you love to code, the slower you are at launching.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This guide explores how shifting your mindset from "builder" to "assembler" using a &lt;strong&gt;Next.js SaaS starter kit&lt;/strong&gt; can save your startup before you even write the first line of business logic.&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%2Fsassypack.collabtower.com%2Fimages%2Farticles%2Fcode-editor-mern-stack-setup.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%2Fsassypack.collabtower.com%2Fimages%2Farticles%2Fcode-editor-mern-stack-setup.jpg" alt="Code editor showing MERN stack setup with Next.js and MongoDB" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: The "Configuration Purgatory"
&lt;/h2&gt;

&lt;p&gt;Why is building a SaaS application from scratch so deceptively difficult?&lt;/p&gt;

&lt;p&gt;On the surface, a web app seems simple. You need a frontend, a backend, and a database. However, modern SaaS standards have raised the bar. Users expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; impeccable authentication (Google login, magic links, password resets).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed:&lt;/strong&gt; Sub-second page loads and SEO optimization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability:&lt;/strong&gt; recurring billing that handles failed payments and upgrades automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experience:&lt;/strong&gt; A sleek, mobile-responsive dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building this infrastructure manually is what we call "Configuration Purgatory."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Math of Manual Setup
&lt;/h3&gt;

&lt;p&gt;Let’s break down the time cost for a senior developer building a standard MERN (MongoDB, Express, React, Node) stack app with Next.js capabilities from zero:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication System (Auth.js / NextAuth):&lt;/strong&gt; 15–20 hours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe/Payment Integration (Webhooks, Checkout):&lt;/strong&gt; 20–30 hours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Schema &amp;amp; ORM Setup:&lt;/strong&gt; 10 hours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard UI &amp;amp; Component Library:&lt;/strong&gt; 25+ hours&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment Pipelines &amp;amp; SEO Config:&lt;/strong&gt; 10 hours&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total wasted time:&lt;/strong&gt; ~80 to 100 hours.&lt;/p&gt;

&lt;p&gt;That is two and a half weeks of full-time work just to get to the &lt;em&gt;starting line&lt;/em&gt;. For an indie hacker working nights and weekends, that’s two months. This is exactly &lt;a href="https://sassypack.collabtower.com/blog/why-devs-waste-weeks-building-boilerplate" rel="noopener noreferrer"&gt;why devs waste weeks building boilerplate&lt;/a&gt; instead of shipping products.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Shift: Enter the Modern SaaS Starter Kit
&lt;/h2&gt;

&lt;p&gt;Smart founders are moving away from &lt;code&gt;create-app&lt;/code&gt; and toward &lt;strong&gt;SaaS app boilerplate solutions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A SaaS starter kit is a pre-configured codebase that includes all the boring, repetitive features every SaaS needs. It is the difference between building a car engine from raw steel versus buying a reliable engine and building a custom car body around it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why MERN + Next.js?
&lt;/h3&gt;

&lt;p&gt;When looking for the &lt;strong&gt;best MERN stack starter kits&lt;/strong&gt;, the technology choice matters. The MERN stack (MongoDB, Express, React, Node.js) combined with the power of &lt;strong&gt;Next.js&lt;/strong&gt; is currently the gold standard for solopreneurs and startups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Next.js improves web development:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Server-Side Rendering (SSR):&lt;/strong&gt; Unlike standard React apps, Next.js renders pages on the server. This is critical for SEO, allowing your marketing pages to rank on Google immediately.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Unified Backend/Frontend:&lt;/strong&gt; With Next.js API routes, you can often skip a separate Express server for smaller apps, or integrate them tightly for larger ones.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Vercel Deployment:&lt;/strong&gt; Next.js apps can be deployed globally in seconds.&lt;/li&gt;
&lt;/ol&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%2Fsassypack.collabtower.com%2Fimages%2Farticles%2Fmern-architecture-diagram.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%2Fsassypack.collabtower.com%2Fimages%2Farticles%2Fmern-architecture-diagram.jpg" alt="High-level architecture diagram of a MERN SaaS application" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep Dive: What’s Inside a Production-Ready Kit?
&lt;/h2&gt;

&lt;p&gt;Not all boilerplates are created equal. To truly understand &lt;strong&gt;how to build SaaS MVP fast&lt;/strong&gt;, you need to know what components are non-negotiable in a starter kit.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Robust Authentication
&lt;/h3&gt;

&lt;p&gt;A simple email/password form isn't enough. A &lt;strong&gt;SaaS starter kit with auth&lt;/strong&gt; must handle session management, protected routes (keeping non-paying users out of premium features), and social logins (Google/GitHub).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Monetization Infrastructure
&lt;/h3&gt;

&lt;p&gt;This is usually the hardest part for developers. It’s not just about a "Buy" button. You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subscription management (Monthly/Yearly toggle).&lt;/li&gt;
&lt;li&gt;Webhook handling (what happens when a credit card fails?).&lt;/li&gt;
&lt;li&gt;Customer portals (so users can cancel or upgrade without emailing you).&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Handling payments manually is risky. A good starter kit pre-integrates Stripe or Paystack so you don't touch sensitive financial data. For a deeper look at integration, read our guide on &lt;a href="https://sassypack.collabtower.com/blog/how-to-add-stripe-or-paystack-payments-to-your-saas" rel="noopener noreferrer"&gt;adding Stripe or Paystack payments to your SaaS&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. The Dashboard UI
&lt;/h3&gt;

&lt;p&gt;Your users spend 90% of their time in the dashboard. If it looks clunky, they will churn. Modern kits utilize Tailwind CSS to provide beautiful, responsive components out of the box.&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%2Fsassypack.collabtower.com%2Fimages%2Farticles%2Fsaas-app-onboarding-screen.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%2Fsassypack.collabtower.com%2Fimages%2Farticles%2Fsaas-app-onboarding-screen.jpg" alt="SaaS app onboarding screen with modern dashboard UI" width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How SassyPack Solves the Paradox
&lt;/h2&gt;

&lt;p&gt;If you are looking for a &lt;strong&gt;SassyPack MERN &amp;amp; Next.js starter kit&lt;/strong&gt; that bridges the gap between a raw framework and a finished product, this is where we come in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sassypack.collabtower.com/" rel="noopener noreferrer"&gt;SassyPack&lt;/a&gt; was built to eliminate the 100 hours of setup we mentioned earlier. It is a fully functional SaaS application waiting for your unique idea.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features of SassyPack:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full Stack MERN &amp;amp; Next.js:&lt;/strong&gt; The most popular, hireable stack in the world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-built Auth:&lt;/strong&gt; Secure login flows ready to go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe &amp;amp; Paystack Ready:&lt;/strong&gt; Just add your API keys, and you are ready to take money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beautiful Dashboard:&lt;/strong&gt; Professional UI components based on Tailwind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO Optimized:&lt;/strong&gt; Meta tags, sitemaps, and structured data configurations are pre-set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SassyPack isn't just a template; it's a &lt;strong&gt;full stack app boilerplate setup&lt;/strong&gt; designed to let you deploy on Day 1.&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%2Fsassypack.collabtower.com%2Fimages%2Farticles%2Fdeveloper-building-saas-dashboard.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%2Fsassypack.collabtower.com%2Fimages%2Farticles%2Fdeveloper-building-saas-dashboard.jpg" alt="Developer building a SaaS dashboard using SassyPack" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Case Study: The "Weekend Launch"
&lt;/h2&gt;

&lt;p&gt;Let’s look at a hypothetical comparison of two developers, Sarah and Mike. Both have an idea for an AI-powered writing assistant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "From Scratch" Path (Mike):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Friday Night:&lt;/strong&gt; Mike initializes a Next.js project. He spends 4 hours choosing a UI library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saturday:&lt;/strong&gt; Mike fights with Auth0 configurations. He gets login working but breaks the session persistence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sunday:&lt;/strong&gt; Mike tries to set up Stripe webhooks. He realizes he needs a backend server to listen to them securely. He starts setting up Express.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; By Monday morning, Mike has a login page and a broken payment button. No AI features built.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The SassyPack Path (Sarah):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Friday Night:&lt;/strong&gt; Sarah downloads &lt;strong&gt;SassyPack&lt;/strong&gt;. She runs &lt;code&gt;npm install&lt;/code&gt; and adds her API keys to the &lt;code&gt;.env&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saturday Morning:&lt;/strong&gt; The dashboard, auth, and payments work immediately. Sarah deletes the placeholder text and connects her OpenAI API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saturday Afternoon:&lt;/strong&gt; She customizes the landing page copy and colors to match her brand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sunday:&lt;/strong&gt; Sarah deploys to Vercel. She posts her product on Product Hunt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; By Monday morning, Sarah has her first 3 paying customers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sarah understood that &lt;strong&gt;rapid web app development tools&lt;/strong&gt; are not "cheating"—they are smart business.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes When Choosing a Starter Kit
&lt;/h2&gt;

&lt;p&gt;If you are browsing for &lt;strong&gt;full stack SaaS starter kit comparison&lt;/strong&gt; guides, watch out for these pitfalls:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Vendor Lock-in
&lt;/h3&gt;

&lt;p&gt;Some kits rely on obscure libraries or proprietary backend-as-a-service tools (like Firebase-only wrappers) that are hard to migrate away from. &lt;strong&gt;SassyPack&lt;/strong&gt; uses standard MERN technologies (MongoDB, Node, React). You own your code.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Bloated Code
&lt;/h3&gt;

&lt;p&gt;Some templates include &lt;em&gt;too much&lt;/em&gt;. If a kit comes with a chat app, a forum, and a blog when you only need a dashboard, you will spend days deleting code. Look for a clean architecture that is easy to extend.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Outdated Dependencies
&lt;/h3&gt;

&lt;p&gt;The JavaScript ecosystem moves fast. Ensure the kit you buy is maintained. A &lt;strong&gt;Next.js SaaS starter kit&lt;/strong&gt; running on Next.js 10 is useless in a Next.js 14 world.&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%2Fsassypack.collabtower.com%2Fimages%2Farticles%2Fapp-deployment-process-visual.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%2Fsassypack.collabtower.com%2Fimages%2Farticles%2Fapp-deployment-process-visual.jpg" alt="Visual walkthrough of app deployment workflow on Vercel" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Action Plan: From Zero to Launch in 3 Steps
&lt;/h2&gt;

&lt;p&gt;Ready to stop configuring and start shipping? Here is your roadmap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Validate, Don’t Code
&lt;/h3&gt;

&lt;p&gt;Before you buy anything, write down your idea. Who is it for? What is the one core feature they need? If you need help here, check out our article on &lt;a href="https://sassypack.collabtower.com/blog/building-saas-apps-with-mern-stack" rel="noopener noreferrer"&gt;building SaaS apps with the MERN stack&lt;/a&gt; to understand the architecture better.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Acquire the Infrastructure
&lt;/h3&gt;

&lt;p&gt;Don’t build the foundation; buy it. Download &lt;strong&gt;SassyPack&lt;/strong&gt; to secure your authentication, database connection, and payment processing instantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: The "One Feature" Sprint
&lt;/h3&gt;

&lt;p&gt;Build &lt;em&gt;only&lt;/em&gt; the core value proposition of your app. If it’s an image generator, build the generation form. If it’s an analytics tool, build the chart. Do not build a blog or a help center yet. Use SassyPack's pre-built UI components to slap this feature into the dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Deploy
&lt;/h3&gt;

&lt;p&gt;Push your code to GitHub and connect it to Vercel. Because SassyPack is optimized for &lt;strong&gt;rapid deployment solutions for SaaS&lt;/strong&gt;, your live URL will be ready in minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: Speed is Your Only Advantage
&lt;/h2&gt;

&lt;p&gt;As an indie hacker or a small team, you cannot out-spend Google. You cannot out-hire Microsoft. Your only competitive advantage is &lt;strong&gt;speed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every hour you spend writing boilerplate code is an hour you are not talking to customers or improving your core product.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Next.js SaaS starter kit&lt;/strong&gt; like SassyPack isn't just a collection of files; it is a time machine. It buys you the 3 weeks you would have lost to "Configuration Purgatory" and hands them back to you so you can focus on what actually matters: building a business.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ready to launch your SaaS this weekend?&lt;/strong&gt;&lt;br&gt;
Stop reinventing the wheel. Get the complete &lt;a href="https://sassypack.collabtower.com/" rel="noopener noreferrer"&gt;SassyPack&lt;/a&gt; toolkit today and turn your idea into recurring revenue 10x faster.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>saas</category>
      <category>webdev</category>
      <category>mern</category>
    </item>
  </channel>
</rss>
