<?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>Why Great Developers Get Rejected Before a Human Reads Their Resume</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 09 Jun 2026 12:58:27 +0000</pubDate>
      <link>https://dev.to/thekarlesi/why-great-developers-get-rejected-before-a-human-reads-their-resume-pi6</link>
      <guid>https://dev.to/thekarlesi/why-great-developers-get-rejected-before-a-human-reads-their-resume-pi6</guid>
      <description>&lt;p&gt;Most developers assume that if they're qualified, they'll get an interview.&lt;/p&gt;

&lt;p&gt;Unfortunately, that's not how hiring works anymore.&lt;/p&gt;

&lt;p&gt;Before your resume reaches an engineering manager, recruiter, or CTO, it usually passes through an Applicant Tracking System (ATS).&lt;/p&gt;

&lt;p&gt;And that system doesn't care how talented you are.&lt;/p&gt;

&lt;p&gt;It only cares whether your resume matches what the job description is asking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Filter
&lt;/h2&gt;

&lt;p&gt;Imagine two developers applying for the same role.&lt;/p&gt;

&lt;p&gt;Developer A has built production systems, shipped features, and contributed to open-source projects.&lt;/p&gt;

&lt;p&gt;Developer B has less experience but uses the exact language found in the job description.&lt;/p&gt;

&lt;p&gt;Many ATS systems will rank Developer B higher.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because the software cannot infer experience the way a human can. It primarily evaluates keyword alignment, skills, technologies, and relevance.&lt;/p&gt;

&lt;p&gt;If the posting mentions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes&lt;/li&gt;
&lt;li&gt;CI/CD&lt;/li&gt;
&lt;li&gt;Microservices&lt;/li&gt;
&lt;li&gt;AWS&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...and your resume doesn't mention them, even if you've used them extensively, you may score poorly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Generic Resumes
&lt;/h2&gt;

&lt;p&gt;Many developers use a single resume for every application.&lt;/p&gt;

&lt;p&gt;The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing role-specific keywords&lt;/li&gt;
&lt;li&gt;Generic project descriptions&lt;/li&gt;
&lt;li&gt;Weak accomplishment statements&lt;/li&gt;
&lt;li&gt;Skills sections that don't match the role&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A backend engineering position and a frontend engineering position may require completely different keyword profiles.&lt;/p&gt;

&lt;p&gt;Using the same resume everywhere dramatically reduces your chances of passing ATS screening.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ATS Systems Actually Look For
&lt;/h2&gt;

&lt;p&gt;Most systems evaluate combinations of:&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Skills
&lt;/h3&gt;

&lt;p&gt;Programming languages, frameworks, databases, and tools.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;AWS&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Experience Relevance
&lt;/h3&gt;

&lt;p&gt;Do your previous roles resemble the target role?&lt;/p&gt;

&lt;h3&gt;
  
  
  Keyword Alignment
&lt;/h3&gt;

&lt;p&gt;Are the important terms from the job description present throughout your resume?&lt;/p&gt;

&lt;h3&gt;
  
  
  Resume Structure
&lt;/h3&gt;

&lt;p&gt;Can the ATS parse your document correctly?&lt;/p&gt;

&lt;p&gt;Fancy layouts often create parsing problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  How To Improve Your ATS Score
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Use the Exact Job Description
&lt;/h3&gt;

&lt;p&gt;Never optimize against a generic role title.&lt;/p&gt;

&lt;p&gt;Always analyze your resume against the actual posting.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Match Skills Honestly
&lt;/h3&gt;

&lt;p&gt;If you've used a technology professionally, make sure it's represented clearly.&lt;/p&gt;

&lt;p&gt;Don't keyword stuff.&lt;/p&gt;

&lt;p&gt;Don't invent experience.&lt;/p&gt;

&lt;p&gt;Simply make your existing experience visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rewrite Weak Bullet Points
&lt;/h3&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;"Worked on backend services."&lt;/p&gt;

&lt;p&gt;Write:&lt;/p&gt;

&lt;p&gt;"Built Node.js microservices and REST APIs supporting 40,000 monthly users."&lt;/p&gt;

&lt;p&gt;The second version provides context, technologies, and impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Quantify Results
&lt;/h3&gt;

&lt;p&gt;Recruiters and ATS systems both benefit from measurable outcomes.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduced load times by 35%&lt;/li&gt;
&lt;li&gt;Automated deployment workflows&lt;/li&gt;
&lt;li&gt;Increased system reliability&lt;/li&gt;
&lt;li&gt;Improved API response times&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I Built Hireva
&lt;/h2&gt;

&lt;p&gt;After seeing how many qualified candidates struggled to get interviews, I built Hireva.&lt;/p&gt;

&lt;p&gt;Instead of guessing whether a resume matches a role, users can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload their resume&lt;/li&gt;
&lt;li&gt;Paste the job description&lt;/li&gt;
&lt;li&gt;Receive an ATS match score&lt;/li&gt;
&lt;li&gt;Identify missing keywords&lt;/li&gt;
&lt;li&gt;Find improvement opportunities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to trick ATS systems.&lt;/p&gt;

&lt;p&gt;The goal is to help qualified candidates communicate their experience more effectively.&lt;/p&gt;

&lt;p&gt;You can check it out here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hireva.collabtower.com/" rel="noopener noreferrer"&gt;https://hireva.collabtower.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Most developers spend months learning frameworks, databases, cloud platforms, and system design.&lt;/p&gt;

&lt;p&gt;Then they lose opportunities because their resume doesn't communicate that experience in a format hiring systems understand.&lt;/p&gt;

&lt;p&gt;Before submitting your next application, compare your resume against the actual job description.&lt;/p&gt;

&lt;p&gt;A few targeted improvements can make the difference between being filtered out and getting the interview.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>programming</category>
      <category>career</category>
    </item>
    <item>
      <title>10 Best SaaS Boilerplates for Next.js</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Mon, 08 Jun 2026 13:20:13 +0000</pubDate>
      <link>https://dev.to/thekarlesi/10-best-saas-boilerplates-for-nextjs-41pa</link>
      <guid>https://dev.to/thekarlesi/10-best-saas-boilerplates-for-nextjs-41pa</guid>
      <description>&lt;p&gt;Building a SaaS product is hard enough.&lt;/p&gt;

&lt;p&gt;Spending weeks setting up authentication, payments, dashboards, blogs, analytics, and deployment infrastructure makes it even harder.&lt;/p&gt;

&lt;p&gt;That's why SaaS boilerplates have become one of the fastest ways to launch products.&lt;/p&gt;

&lt;p&gt;Instead of starting from a blank repository, founders can focus on solving customer problems while the foundation is already in place.&lt;/p&gt;

&lt;p&gt;Here are the best SaaS boilerplates for Next.js in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. SassyPack
&lt;/h2&gt;

&lt;p&gt;Best for: Founders who want production-ready SaaS infrastructure without unnecessary complexity&lt;/p&gt;

&lt;p&gt;SassyPack focuses on the features most SaaS founders need before launch.&lt;/p&gt;

&lt;p&gt;Included modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Protected routes&lt;/li&gt;
&lt;li&gt;Stripe billing&lt;/li&gt;
&lt;li&gt;User dashboard&lt;/li&gt;
&lt;li&gt;SEO-ready blog&lt;/li&gt;
&lt;li&gt;PostHog analytics&lt;/li&gt;
&lt;li&gt;Open Graph image setup&lt;/li&gt;
&lt;li&gt;Landing pages&lt;/li&gt;
&lt;li&gt;Profile management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike many starter kits that include dozens of rarely used features, SassyPack stays focused on launch-critical functionality.&lt;/p&gt;

&lt;p&gt;Highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-time purchase&lt;/li&gt;
&lt;li&gt;Lifetime updates&lt;/li&gt;
&lt;li&gt;Source code included&lt;/li&gt;
&lt;li&gt;MERN + Next.js stack&lt;/li&gt;
&lt;li&gt;Production-tested architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Learn more:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sassypack.collabtower.com/" rel="noopener noreferrer"&gt;https://sassypack.collabtower.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. ShipFast
&lt;/h2&gt;

&lt;p&gt;Best for: Fast MVP launches&lt;/p&gt;

&lt;p&gt;ShipFast became popular among indie hackers for helping founders launch quickly.&lt;/p&gt;

&lt;p&gt;Strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fast setup&lt;/li&gt;
&lt;li&gt;Stripe integration&lt;/li&gt;
&lt;li&gt;Community support&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. MakerKit
&lt;/h2&gt;

&lt;p&gt;Best for: Large SaaS applications&lt;/p&gt;

&lt;p&gt;MakerKit provides a comprehensive foundation for SaaS products with advanced features.&lt;/p&gt;

&lt;p&gt;Strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Team support&lt;/li&gt;
&lt;li&gt;Multi-tenancy&lt;/li&gt;
&lt;li&gt;Enterprise-ready architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Supastarter
&lt;/h2&gt;

&lt;p&gt;Best for: Supabase users&lt;/p&gt;

&lt;p&gt;Built around Supabase and Next.js.&lt;/p&gt;

&lt;p&gt;Strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supabase integration&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Modern architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Nextbase
&lt;/h2&gt;

&lt;p&gt;Best for: Simplicity&lt;/p&gt;

&lt;p&gt;A lightweight starter designed for developers who prefer minimal abstractions.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. SaaS Pegasus
&lt;/h2&gt;

&lt;p&gt;Best for: Django developers&lt;/p&gt;

&lt;p&gt;Focused on rapid SaaS development using Django.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. LaunchFast
&lt;/h2&gt;

&lt;p&gt;Best for: MVP validation&lt;/p&gt;

&lt;p&gt;Strong choice for testing ideas quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Gravity
&lt;/h2&gt;

&lt;p&gt;Best for: Startups building internal tools&lt;/p&gt;

&lt;p&gt;Offers reusable components and dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. TurboStarter
&lt;/h2&gt;

&lt;p&gt;Best for: Monorepo architecture&lt;/p&gt;

&lt;p&gt;Built around modern Turborepo workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Open SaaS
&lt;/h2&gt;

&lt;p&gt;Best for: Open-source enthusiasts&lt;/p&gt;

&lt;p&gt;Provides a free starting point for SaaS builders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Founders Buy Boilerplates
&lt;/h2&gt;

&lt;p&gt;The biggest reason is not coding speed.&lt;/p&gt;

&lt;p&gt;It's reducing decision fatigue.&lt;/p&gt;

&lt;p&gt;Most founders lose time rebuilding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Billing systems&lt;/li&gt;
&lt;li&gt;User dashboards&lt;/li&gt;
&lt;li&gt;Blog infrastructure&lt;/li&gt;
&lt;li&gt;Analytics&lt;/li&gt;
&lt;li&gt;SEO foundations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A boilerplate eliminates that repetitive work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes a Great SaaS Boilerplate?
&lt;/h2&gt;

&lt;p&gt;The best boilerplates provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Production-ready architecture&lt;/li&gt;
&lt;li&gt;Stripe billing&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;SEO support&lt;/li&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;Easy customization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More features are not always better.&lt;/p&gt;

&lt;p&gt;The best starter kits focus on what founders actually need to launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Launching a SaaS product is already difficult.&lt;/p&gt;

&lt;p&gt;Your starter kit should remove obstacles, not add complexity.&lt;/p&gt;

&lt;p&gt;If you're building with Next.js and want a focused foundation that already includes auth, billing, dashboards, analytics, and SEO infrastructure, SassyPack is one of the strongest options available today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sassypack.collabtower.com/" rel="noopener noreferrer"&gt;https://sassypack.collabtower.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>nextjs</category>
      <category>beginners</category>
    </item>
    <item>
      <title>7 Best SaaS Courses for Developers Who Want to Launch a Product in 2026</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 02 Jun 2026 20:19:02 +0000</pubDate>
      <link>https://dev.to/thekarlesi/7-best-saas-courses-for-developers-who-want-to-launch-a-product-in-2026-4829</link>
      <guid>https://dev.to/thekarlesi/7-best-saas-courses-for-developers-who-want-to-launch-a-product-in-2026-4829</guid>
      <description>&lt;p&gt;Building a SaaS has never been easier.&lt;/p&gt;

&lt;p&gt;Launching one has never been harder.&lt;/p&gt;

&lt;p&gt;Most developers don't struggle with coding. They struggle with turning knowledge into a finished product.&lt;/p&gt;

&lt;p&gt;That's why SaaS-focused courses have exploded in popularity. The right course can save weeks of confusion around architecture, authentication, payments, deployment, and launch strategy.&lt;/p&gt;

&lt;p&gt;After reviewing popular options, here are some of the best SaaS courses and learning resources available in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Zero to SaaS
&lt;/h2&gt;

&lt;p&gt;Best for: Developers who want to build and launch a SaaS quickly&lt;/p&gt;

&lt;p&gt;Many courses spend dozens of hours teaching concepts without helping students ship.&lt;/p&gt;

&lt;p&gt;Zero to SaaS takes a different approach.&lt;/p&gt;

&lt;p&gt;The blueprint is designed around a simple outcome:&lt;/p&gt;

&lt;p&gt;Build and launch a SaaS in 14 focused days.&lt;/p&gt;

&lt;p&gt;Instead of overwhelming students with endless theory, it walks through the critical decisions involved in creating a launch-ready product, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js architecture&lt;/li&gt;
&lt;li&gt;Database design&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Stripe payments&lt;/li&gt;
&lt;li&gt;Deployment&lt;/li&gt;
&lt;li&gt;Launch preparation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The focus is execution rather than information consumption.&lt;/p&gt;

&lt;p&gt;If you've spent months learning but haven't shipped anything, this practical approach is refreshing.&lt;/p&gt;

&lt;p&gt;Website:&lt;br&gt;
&lt;a href="https://zero-to-saas.collabtower.com/" rel="noopener noreferrer"&gt;https://zero-to-saas.collabtower.com/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Full Stack Open
&lt;/h2&gt;

&lt;p&gt;Best for: Deep technical learning&lt;/p&gt;

&lt;p&gt;Full Stack Open is one of the most respected free developer programs available.&lt;/p&gt;

&lt;p&gt;It covers modern web development, React, APIs, testing, and backend systems in significant depth.&lt;/p&gt;

&lt;p&gt;The downside is that it's designed more for learning engineering concepts than launching products.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Odin Project
&lt;/h2&gt;

&lt;p&gt;Best for: Beginners starting from scratch&lt;/p&gt;

&lt;p&gt;The Odin Project remains one of the best free resources for aspiring developers.&lt;/p&gt;

&lt;p&gt;It provides a structured curriculum covering frontend and backend development.&lt;/p&gt;

&lt;p&gt;For SaaS founders, it's an excellent foundation before moving into product building.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Buildspace
&lt;/h2&gt;

&lt;p&gt;Best for: Community-driven builders&lt;/p&gt;

&lt;p&gt;Buildspace became popular for helping creators build projects alongside other developers.&lt;/p&gt;

&lt;p&gt;The community aspect is one of its biggest strengths.&lt;/p&gt;

&lt;p&gt;Students gain accountability and feedback while working on real products.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Indie Hackers
&lt;/h2&gt;

&lt;p&gt;Best for: Learning from founders&lt;/p&gt;

&lt;p&gt;While not technically a course, Indie Hackers offers an enormous amount of practical startup knowledge.&lt;/p&gt;

&lt;p&gt;You'll find interviews, case studies, launch stories, and lessons from bootstrapped founders.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Y Combinator Startup School
&lt;/h2&gt;

&lt;p&gt;Best for: Startup fundamentals&lt;/p&gt;

&lt;p&gt;Startup School focuses less on coding and more on company building.&lt;/p&gt;

&lt;p&gt;Topics include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;Distribution&lt;/li&gt;
&lt;li&gt;Customer discovery&lt;/li&gt;
&lt;li&gt;Growth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's valuable once you've moved beyond the technical stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. freeCodeCamp
&lt;/h2&gt;

&lt;p&gt;Best for: Expanding technical skills&lt;/p&gt;

&lt;p&gt;freeCodeCamp provides thousands of hours of free programming content.&lt;/p&gt;

&lt;p&gt;It's an excellent supplement for developers looking to strengthen specific skills while building products.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes a Great SaaS Course?
&lt;/h2&gt;

&lt;p&gt;The best SaaS courses do more than teach code.&lt;/p&gt;

&lt;p&gt;They help students answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What should I build?&lt;/li&gt;
&lt;li&gt;How much should I build?&lt;/li&gt;
&lt;li&gt;When should I launch?&lt;/li&gt;
&lt;li&gt;How do I charge users?&lt;/li&gt;
&lt;li&gt;How do I avoid overengineering?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't knowledge accumulation.&lt;/p&gt;

&lt;p&gt;The goal is product creation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Course Should You Choose?
&lt;/h2&gt;

&lt;p&gt;If you're completely new to development, start with foundational resources like The Odin Project or freeCodeCamp.&lt;/p&gt;

&lt;p&gt;If you're already comfortable with React and modern web development, choose a resource focused on shipping products.&lt;/p&gt;

&lt;p&gt;That's where execution-focused programs such as Zero to SaaS provide the most value.&lt;/p&gt;

&lt;p&gt;The difference between learning and launching often comes down to having a clear roadmap.&lt;/p&gt;

&lt;p&gt;And for many developers, that's exactly what's missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Most aspiring founders don't need more information.&lt;/p&gt;

&lt;p&gt;They need a system that helps them finish.&lt;/p&gt;

&lt;p&gt;The internet is full of tutorials.&lt;/p&gt;

&lt;p&gt;Launched products are much harder to find.&lt;/p&gt;

&lt;p&gt;Choose resources that move you closer to shipping, not just studying.&lt;/p&gt;

&lt;p&gt;Because the fastest way to learn SaaS is still to build one.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Why Great Software Engineers Get Rejected Before a Human Reads Their Resume</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Sun, 31 May 2026 16:46:48 +0000</pubDate>
      <link>https://dev.to/thekarlesi/why-great-software-engineers-get-rejected-before-a-human-reads-their-resume-2876</link>
      <guid>https://dev.to/thekarlesi/why-great-software-engineers-get-rejected-before-a-human-reads-their-resume-2876</guid>
      <description>&lt;p&gt;Most developers assume that if they're qualified, they'll eventually get an interview.&lt;/p&gt;

&lt;p&gt;Unfortunately, that's not how modern hiring works.&lt;/p&gt;

&lt;p&gt;For many engineering roles, your first reviewer isn't a recruiter.&lt;/p&gt;

&lt;p&gt;It's software.&lt;/p&gt;

&lt;p&gt;Applicant Tracking Systems (ATS) are used by companies to process hundreds or thousands of applications. Before a human sees your resume, an automated system often decides whether your application deserves a closer look.&lt;/p&gt;

&lt;p&gt;The frustrating part?&lt;/p&gt;

&lt;p&gt;You can be qualified and still get filtered out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Resume Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Imagine this job description:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;AWS&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;CI/CD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now imagine your resume says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Built frontend applications&lt;/li&gt;
&lt;li&gt;Developed APIs&lt;/li&gt;
&lt;li&gt;Managed deployments&lt;/li&gt;
&lt;li&gt;Worked with cloud infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A recruiter understands what you mean.&lt;/p&gt;

&lt;p&gt;An ATS might not.&lt;/p&gt;

&lt;p&gt;While modern ATS platforms are becoming smarter, keyword matching still plays a major role in how resumes are ranked and filtered.&lt;/p&gt;

&lt;p&gt;The result is that many developers accidentally hide their experience behind vague language.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Difference Between Experience and Visibility
&lt;/h2&gt;

&lt;p&gt;Many engineers focus on gaining skills.&lt;/p&gt;

&lt;p&gt;Fewer focus on communicating those skills.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;"Worked on backend services."&lt;/p&gt;

&lt;p&gt;Try:&lt;/p&gt;

&lt;p&gt;"Built Node.js REST APIs backed by PostgreSQL serving 50K monthly users."&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;"Maintained deployment pipeline."&lt;/p&gt;

&lt;p&gt;Try:&lt;/p&gt;

&lt;p&gt;"Managed CI/CD workflows using GitHub Actions and Docker, reducing deployment failures by 35%."&lt;/p&gt;

&lt;p&gt;Both statements may describe the same work.&lt;/p&gt;

&lt;p&gt;One is simply easier for both humans and software to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keywords Are Not About Gaming the System
&lt;/h2&gt;

&lt;p&gt;A common criticism of ATS optimization is that it encourages keyword stuffing.&lt;/p&gt;

&lt;p&gt;That's not the goal.&lt;/p&gt;

&lt;p&gt;The goal is clarity.&lt;/p&gt;

&lt;p&gt;If you use React every day, your resume should say React.&lt;/p&gt;

&lt;p&gt;If you've deployed applications with AWS, your resume should say AWS.&lt;/p&gt;

&lt;p&gt;If you've designed microservices, your resume should say microservices.&lt;/p&gt;

&lt;p&gt;You aren't adding fake experience.&lt;/p&gt;

&lt;p&gt;You're making real experience visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Software Engineering Recruiters Actually Look For
&lt;/h2&gt;

&lt;p&gt;Across thousands of engineering job descriptions, certain patterns appear repeatedly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Languages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;Java&lt;/li&gt;
&lt;li&gt;Go&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Infrastructure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AWS&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;Kubernetes&lt;/li&gt;
&lt;li&gt;Terraform&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Development Practices
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;CI/CD&lt;/li&gt;
&lt;li&gt;Unit Testing&lt;/li&gt;
&lt;li&gt;Agile&lt;/li&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;REST APIs&lt;/li&gt;
&lt;li&gt;Microservices&lt;/li&gt;
&lt;li&gt;System Design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact technologies vary by role, but the principle remains the same:&lt;/p&gt;

&lt;p&gt;Recruiters want evidence that you've solved problems using the tools they care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Resume Is Rarely Enough
&lt;/h2&gt;

&lt;p&gt;A mistake I see often is sending the same resume to every role.&lt;/p&gt;

&lt;p&gt;A backend engineering position and a frontend engineering position may prioritize completely different technologies.&lt;/p&gt;

&lt;p&gt;A cloud engineering role may care far more about AWS and Kubernetes than React.&lt;/p&gt;

&lt;p&gt;The strongest applications align the resume with the job description.&lt;/p&gt;

&lt;p&gt;Not by inventing experience.&lt;/p&gt;

&lt;p&gt;By emphasizing the most relevant experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Better Approach
&lt;/h2&gt;

&lt;p&gt;Before applying, compare your resume against the job description.&lt;/p&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing technologies&lt;/li&gt;
&lt;li&gt;Missing frameworks&lt;/li&gt;
&lt;li&gt;Weak bullet points&lt;/li&gt;
&lt;li&gt;Generic wording&lt;/li&gt;
&lt;li&gt;Skills that aren't clearly represented&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The closer your resume reflects the language used by the employer, the easier it becomes for both ATS systems and recruiters to understand your fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Software engineering interviews are difficult enough.&lt;/p&gt;

&lt;p&gt;You shouldn't lose opportunities because your experience wasn't communicated clearly.&lt;/p&gt;

&lt;p&gt;A good resume doesn't just describe what you've done.&lt;/p&gt;

&lt;p&gt;It translates your experience into language that recruiters, hiring managers, and ATS systems can immediately understand.&lt;/p&gt;

&lt;p&gt;If you're curious about which ATS keywords appear most often in software engineering roles, you can explore this breakdown:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hireva.collabtower.com/resume-keywords/software-engineer" rel="noopener noreferrer"&gt;https://hireva.collabtower.com/resume-keywords/software-engineer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The goal isn't to optimize for robots.&lt;/p&gt;

&lt;p&gt;The goal is to make sure your work gets seen by humans.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>career</category>
    </item>
    <item>
      <title>Why Your Resume Keeps Getting Rejected by ATS Systems (Even When You’re Qualified)</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Wed, 27 May 2026 12:05:36 +0000</pubDate>
      <link>https://dev.to/thekarlesi/why-your-resume-keeps-getting-rejected-by-ats-systems-even-when-youre-qualified-53lm</link>
      <guid>https://dev.to/thekarlesi/why-your-resume-keeps-getting-rejected-by-ats-systems-even-when-youre-qualified-53lm</guid>
      <description>&lt;p&gt;If you are applying for software engineering roles and not getting responses, there is a high chance your resume is not failing at the human level.&lt;/p&gt;

&lt;p&gt;It is failing at the parsing level.&lt;/p&gt;

&lt;p&gt;Most companies now use Applicant Tracking Systems (ATS) to automatically filter resumes before a recruiter even opens them. These systems do not care about your intent or potential. They care about structure, keywords, and alignment with the job description.&lt;/p&gt;




&lt;h2&gt;
  
  
  What an ATS Actually Does (From a Developer Perspective)
&lt;/h2&gt;

&lt;p&gt;Think of an ATS as a basic text-matching and ranking system.&lt;/p&gt;

&lt;p&gt;At a high level, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parses your resume into structured fields&lt;/li&gt;
&lt;li&gt;Extracts keywords from job descriptions&lt;/li&gt;
&lt;li&gt;Compares overlap between resume and job posting&lt;/li&gt;
&lt;li&gt;Assigns a match score&lt;/li&gt;
&lt;li&gt;Filters candidates below a threshold&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not AI in the way modern LLMs are. It is closer to keyword indexing, weighted scoring, and rule-based filtering.&lt;/p&gt;

&lt;p&gt;Which means small mismatches can have a big impact.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Developers Get Filtered Out
&lt;/h2&gt;

&lt;p&gt;Even experienced engineers get rejected for reasons that have nothing to do with skill.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Missing exact terminology
&lt;/h3&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;Job description requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes&lt;/li&gt;
&lt;li&gt;CI/CD&lt;/li&gt;
&lt;li&gt;Microservices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your resume says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;container orchestration&lt;/li&gt;
&lt;li&gt;deployment pipelines&lt;/li&gt;
&lt;li&gt;distributed services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You are describing the same experience, but ATS systems often rely on exact keyword matching or weak semantic inference.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Over-generalized bullet points
&lt;/h3&gt;

&lt;p&gt;Bad example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Built backend services for scalable applications&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Good example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Built Node.js microservices deployed on Kubernetes with CI/CD pipelines using GitHub Actions&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second version aligns directly with ATS keyword extraction logic.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Non-standard formatting
&lt;/h3&gt;

&lt;p&gt;ATS parsers struggle with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tables&lt;/li&gt;
&lt;li&gt;multi-column layouts&lt;/li&gt;
&lt;li&gt;icons&lt;/li&gt;
&lt;li&gt;complex PDF structures&lt;/li&gt;
&lt;li&gt;embedded design elements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If parsing fails, keyword extraction becomes incomplete.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. No alignment to job description
&lt;/h3&gt;

&lt;p&gt;Most developers use a single resume for every application.&lt;/p&gt;

&lt;p&gt;ATS systems heavily reward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;job-specific keyword matching&lt;/li&gt;
&lt;li&gt;tailored bullet points&lt;/li&gt;
&lt;li&gt;mirrored terminology from the job post&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How ATS Scoring Works (Simplified)
&lt;/h2&gt;

&lt;p&gt;ATS systems often estimate compatibility like this:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ATS Score = (Matched Keywords / Total Relevant Keywords) × 100&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Some systems also weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;skill frequency&lt;/li&gt;
&lt;li&gt;keyword placement (title vs body)&lt;/li&gt;
&lt;li&gt;recency of experience&lt;/li&gt;
&lt;li&gt;role relevance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But keyword overlap remains the dominant factor.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Practical Debugging Workflow for Developers
&lt;/h2&gt;

&lt;p&gt;If you want to treat your resume like a system you can iterate on:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Extract job keywords
&lt;/h3&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;required skills&lt;/li&gt;
&lt;li&gt;repeated terms&lt;/li&gt;
&lt;li&gt;tools and frameworks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Map your experience
&lt;/h3&gt;

&lt;p&gt;For each keyword:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;do you have it?&lt;/li&gt;
&lt;li&gt;is it explicitly written?&lt;/li&gt;
&lt;li&gt;is it buried in generic wording?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: Rewrite for explicit matching
&lt;/h3&gt;

&lt;p&gt;Do not assume inference.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;worked with cloud infrastructure&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;deployed applications on AWS using EC2, S3, and Docker containers&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Step 4: Re-run against the job description
&lt;/h3&gt;

&lt;p&gt;This is where tools help automate iteration.&lt;/p&gt;

&lt;p&gt;One example is &lt;a href="https://hireva.collabtower.com/?utm_source=dev.to"&gt;Hireva&lt;/a&gt;, which compares your resume directly against a job description and highlights missing keywords and weak alignment areas.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Problem Exists in Modern Hiring Systems
&lt;/h2&gt;

&lt;p&gt;From a systems design perspective, ATS tools exist because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;companies receive hundreds to thousands of applications per role&lt;/li&gt;
&lt;li&gt;manual review does not scale&lt;/li&gt;
&lt;li&gt;keyword filtering is cheap and fast&lt;/li&gt;
&lt;li&gt;ranking systems reduce recruiter workload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the system is optimized for throughput, not nuance.&lt;/p&gt;

&lt;p&gt;That creates a mismatch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;qualified candidates get filtered out&lt;/li&gt;
&lt;li&gt;keyword-optimized candidates get through&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common Misconception Among Developers
&lt;/h2&gt;

&lt;p&gt;Many engineers believe:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“If I am good enough, I will get through.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But ATS systems do not evaluate “good enough.”&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;lexical similarity&lt;/li&gt;
&lt;li&gt;structured relevance&lt;/li&gt;
&lt;li&gt;keyword density&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is closer to search engine ranking than human evaluation.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Fix It Without Gaming the System
&lt;/h2&gt;

&lt;p&gt;You do not need to spam keywords.&lt;/p&gt;

&lt;p&gt;You need to translate your experience into the language the system understands.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Weak&lt;/th&gt;
&lt;th&gt;Strong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;worked on APIs&lt;/td&gt;
&lt;td&gt;built REST APIs using Express.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloud deployment&lt;/td&gt;
&lt;td&gt;deployed services on AWS EC2 with Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD pipelines&lt;/td&gt;
&lt;td&gt;implemented CI/CD using GitHub Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This improves both ATS readability and recruiter clarity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tools That Help (If You Want to Automate This)
&lt;/h2&gt;

&lt;p&gt;There are several ATS optimization tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jobscan – deep keyword matching and scoring
&lt;/li&gt;
&lt;li&gt;Teal – resume + job tracking ecosystem
&lt;/li&gt;
&lt;li&gt;Resume Worded – resume feedback and scoring
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And simpler tools focused on direct job matching, like &lt;a href="https://hireva.collabtower.com/?utm_source=dev.to"&gt;Hireva&lt;/a&gt;, which focuses on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;job-specific ATS scoring&lt;/li&gt;
&lt;li&gt;keyword gap detection&lt;/li&gt;
&lt;li&gt;fast resume-to-job comparison&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;If you are applying to developer roles today, your resume is not just a document.&lt;/p&gt;

&lt;p&gt;It is a structured data input into a filtering system.&lt;/p&gt;

&lt;p&gt;And if your structure does not align with the system’s expectations, your application never reaches a human.&lt;/p&gt;

&lt;p&gt;The goal is not to exaggerate your experience.&lt;/p&gt;

&lt;p&gt;It is to express it in a format machines can correctly interpret.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Stop Learning, Start Building: The Fastest Way to Become a Developer in 2026</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 12 May 2026 10:28:20 +0000</pubDate>
      <link>https://dev.to/thekarlesi/stop-learning-start-building-the-fastest-way-to-become-a-developer-in-2026-21fj</link>
      <guid>https://dev.to/thekarlesi/stop-learning-start-building-the-fastest-way-to-become-a-developer-in-2026-21fj</guid>
      <description>&lt;p&gt;If you’ve been learning programming for a while and still feel stuck, you are not alone.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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




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

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

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

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

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

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

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

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

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




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

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

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




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

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

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

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

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

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




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

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

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

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




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

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

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




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

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

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

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




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

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




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

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

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

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




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

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

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

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




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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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




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

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

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

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

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

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

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




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

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

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

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




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

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

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

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

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




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

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

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

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




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

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

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

</description>
      <category>react</category>
      <category>mongodb</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Google OAuth + Email Auth in Next.js — The Complete SaaS Authentication Guide (2026)</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 24 Mar 2026 14:32:00 +0000</pubDate>
      <link>https://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="" height=""&gt;&lt;/a&gt;&lt;/p&gt;




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

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

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

GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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




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

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

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

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

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

&lt;/div&gt;



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




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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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




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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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




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

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

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

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

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

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

&lt;/div&gt;



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




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

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

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

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

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

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

&lt;/div&gt;



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




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

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

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

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




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

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

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

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

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

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




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

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




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

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

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

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

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




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

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




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

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

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

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

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

</description>
      <category>nextjs</category>
      <category>authentication</category>
      <category>oauth</category>
      <category>saas</category>
    </item>
    <item>
      <title>Stripe Subscription Lifecycle in Next.js — The Complete Developer Guide (2026)</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 17 Mar 2026 13:26:46 +0000</pubDate>
      <link>https://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="" height=""&gt;&lt;/a&gt;&lt;/p&gt;




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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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




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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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




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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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




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

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

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

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

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




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

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

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

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

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

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




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

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




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

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

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

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

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




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

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




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

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

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

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

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

</description>
      <category>nextjs</category>
      <category>stripe</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Beyond find(): Mastering MongoDB Aggregations for Real-Time SaaS Analytics</title>
      <dc:creator>Esimit Karlgusta</dc:creator>
      <pubDate>Tue, 24 Feb 2026 09:04:22 +0000</pubDate>
      <link>https://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>
  </channel>
</rss>
