DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Stripe Customer Portal: Self-Service Billing Without Building It

Stripe Customer Portal: Self-Service Billing Without Building It

Building subscription management — upgrade, downgrade, cancel, update card — takes weeks. Stripe Customer Portal does all of it in an afternoon.

What the Portal Provides

  • Update payment method
  • View billing history and download invoices
  • Cancel subscription
  • Upgrade/downgrade plans
  • Update billing address

All Stripe-hosted, fully branded to your business.

Setup: One-Time Configuration

// Create portal configuration (run once)
const configuration = await stripe.billingPortal.configurations.create({
  business_profile: {
    headline: 'Manage your Whoff Agents subscription',
    privacy_policy_url: 'https://whoffagents.com/privacy',
    terms_of_service_url: 'https://whoffagents.com/terms',
  },
  features: {
    payment_method_update: { enabled: true },
    subscription_cancel: {
      enabled: true,
      mode: 'at_period_end', // Don't cancel immediately
    },
    subscription_update: {
      enabled: true,
      default_allowed_updates: ['price'],
      proration_behavior: 'create_prorations',
    },
    invoice_history: { enabled: true },
  },
});

// Save configuration.id to env: STRIPE_PORTAL_CONFIG_ID
Enter fullscreen mode Exit fullscreen mode

Create Portal Session

// app/api/billing/portal/route.ts
import { auth } from '@/auth';
import { stripe } from '@/lib/stripe';
import { db } from '@/db';
import { redirect } from 'next/navigation';

export async function POST(request: Request) {
  const session = await auth();
  if (!session) return new Response('Unauthorized', { status: 401 });

  const user = await db.user.findUnique({
    where: { id: session.user.id },
    select: { stripeCustomerId: true },
  });

  if (!user?.stripeCustomerId) {
    return new Response('No Stripe customer found', { status: 400 });
  }

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

  return Response.redirect(portalSession.url);
}
Enter fullscreen mode Exit fullscreen mode

Billing Page Button

// app/dashboard/billing/page.tsx
export default async function BillingPage() {
  const session = await auth();
  const user = await getUser(session.user.id);

  return (
    <div>
      <h1>Billing</h1>
      <p>Current plan: <strong>{user.plan}</strong></p>
      <form action='/api/billing/portal' method='POST'>
        <button type='submit'>Manage Subscription</button>
      </form>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Handle Portal Events via Webhooks

switch (event.type) {
  case 'customer.subscription.updated':
    const sub = event.data.object;
    await db.user.update({
      where: { stripeCustomerId: sub.customer as string },
      data: { plan: sub.items.data[0].price.lookup_key ?? 'free' },
    });
    break;

  case 'customer.subscription.deleted':
    await db.user.update({
      where: { stripeCustomerId: event.data.object.customer as string },
      data: { plan: 'free' },
    });
    break;
}
Enter fullscreen mode Exit fullscreen mode

Stripe Customer Portal ships pre-configured in the AI SaaS Starter Kit — portal API route, billing page, and webhook handlers all included. $99 at whoffagents.com.

Top comments (0)