The Revenue Layer: Implementing Stripe Subscriptions in Next.js and MongoDB
Karl Gusta
Instructor & Founder
From Developer to Founder: The Billing Milestone
There is a specific psychological shift that happens when a developer moves from building a project to launching a SaaS. It occurs the moment you add a Buy Now button. Suddenly, your code is not just a collection of logic; it is a value proposition.
Integrating payments is often the most intimidating part of the SaaS journey. You are dealing with real money, tax compliance, and the fear of a user being charged twice or not gaining access after paying. Many developers postpone this step indefinitely, but in the Zero to SaaS methodology, we treat billing as a core pillar of development.
In this lesson, we are going to implement the subscription lifecycle. We will move beyond simple one-time payments and build a recurring revenue system using Stripe Checkout and Webhooks, perfectly synced with your MongoDB user profiles.
The Problem: The Synchronization Nightmare
The biggest technical challenge in SaaS billing is keeping your database in sync with your payment processor. If a user upgrades their plan on Stripe, your database needs to know immediately. If their credit card fails and the subscription is canceled, your app must automatically revoke their access.
Many beginners try to solve this by manually checking the Stripe API every time a user logs in. This is slow, inefficient, and will quickly hit Stripe rate limits. The solution is an event-driven architecture where Stripe tells your app when something happens.

The Shift: Event-Driven Billing
The shift is moving from a pull model to a push model. Instead of asking Stripe if a user is subscribed, we listen for Stripe Webhooks. Stripe acts as the source of truth for payment status, and our MongoDB database acts as a mirror of that truth for fast, local access.
We will use Stripe Checkout to handle the actual payment UI. This is a massive advantage because it handles SCA (Strong Customer Authentication), Apple Pay, Google Pay, and localized currencies out of the box. Your app never touches a credit card number, which significantly reduces your PCI compliance burden.
Stripe Subscriptions in Next.js 2 dives deeper into why this architectural choice is the safest for solo founders.
Deep Dive: The Billing Workflow
1. The Product Catalog Strategy
Before writing code, you must define your products in the Stripe Dashboard. For a standard SaaS, you typically need two Price IDs: a Monthly and a Yearly tier.
The Workflow:
- Create a Product in Stripe (e.g., Pro Plan).
- Create a Price for that product (e.g., $19/month).
- Store these Price IDs in your
.env.localfile. - Never hardcode these IDs in your components; always use environment variables to make switching between Test and Live modes easier.
2. Creating the Checkout Session
When a user clicks Upgrade, you call a Server Action that creates a Stripe Checkout Session. This session is a temporary URL where the user completes their purchase.
The Logic:
You must pass a client_reference_id to the session. This should be the user's MongoDB _id. This is the single most important piece of the puzzle; it is the link that allows you to identify which user just paid when the webhook hits your server later.
typescript// app/actions/stripeActions.ts "use server" import Stripe from "stripe"; import { getServerSession } from "next-auth"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function createCheckoutSession(priceId: string) { const session = await getServerSession(); if (!session) throw new Error("Unauthorized"); const checkoutSession = await stripe.checkout.sessions.create({ mode: "subscription", payment_method_types: ["card"], line_items: [{ price: priceId, quantity: 1 }], success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`, cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`, client_reference_id: session.user.id, customer_email: session.user.email, }); return { url: checkoutSession.url }; }
3. The Webhook Handler: The Source of Truth
A webhook is a dedicated API route in your Next.js app that Stripe calls whenever a subscription is created, updated, or deleted. This route must be public and must verify the Stripe signature to ensure the request actually came from Stripe.
The Workflow:
- Stripe sends a
checkout.session.completedevent. - Your webhook extracts the
client_reference_id(the MongoDB User ID). - Your webhook updates the User document in MongoDB with the
stripeCustomerIdand setsisSubscribed: true. - Your app is now in sync.

4. The Customer Portal
A professional SaaS must allow users to manage their own billing. You should not build a UI for updating credit cards or canceling plans. Instead, use the Stripe Customer Portal. You create a link, the user is redirected to a Stripe-hosted page to manage their subscription, and they are sent back to your dashboard when they are finished.
Key Benefits and Learning Outcomes
- Automated Revenue: Your app makes money while you sleep without manual intervention.
- Professional Trust: Using Stripe Checkout gives your brand instant credibility with users.
- Data Consistency: Webhooks ensure your database always reflects the actual payment state.
- Low Maintenance: Stripe handles tax, receipts, and card expirations automatically.
Common Mistakes to Avoid
- Ignoring the Trial State: If you offer a free trial, ensure your webhook handles the
customer.subscription.updatedevent so you know when the trial ends and the first payment clears. - Local Webhook Testing: Webhooks cannot reach your
localhost. You must use the Stripe CLI to forward events to your local machine during development. - Missing Secret Keys: Forgetting to set the
STRIPE_WEBHOOK_SECRETis the number one cause of webhook 500 errors. - Hardcoding Prices: If you hardcode price IDs, you will have a nightmare updating your pricing tiers in the future. Always use a config file or environment variables.

Pro Tips and Best Practices
- The Grace Period: When a subscription is canceled, do not revoke access immediately. Check the
period_enddate from Stripe and allow the user to keep access until the end of their current billing cycle. - Stripe Tax: Enable Stripe Tax in your dashboard. It automatically calculates and collects sales tax based on the user's location, saving you a massive legal headache later.
- Idempotency: Stripe might send the same webhook twice. Ensure your webhook logic is idempotent (meaning running it twice has the same effect as running it once) to avoid duplicate data entries.
- Tiered Access: Use a helper function in your Next.js layouts to check the user's plan. This allows you to easily hide or show features based on whether the user is on a Free, Pro, or Enterprise tier.
How This Fits Into the Zero to SaaS Journey
We have built the foundation, secured the gates, and established the database. Now, we have connected the revenue engine. In the Zero to SaaS journey, this is the point of no return. You are no longer just building an app; you are building a commercial product.
The integration you build today is what transforms your technical skills into a business asset. From here, every feature you add increases the value of the subscription you have just enabled.
Real-World Use Case: The 'Premium Template' Library
Imagine a SaaS that provides high-quality landing page templates.
- Free Tier: Users can browse templates and download one per month.
- Pro Tier: Users click Subscribe, pay via Stripe Checkout, and the webhook updates their MongoDB profile to
plan: "pro". - The Result: The download button in the Next.js frontend now checks the user's plan field in MongoDB. If it says Pro, the download starts instantly. If not, they are redirected back to the pricing page.
This logic is simple to implement once the Stripe-to-MongoDB bridge is built.

Action Plan: What to Build Next
It is time to get paid. Follow these steps to integrate Stripe:
- Setup: Create a Stripe account and toggle on Test Mode.
- Products: Create one subscription product with a monthly price.
- Integrate: Implement the Server Action to redirect a user to Stripe Checkout.
- Listen: Install the Stripe CLI and start listening for webhooks on your local machine.
- Update: Write the webhook handler logic to update your MongoDB user document when a payment succeeds.
Completing these steps is the difference between a developer and a founder.
Launch Your Revenue Engine Today
The transition from $0 to $1 in revenue is the hardest part of any SaaS journey. By implementing this workflow, you have removed the technical barriers to that first dollar. You now have a production-ready billing system that can scale with your ambitions.
If you want the full Stripe webhook boilerplate and a pre-built pricing table that connects directly to your backend, you can find it in our comprehensive curriculum.
Ready to show the world? Follow the Deploy SaaS on Vercel guide to take your application live and start accepting real payments.