<?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: Prince</title>
    <description>The latest articles on DEV Community by Prince (@offpage_prince).</description>
    <link>https://dev.to/offpage_prince</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%2F3904148%2F08be118a-2043-4f34-aeeb-decac6008f37.png</url>
      <title>DEV Community: Prince</title>
      <link>https://dev.to/offpage_prince</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/offpage_prince"/>
    <language>en</language>
    <item>
      <title>How to Build a Stripe Customer Portal in Next.js SaaS</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Mon, 25 May 2026 11:02:03 +0000</pubDate>
      <link>https://dev.to/offpage_prince/how-to-build-a-stripe-customer-portal-in-nextjs-saas-1b0n</link>
      <guid>https://dev.to/offpage_prince/how-to-build-a-stripe-customer-portal-in-nextjs-saas-1b0n</guid>
      <description>&lt;p&gt;When building a B2B SaaS application, one feature that often gets overlooked is the customer portal. It's tough when a lot of the complexity is hidden — Stripe handles the billing, but your users still need a place to manage their subscriptions, update payment methods, view invoices, and cancel or upgrade their plans.&lt;/p&gt;

&lt;p&gt;That's where the Stripe Customer Portal comes in. In this guide, we'll walk through how to integrate it into a Next.js SaaS application step by step.&lt;/p&gt;

&lt;p&gt;What is the Stripe Customer Portal?&lt;/p&gt;

&lt;p&gt;The Stripe Customer Portal is a hosted UI that lets your customers manage their own billing. Instead of building a custom billing dashboard, you redirect your users to Stripe's portal where they can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;View and download invoices&lt;/li&gt;
&lt;li&gt;Update their payment method&lt;/li&gt;
&lt;li&gt;Cancel or upgrade their subscription&lt;/li&gt;
&lt;li&gt;Manage billing details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part? You don't have to build any of this yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we start, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Next.js application (v13+ with App Router recommended)&lt;/li&gt;
&lt;li&gt;A Stripe account with billing enabled&lt;/li&gt;
&lt;li&gt;Stripe's Node.js SDK installed: &lt;code&gt;npm install stripe&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Enable the Customer Portal in Stripe Dashboard
&lt;/h2&gt;

&lt;p&gt;First, go to your Stripe Dashboard → &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Billing&lt;/strong&gt; → &lt;strong&gt;Customer Portal&lt;/strong&gt; and enable it. Configure what customers can and cannot do (cancel subscriptions, update payment methods, etc.).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create a Customer Portal Session API Route
&lt;/h2&gt;

&lt;p&gt;Create a new API route in your Next.js app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/billing/portal/route.ts&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;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;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="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="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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="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="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;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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="c1"&gt;// Get the customer's Stripe customer ID from your database&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="nf"&gt;getUserFromDB&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;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="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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="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;No billing account found&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="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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;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;h2&gt;
  
  
  Step 3: Add a "Manage Billing" Button to Your UI
&lt;/h2&gt;

&lt;p&gt;Now create a client component that calls this API and redirects the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/BillingPortalButton.tsx&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;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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BillingPortalButton&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;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&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="kc"&gt;false&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;handleManageBilling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLoading&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="k"&gt;try&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/billing/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="nx"&gt;data&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="k"&gt;if &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;url&lt;/span&gt;&lt;span class="p"&gt;)&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;data&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;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;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;Failed to open billing portal:&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setLoading&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="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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleManageBilling&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&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="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Loading...&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;Manage Billing&lt;/span&gt;&lt;span class="dl"&gt;'&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;/button&lt;/span&gt;&lt;span class="err"&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;h2&gt;
  
  
  Step 4: Handle the Return URL
&lt;/h2&gt;

&lt;p&gt;When a customer finishes in the portal, Stripe redirects them back to your &lt;code&gt;return_url&lt;/code&gt;. Make sure this page exists and ideally refreshes the user's subscription status.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/dashboard/billing/page.tsx&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/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;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;BillingPage&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;subscription&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Billing&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Current&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&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;plan&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;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;:&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;status&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;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BillingPortalButton&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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;h2&gt;
  
  
  Step 5: Sync Subscription Changes via Webhooks
&lt;/h2&gt;

&lt;p&gt;When a customer makes changes in the portal, Stripe fires webhooks. Make sure you're listening to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;customer.subscription.updated&lt;/code&gt; — plan change or cancellation scheduled&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;customer.subscription.deleted&lt;/code&gt; — subscription fully cancelled&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;invoice.payment_succeeded&lt;/code&gt; — successful renewal&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;invoice.payment_failed&lt;/code&gt; — failed payment
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/webhooks/stripe/route.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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="o"&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;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;sig&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="o"&gt;!&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="kd"&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;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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;syncSubscriptionToDatabase&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="k"&gt;break&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleSubscriptionCancelled&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="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;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Stripe customer ID not saved:&lt;/strong&gt; When a user first subscribes, save their &lt;code&gt;stripeCustomerId&lt;/code&gt; in your database immediately. Without it, you can't create portal sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Multiple customers for the same email:&lt;/strong&gt; Use &lt;code&gt;stripe.customers.list({ email })&lt;/code&gt; to check if a customer already exists before creating a new one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Portal not configured:&lt;/strong&gt; If you get a "Customer portal not enabled" error, make sure you've activated the portal in your Stripe Dashboard settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Webhook signature verification failing:&lt;/strong&gt; Always use the raw request body (not parsed JSON) for webhook signature verification.&lt;/p&gt;

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

&lt;p&gt;The Stripe Customer Portal is one of the fastest wins you can add to a SaaS product. In under an hour, you give your users full control over their billing without building a single billing UI component from scratch.&lt;/p&gt;

&lt;p&gt;If you're starting a new SaaS project and want all of this — Stripe billing, customer portal, webhooks, and subscription management — already wired up and production-ready, check out &lt;a href="https://kostra.io" rel="noopener noreferrer"&gt;Kostra&lt;/a&gt;, a Next.js SaaS boilerplate that includes complete Stripe integration out of the box.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://kostra.io/blog/nextjs-stripe-customer-portal" rel="noopener noreferrer"&gt;Kostra blog&lt;/a&gt; — a production-ready Next.js SaaS boilerplate.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>stripe</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
    <item>
      <title>How I built a SaaS in 48 hours</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Mon, 04 May 2026 12:39:53 +0000</pubDate>
      <link>https://dev.to/offpage_prince/how-i-built-a-saas-in-48-hours-25jd</link>
      <guid>https://dev.to/offpage_prince/how-i-built-a-saas-in-48-hours-25jd</guid>
      <description>&lt;p&gt;Last to last weekend i had an idea for a SaaS popped into my head. Pretty straightforward: a service that helps developers write better documentation, giving readability scores and improving the content.&lt;/p&gt;

&lt;p&gt;Pretty basic concept. Clear problem. There is market demand.&lt;/p&gt;

&lt;p&gt;Ordinarily, the first couple weeks would go into just setting things up. This time I took a different route. I leveraged the power of Kostra a production-ready Next.js SaaS boilerplate and had everything live within 48 hours.&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%2Fu9tzw30lo0tf5r7zjbm3.png" 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%2Fu9tzw30lo0tf5r7zjbm3.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's how that went down:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hour 0: The turning point&lt;/strong&gt;&lt;br&gt;
All the SaaSes I've developed over the years followed the exact same pattern: excitement on day one, followed by weeks wasted handling Stripe, authentication, and initial configuration with no real product development happening.&lt;/p&gt;

&lt;p&gt;With this project, I decided not to reinvent the wheel. For some time already I have been following Kostra a Next.js template with authentication, Stripe payments, file uploads, email capabilities, admin panel and even a blog all prebuilt and ready to use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hours 1-2: Initial Setup&lt;/strong&gt;&lt;br&gt;
Fork the project. Create environment variables. Connect Stripe and the database. Deploy on Vercel.&lt;/p&gt;

&lt;p&gt;Completed.&lt;/p&gt;

&lt;p&gt;In two hours, I achieved:&lt;/p&gt;

&lt;p&gt;OAuth &amp;amp; password authentication&lt;br&gt;
Stripe subscriptions setup&lt;br&gt;
Admin dashboard live&lt;br&gt;
Resend for email&lt;br&gt;
Production URL&lt;/p&gt;

&lt;p&gt;No need for debugging any authentication flow. No need to struggle with Stripe documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hours 3 to 8: Building the product&lt;/strong&gt;&lt;br&gt;
After taking care of the foundations, I started working on the actual product.&lt;/p&gt;

&lt;p&gt;It required three main components:&lt;/p&gt;

&lt;p&gt;Input text UI&lt;br&gt;
Built using Kostra's atomic component library within less than an hour.&lt;br&gt;
API endpoint for analyzing text&lt;br&gt;
This was fairly easy due to Kostra’s repo/service architecture; one single-file service was sufficient.&lt;br&gt;
Results UI&lt;br&gt;
Scores, suggestions, and rephrased text. Again, this was built quickly using ready-made components.&lt;/p&gt;

&lt;p&gt;In less than eight hours, I had the product up and running. Text input &amp;gt; analysis results – that's it!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hours 9 to 16: Billing&lt;/strong&gt;&lt;br&gt;
Here's where many projects fail to deliver on time. Billing via Stripe might take days.&lt;/p&gt;

&lt;p&gt;Given that Kostra has Stripe integration out-of-the-box, I only needed to:&lt;/p&gt;

&lt;p&gt;Create two tiers of subscriptions: free plan (five uses per month) and pro plan ($19 monthly).&lt;br&gt;
Use the credit system of Kostra for tracking usage.&lt;br&gt;
Gate the API endpoint with usage limits.&lt;br&gt;
Set up a pricing page with Stripe Checkout.&lt;/p&gt;

&lt;p&gt;No need to configure the billing dashboard from scratch.&lt;/p&gt;

&lt;p&gt;Total effort: six hours instead of several days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hours 17-24: Auth customizations, user setup, email&lt;/strong&gt;&lt;br&gt;
The auth part was done, so I just worked on:&lt;/p&gt;

&lt;p&gt;Onboarding form: Added use case and team size field&lt;br&gt;
Welcome email: Duplicated one of the templates and customized it (45 min)&lt;br&gt;
Limits on usage: Automated through credits system&lt;/p&gt;

&lt;p&gt;Nothing too strenuous here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hours 25-36: Polishing&lt;/strong&gt;&lt;br&gt;
This is when you get to do things that make it feel like an actual product:&lt;/p&gt;

&lt;p&gt;Error states for API requests&lt;br&gt;
Loading states&lt;br&gt;
Empty state for new accounts&lt;br&gt;
Mobile responsiveness (this is mostly done by now)&lt;br&gt;
Landing page (very simple)&lt;/p&gt;

&lt;p&gt;Used the in-built blog for the launch post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hours 37-48: Testing &amp;amp; Launch&lt;/strong&gt;&lt;br&gt;
Kostra has jests and CI/CD included. I tested the following:&lt;/p&gt;

&lt;p&gt;Text processing&lt;br&gt;
Subscription gates&lt;/p&gt;

&lt;p&gt;Tested everything and all passed.&lt;/p&gt;

&lt;p&gt;After that, did a soft launch into a few communities.&lt;/p&gt;

&lt;p&gt;In those first 48 hours, I had:&lt;/p&gt;

&lt;p&gt;Live SaaS on a real domain&lt;br&gt;
3 subscribers ($19/mo)&lt;br&gt;
17 free users&lt;br&gt;
No infra problems&lt;/p&gt;

&lt;p&gt;Here’s what 48 hours really looked like:&lt;/p&gt;

&lt;p&gt;Hours 1-2: Initial Setup&lt;br&gt;
Hours 3-8: Core Product Development&lt;br&gt;
Hours 9-16: Billing Integration&lt;br&gt;
Hours 17-24: User Onboarding + Email Integration&lt;br&gt;
Hours 25-36: Finalization&lt;br&gt;
Hours 37-48: Testing + Launch&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What didn’t I have to build&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OAuth/Authentication, one time password generation, JWT auth etc.&lt;br&gt;
Stripe integration checkout, subscriptions&lt;br&gt;
Billing portal&lt;br&gt;
Credit/usage system&lt;br&gt;
Admin portal&lt;br&gt;
Email system&lt;br&gt;
Files upload capability&lt;/p&gt;

&lt;p&gt;Each of these tasks takes several days to complete. In total, this is many weeks of effort that I didn’t have to spend.&lt;br&gt;
Trade-offs&lt;/p&gt;

&lt;p&gt;Kostra has a cost $150 upfront.&lt;br&gt;
It’s opinionated, too. You’re building on its patterns (services, components, architecture). This is usually helpful, but sometimes constraining.&lt;/p&gt;

&lt;p&gt;If you want to build an entirely custom architecture, you may find this frustrating.&lt;br&gt;
However, if you’re building the typical SaaS application (authentication, billing, users, email), it’s an excellent trade-off.&lt;br&gt;
Would I use it again?&lt;br&gt;
Absolutely.&lt;/p&gt;

&lt;p&gt;I’ve probably saved three weeks at minimum. Even more importantly, it allowed me to focus on the real application and not worry about recreating infrastructure that I’ve built many times before.&lt;br&gt;
If you’re trying to get something done quickly, rather than reinventing the wheel, Kostra makes sense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Would I Use It Again?&lt;/strong&gt;&lt;br&gt;
Yes. Without hesitation.&lt;br&gt;
The production-ready Next.js boilerplate saved me at minimum three weeks of setup work. At my hourly rate that is worth far more than $150. But more importantly, it kept me focused on the problem I was actually trying to solve instead of infrastructure I have solved before.&lt;br&gt;
If you are building a SaaS product and want to skip straight to the product, Kostra is the nextjs saas starter I would recommend. It is the foundation I wish I had on every project I built before this one.&lt;br&gt;
You can check out kostra at : &lt;a href="https://kostra.io/" rel="noopener noreferrer"&gt;https://kostra.io/&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>What I Learned Building a Production-Ready Next.js Boilerplate</title>
      <dc:creator>Prince</dc:creator>
      <pubDate>Wed, 29 Apr 2026 11:26:37 +0000</pubDate>
      <link>https://dev.to/offpage_prince/what-i-learned-building-a-production-ready-nextjs-boilerplate-1hcl</link>
      <guid>https://dev.to/offpage_prince/what-i-learned-building-a-production-ready-nextjs-boilerplate-1hcl</guid>
      <description>&lt;p&gt;I have been working on many SaaS products for some years now. In each of them, my workflow was pretty much the same during the first two to four weeks of development – setting up authentication, connecting Stripe, creating an admin dashboard, configuring emails before even thinking about the actual product.&lt;/p&gt;

&lt;p&gt;This is when I realized the need for a production-ready Next.js boilerplate that covers all those boring tasks from the beginning.&lt;br&gt;
Here is what I learned along the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Most Next.js Boilerplates Are Failing&lt;/strong&gt;&lt;br&gt;
Google "nextjs boilerplate" and you'll find a dozen different options. All of them will give you something like a good starting point with TypeScript and TailwindCSS. But that is it.&lt;br&gt;
Sure, it's fine if you are trying something as a personal hobby project. It's not fine if you want to build a real SaaS product.&lt;br&gt;
In my opinion, a proper production-ready Next.js boilerplate needs to tackle problems you haven't faced yet not just the ones you already know.&lt;br&gt;
The True Cost of Creating a SaaS Product from Scratch&lt;br&gt;
Before building Kostra, I tracked how much time I needed to do some common setups for each of my SaaS projects:&lt;br&gt;
• Authentication using Google OAuth, email/password, and OTP verification: 3-4 days &lt;br&gt;
• Stripe integration with subscriptions, one-time payments, webhooks, billing portal: 4-5 days &lt;br&gt;
• A file upload system with S3, pre-signed URLs, and private buckets: 2-3 days &lt;br&gt;
• Admin panel with user management and role permissions: 3-4 days &lt;br&gt;
• Email system for sending templates with a flexible provider: 2-3 days &lt;br&gt;
• Blog/CMS with categories, SEO slugs, and Rich Text Editor: 2-3 days &lt;br&gt;
That is about four-five weeks worth of setups before even coding a single line of your product itself. On every new project.&lt;br&gt;
Even if you value your time at $50/hour, that makes almost $10,000 in setups per project. Even reusing some code is not enough to reduce it.&lt;br&gt;
What a True Next.js SaaS Boilerplate Should Have&lt;br&gt;
In order to create a proper Next.js SaaS boilerplate, I wrote down all the necessary features I would expect from a production app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Authentication System&lt;/strong&gt;&lt;br&gt;
A basic auth implementation is always easy. A production-ready auth system requires lots of work. Here are some features that Kostra comes with: &lt;br&gt;
• Support for Google OAuth and email/password by default &lt;br&gt;
• OTP verification for login without using passwords &lt;br&gt;
• JWT-based sessions with secure cookies &lt;br&gt;
• Password reset system with expiring tokens &lt;br&gt;
• Role-based authentication (admin and user routes) &lt;/p&gt;

&lt;p&gt;Most nextjs boilerplate options end their auth implementation after saying "users can sign in". And it isn't enough for a production app.&lt;/p&gt;

&lt;p&gt;**2. Comprehensive Stripe Integration&lt;br&gt;
Creating a checkout page for payments is easy. Integrating with Stripe in a SaaS app requires more. Here is what Kostra comes with for a Stripe integration: &lt;br&gt;
• Subscription management (upgrades/downgrades) &lt;br&gt;
• Payments and one-time payments &lt;br&gt;
• Handling all important events via webhooks &lt;br&gt;
• A billing portal where customers can manage their own subscriptions &lt;br&gt;
• Credits system for usage-based payments &lt;br&gt;
It takes the longest to create a Stripe integration correctly because Stripe offers a wide range of possible solutions and a lot of edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Real File Management System&lt;/strong&gt; &lt;br&gt;
Saving files locally in dev environment is okay. Storing them securely on a cloud service requires some extra steps. Kostra uses Cloudflare R2 and S3 and includes such important things as:&lt;br&gt;
• Pre-signed URLs for secure direct uploads &lt;br&gt;
• Public and private buckets for storing sensitive files &lt;br&gt;
• A drag-and-drop interface for uploading files &lt;br&gt;
• Organization of files based on their purposes &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Proper Architecture&lt;/strong&gt; &lt;br&gt;
This is where many boilerplate saas apps cut off a lot of corners. In Kostra, there are such architectural solutions as:&lt;br&gt;
• Atomic Design with UI components being atoms, molecules, and organisms for easier maintainability &lt;br&gt;
• Repository and Service patterns to separate concerns and easily modify your code &lt;br&gt;
• Factory Patterns to connect external services (email providers, cloud providers) without modifying your code &lt;br&gt;
• Integration with Jest for running integration tests and CI/CD &lt;br&gt;
Poor architecture seems cheap until you realize that your app has reached a point where it starts slowing you down and costing a lot of money for rewriting everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Other Features a SaaS Product Requires&lt;/strong&gt; &lt;br&gt;
In addition to the mentioned ones, a full-featured saas template needs:&lt;br&gt;
• A blog/CMS with SEO-friendly slugs and Rich Text Editor &lt;br&gt;
• Forms for capturing leads and contacting customers &lt;br&gt;
• Sending email campaigns and templates for Resend and AWS SES &lt;br&gt;
• Managing users with filtering, pagination, soft deletion, and onboarding status &lt;br&gt;
• Admin panel with information about all registered users, credits, and subscribed plans &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I Would Do Differently&lt;/strong&gt;&lt;br&gt;
Think about architecture first. I built several features too early which I had to rewrite later because of poor architecture decisions. Thinking about repositories and services first could save me a week or two.&lt;br&gt;
Don't underestimate emails. They seem like a minor feature but when you add templates, handling deliverability issues, using several providers for transactional and marketing messages, it becomes really complex.&lt;br&gt;
Test your code from day one. Configuring Jest and setting up CI/CD from day one looks like an unnecessary overhead but skipping it until you see it as important slows everything down significantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who This Is For&lt;/strong&gt;&lt;br&gt;
Kostra is for developers and product owners who want to launch a SaaS app fast without sacrificing its production-ready setup. For someone who wants to try out Next.js and have freedom in developing a personal project, T3 Stack or create-next-app may be a better option.&lt;br&gt;
You can check it out here: &lt;a href="https://kostra.io/" rel="noopener noreferrer"&gt;https://kostra.io/&lt;/a&gt;&lt;/p&gt;

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