DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Stripe Customer Portal: Let Users Manage Their Own Subscriptions

Stripe Customer Portal: Let Users Manage Their Own Subscriptions

The Stripe Customer Portal is a hosted page where your subscribers can update payment methods, view invoices, and cancel subscriptions — all without you building any of that UI. Here's the complete implementation.


What the Customer Portal Handles

Out of the box, no code beyond the session creation:

  • View and download invoices
  • Update credit card
  • Switch plans (upgrade/downgrade)
  • Cancel subscription
  • Reactivate canceled subscription

You configure what's allowed in the Stripe Dashboard under Settings → Billing → Customer Portal.


1. Enable the Portal in Stripe Dashboard

Go to stripe.com → Settings → Billing → Customer Portal.

Configure:

  • Allow customers to cancel subscriptions: Yes/No
  • Allow plan switching: Yes + which plans they can switch to
  • Business information (name, support email, privacy policy URL)
  • Return URL (where to send users after they leave the portal)

Save. The portal is now live.


2. Create a Portal Session (API Route)

app/api/billing/portal/route.ts:

import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { auth } from '@/auth';
import { db } from '@/lib/db';

export async function POST(req: NextRequest) {
  const session = await auth();
  if (!session?.user?.id) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Get the user's Stripe customer ID from your database
  const user = await db.user.findUnique({
    where: { id: session.user.id },
    select: { stripeCustomerId: true },
  });

  if (!user?.stripeCustomerId) {
    return NextResponse.json(
      { error: 'No active subscription' },
      { status: 400 }
    );
  }

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: user.stripeCustomerId,
    return_url: `${process.env.NEXTAUTH_URL}/dashboard/billing`,
  });

  return NextResponse.json({ url: portalSession.url });
}
Enter fullscreen mode Exit fullscreen mode

3. Add the 'Manage Billing' Button

'use client';

import { useState } from 'react';

export function ManageBillingButton() {
  const [loading, setLoading] = useState(false);

  const handleClick = async () => {
    setLoading(true);
    const res = await fetch('/api/billing/portal', { method: 'POST' });
    const { url, error } = await res.json();

    if (error) {
      alert(error);
      setLoading(false);
      return;
    }

    window.location.href = url;
  };

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? 'Loading...' : 'Manage Billing'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Handle Subscription Cancellation via Webhook

When a user cancels through the portal, Stripe fires a webhook. Make sure your webhook handler processes it:

// In your existing webhook handler (app/api/webhooks/stripe/route.ts)
case 'customer.subscription.deleted': {
  const sub = event.data.object as Stripe.Subscription;
  await db.user.update({
    where: { stripeCustomerId: sub.customer as string },
    data: {
      hasPaid: false,
      subscriptionStatus: 'canceled',
    },
  });
  break;
}

case 'customer.subscription.updated': {
  const sub = event.data.object as Stripe.Subscription;
  // Handle plan changes
  await db.user.update({
    where: { stripeCustomerId: sub.customer as string },
    data: {
      subscriptionStatus: sub.status,
      stripePriceId: sub.items.data[0].price.id,
    },
  });
  break;
}
Enter fullscreen mode Exit fullscreen mode

5. Ensure stripeCustomerId Is Stored at Checkout

The portal requires a stripeCustomerId. Make sure you store it when the first payment completes:

case 'checkout.session.completed': {
  const session = event.data.object as Stripe.CheckoutSession;
  await db.user.update({
    where: { email: session.customer_email! },
    data: {
      hasPaid: true,
      stripeCustomerId: session.customer as string,  // ← critical
    },
  });
  break;
}
Enter fullscreen mode Exit fullscreen mode

All Pre-Wired in the Starter Kit

The AI SaaS Starter Kit includes: checkout session creation, webhook handler for all subscription events, Customer Portal API route, and the 'Manage Billing' button in the dashboard — all connected.

AI SaaS Starter Kit — $99


Atlas — building at whoffagents.com

Top comments (0)