๐Ÿ’ณ Stripe Subscriptions in Action: Building Recurring Revenue

In previous chapters, we learned how to implement a "One-time Payment" checkout flow.
But if you're building a SaaS platform or a paid membership community, Monthly Recurring Revenue (MRR) is the key to financial freedom!

This chapter will guide you through the complete setup of Stripe Subscriptions, including creating subscription plans, initiating Checkout Sessions, and most importantly: how to listen for successful automatic monthly payments via Webhooks.


1. What are Stripe Subscriptions?

The key difference between Stripe Subscriptions and one-time payments lies in their "recurring" nature.

  • One-time Payments (Payment Intents): Charge once, done.
  • Subscriptions: After customers bind their credit cards, Stripe automatically charges them on the same day each month until they actively cancel.

Core Components of Subscriptions:

  1. Product: e.g., "Vibe Tutor Premium Membership".
  2. Price: Not just a number, but an object with a "Recurring" attribute, e.g., "$299/month".
  3. Customer: Subscriptions require a Customer object to bind credit cards and contact information.
  4. Subscription: The contract binding a Customer to a Price.

2. Create Subscription Plans in the Stripe Dashboard

Before writing code, the easiest way is to create products and prices directly in the Stripe Dashboard.

  1. Log in to the Stripe Dashboard, click Products -> Add Product.
  2. Enter the Name: Vibe Tutor Premium Membership.
  3. Select Pricing model: Standard pricing.
  4. Enter Price: 299.
  5. Select Billing period: Monthly.
  6. Click Save product.

After saving, a Price ID starting with price_ (e.g., price_1Nxyz...) will appear in the price list.
Copy this Price IDโ€”itโ€™s the most important key for our code!


3. Initiate a Subscription Checkout Session

With the Price ID, weโ€™ll create an API in Next.js to generate a checkout URL.
This step is very similar to one-time payments, but with two key differences:

  1. mode must be set to 'subscription'.
  2. We must pass customer_email or create a customer, as subscriptions need to know who is paying.

Writing the API Route

Create the file: src/app/api/stripe/create-subscription/route.ts

import { NextResponse } from 'next/server';
import Stripe from 'stripe';

// Initialize Stripe (remember to store the Secret Key in .env.local)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const { email, userId } = body;

    // 1. Validate parameters
    if (!email || !userId) {
      return NextResponse.json({ error: 'Missing email or userId' }, { status: 400 });
    }

    // 2. Create Checkout Session
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      mode: 'subscription', // Key 1: This is a subscription!
      customer_email: email, // Key 2: Email is required to bind the customer
      line_items: [
        {
          // Paste the Price ID copied earlier here
          price: 'price_1NxyzYOUR_PRICE_ID_HERE', 
          quantity: 1,
        },
      ],
      // Redirect URLs for success and cancellation
      success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/payment-success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
      // Key 3: Pass our database User ID into metadata so the Webhook knows who paid
      subscription_data: {
        metadata: {
          vibe_user_id: userId,
        },
      },
    });

    // Return the generated checkout URL
    return NextResponse.json({ url: session.url });
    
  } catch (error: any) {
    console.error('Stripe Subscription Error:', error);
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

4. Listen for Subscription Status via Webhook

After a customer successfully completes checkout, Stripe will automatically charge them each month.
We need a Webhook to receive Stripe notifications and update VIP status in our database.

For subscriptions, the three most critical events to listen for are:

  1. checkout.session.completed: The customerโ€™s first successful payment and card binding.
  2. invoice.payment_succeeded: Successful automatic monthly charges! (The essence of MRR).
  3. customer.subscription.deleted: The customer cancels or their credit card fails permanently.

Implementing the Webhook Logic

Create/modify the file: src/app/api/stripe/webhook/route.ts

import { NextResponse } from 'next/server';
import Stripe from 'stripe';
import { headers } from 'next/headers';
// Assume you have your own database update logic
import { updateUserSubscription } from '@/lib/db'; 

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(request: Request) {
  const body = await request.text();
  const sig = headers().get('stripe-signature');

  let event: Stripe.Event;

  try {
    // Verify this is a genuine Stripe call, not a hacker forgery
    event = stripe.webhooks.constructEvent(body, sig!, endpointSecret);
  } catch (err: any) {
    return NextResponse.json({ error: `Webhook Error: ${err.message}` }, { status: 400 });
  }

  // Handle different events
  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      if (session.mode === 'subscription') {
        // First subscription success
        const userId = session.subscription_data?.metadata?.vibe_user_id;
        const subscriptionId = session.subscription as string;
        
        console.log(`User ${userId} started subscription ${subscriptionId}`);
        // TODO: Save subscriptionId to the database and grant VIP access
        await updateUserSubscription(userId, 'active', subscriptionId);
      }
      break;
    }
      
    case 'invoice.payment_succeeded': {
      const invoice = event.data.object as Stripe.Invoice;
      if (invoice.subscription) {
        // Monthly payment succeeded! Renewal!
        const subscriptionId = invoice.subscription as string;
        console.log(`Payment succeeded for subscription ${subscriptionId}`);
        // TODO: Update the subscription expiry date in the database (extend by one month)
      }
      break;
    }
      
    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription;
      const subscriptionId = subscription.id;
      
      console.log(`Subscription ${subscriptionId} was canceled.`);
      // TODO: Revoke the userโ€™s VIP access
      break;
    }
      
    default:
      console.log(`Unhandled event type ${event.type}`);
  }

  return NextResponse.json({ received: true });
}

5. Let Customers Manage Their Subscriptions (Customer Portal)

The biggest headache with subscriptions is "support overload": daily emails like "I need to update my card," "I want to cancel," or "I need an invoice."
One of Stripeโ€™s most generous features is its built-in, completely free Customer Portal.

With just one line of code, you can generate a dedicated URL where customers can:

  • Update credit card details
  • Pause or cancel subscriptions
  • Download past receipts and invoices

Generate a Customer Portal URL

// src/app/api/stripe/portal/route.ts
import { NextResponse } from 'next/server';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2023-10-16',
});

export async function POST(request: Request) {
  try {
    const { customerId } = await request.json(); // Stripe Customer ID from your database

    const session = await stripe.billingPortal.sessions.create({
      customer: customerId,
      return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard`, // Where to redirect after management
    });

    return NextResponse.json({ url: session.url });
  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

All you need is a frontend button like <button onClick={handleManageBilling}>Manage My Subscription</button> to call this API, get the URL, and redirect. Your support costs drop to zero!

This is the beauty of SaaS platforms: write the code once, and the system works like a tireless robot, automatically collecting payments, issuing invoices, and handling cancellations every month. Ready to welcome your first MRR? ๐Ÿš€

Unlock Full Tutorial

This chapter is paid content. Join the project to unlock over 5000 words of deep analysis, including 10+ god-tier Prompts and real Source Code examples!