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

Building subscription management UI yourself is a week of work. Stripe's Customer Portal gives you upgrade, downgrade, cancellation, and payment method updates — hosted by Stripe, no code required.

What the Portal Gives You

  • Update payment method
  • View billing history and download invoices
  • Upgrade or downgrade plan
  • Cancel subscription (with optional cancellation flow)
  • Pause subscription (if enabled)

Setup

Configure the portal in Stripe Dashboard → Settings → Customer Portal. Set which plans are available for switching, cancellation behavior, etc.

Server: Create Portal Session

app.post('/api/billing/portal', requireAuth, async (req, res) => {
  const user = await db.users.findUnique({
    where: { id: req.userId },
    select: { stripeCustomerId: true },
  });

  if (!user?.stripeCustomerId) {
    return res.status(400).json({ error: 'No billing account found' });
  }

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

  res.json({ url: session.url });
});
Enter fullscreen mode Exit fullscreen mode

Client: Redirect to Portal

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

  async function openBillingPortal() {
    setLoading(true);
    try {
      const { url } = await fetch('/api/billing/portal', {
        method: 'POST',
      }).then(r => r.json());
      window.location.href = url;
    } finally {
      setLoading(false);
    }
  }

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

Handling Portal Events via Webhooks

// Customer cancelled through portal
case 'customer.subscription.deleted':
  await db.users.update({
    where: { stripeCustomerId: event.data.object.customer as string },
    data: { subscriptionStatus: 'cancelled', plan: 'free' },
  });
  break;

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

Cancellation Flow Customization

In the Stripe Dashboard, you can:

  • Require cancellation reason (valuable feedback)
  • Offer pause instead of cancel
  • Offer discount before cancellation
  • Set a cancellation period (cancel at period end vs immediately)

The Customer Portal, webhook handlers, subscription lifecycle, and billing UI are fully implemented in the AI SaaS Starter Kit — zero billing UI to build yourself.

Top comments (0)