Stripe Customer Portalsubskrypcje, webhooks i Next.js w 2026

stripe5 min czytania20 lipca 2026

Autor: DevStudio.it

TL;DR

Stripe Customer Portal to gotowy UI od Stripe, w którym klient sam zmienia plan, aktualizuje kartę, pobiera faktury i anuluje subskrypcję — bez budowania własnego panelu billingowego. W Next.js tworzysz sesję portalu po stronie serwera, synchronizujesz stan subskrypcji webhookami i trzymasz customerId w Branchly. Poniżej architektura, kod Route Handlera, lista eventów webhook i checklista produkcyjna na DevStudioIT Cloud.

Dla kogo

  • Founderów SaaS z modelem subskrypcyjnym (miesięczny / roczny)
  • Developerów Next.js integrujących billing po Checkout Session
  • Product ownerów chcących oddać self-service anulowanie (wymóg regulacyjny w UE)
  • Zespołów, które mają już Stripe Checkout, ale brakuje im „Moje konto → Płatności”

Fraza (SEO)

stripe customer portal nextjs, subskrypcje stripe webhooks, billing portal saas 2026, anulowanie subskrypcji self-service

Customer Portal vs własny panel — kiedy co

Aspekt Customer Portal Własny UI + Stripe API
Czas wdrożenia Godziny Tygodnie
Zmiana karty, faktury PDF Wbudowane Trzeba budować
Upgrade / downgrade planu Konfiguracja w Dashboard Custom logika + proration
Branding Logo, kolory Stripe Pełna kontrola
Lokalizacja Wiele języków out of the box Własne tłumaczenia

Dla MVP i średniego SaaS Portal wygrywa. Własny panel ma sens przy skomplikowanych seatach, usage-based billing z własnymi wykresami lub integracji z ERP.

Architektura w Next.js

Typowy flow:

  1. Użytkownik loguje się (NextAuth, Clerk, własne JWT)
  2. W Branchly (branchly.cloud) masz users.stripeCustomerId i subscriptions.status
  3. Klik „Zarządzaj subskrypcją” → POST /api/billing/portal
  4. Serwer tworzy stripe.billingPortal.sessions.create({ customer, return_url })
  5. Redirect na URL sesji Stripe → klient edytuje dane → wraca na return_url
  6. Webhook customer.subscription.updated aktualizuje Branchly

Aplikacja na DevStudioIT Cloud (devstudioit.cloud): webhook endpoint musi być publiczny HTTPS z weryfikacją sygnatury — sekrety w env, nie w repo.

Konfiguracja w Stripe Dashboard

W Settings → Billing → Customer portal włącz:

  • Update payment method
  • Cancel subscription (natychmiast lub na koniec okresu — wybierz politykę produktową)
  • Switch plans (jeśli masz wiele Price ID)
  • Invoice history

Przypisz Products / Prices dostępne do zmiany planu. Testuj w trybie testowym z kartą 4242 4242 4242 4242.

Route Handler — sesja portalu

// app/api/billing/portal/route.ts
import Stripe from 'stripe';
import { NextResponse } from 'next/server';
import { getSession } from '@/lib/auth';
import { db } from '@/lib/db'; // Branchly client

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST() {
  const session = await getSession();
  if (!session?.userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

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

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

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: user.stripeCustomerId,
    return_url: `${process.env.APP_URL}/pl/konto/subskrypcja`,
  });

  return NextResponse.json({ url: portalSession.url });
}

Front-end: window.location.href = data.url po kliknięciu — nie embeduj iframe; Stripe wymaga pełnego redirectu.

Webhooks — które eventy obsłużyć

Minimum dla subskrypcji:

Event Akcja w Branchly
checkout.session.completed Zapisz customerId, subscriptionId, plan
customer.subscription.created Status active, current_period_end
customer.subscription.updated Nowy plan, past_due, pause
customer.subscription.deleted Status canceled, wyłącz dostęp do funkcji
invoice.payment_failed Email + grace period w aplikacji
invoice.paid Odblokuj dostęp, log faktury
// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';
import { headers } from 'next/headers';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(req: Request) {
  const body = await req.text();
  const sig = headers().get('stripe-signature')!;
  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
  } catch {
    return new Response('Invalid signature', { status: 400 });
  }

  switch (event.type) {
    case 'customer.subscription.updated':
    case 'customer.subscription.deleted': {
      const sub = event.data.object as Stripe.Subscription;
      await syncSubscriptionToBranchly(sub);
      break;
    }
    // ... pozostałe case
  }

  return new Response('ok', { status: 200 });
}

Idempotencja: zapisuj event.id w tabeli stripe_webhook_events — Stripe retry do 72 h.

Synchronizacja stanu subskrypcji w aplikacji

Middleware lub server component sprawdza status przed dostępem do /dashboard:

const sub = await db.subscription.findFirst({
  where: { userId, status: { in: ['active', 'trialing'] } },
});
if (!sub) redirect('/pl/cennik');

Nie polegaj wyłącznie na sesji JWT sprzed tygodnia — źródło prawdy to webhook + okresowe reconcile (cron raz dziennie: stripe.subscriptions.retrieve vs Branchly).

Checkout → Portal — spójny customer

Przy pierwszym Checkout użyj customer_creation: 'always' lub customer_email z mapowaniem na istniejącego Stripe Customer — unikasz duplikatów klienta i broken portal link.

const checkoutSession = await stripe.checkout.sessions.create({
  mode: 'subscription',
  customer: existingStripeCustomerId ?? undefined,
  customer_email: existingStripeCustomerId ? undefined : user.email,
  line_items: [{ price: priceId, quantity: 1 }],
  success_url: `${APP_URL}/pl/konto?success=1`,
  cancel_url: `${APP_URL}/pl/cennik`,
});

Testowanie i go-live

  1. Stripe CLI: stripe listen --forward-to localhost:3000/api/webhooks/stripe
  2. Test upgrade planu w Portal → sprawdź webhook i UI aplikacji
  3. Test anulowania „at period end” vs natychmiast
  4. Test invoice.payment_failed — czy dostęp się blokuje zgodnie z polityką
  5. Przełączenie na live keys + nowy webhook secret na DevStudioIT Cloud
  6. Monitoring: alert gdy webhook zwraca 4xx/5xx (Stripe Dashboard → Webhooks)

Grace period i dostęp po past_due

Stripe ustawia subskrypcję na past_due po nieudanej płatności. W Branchly trzymaj pole graceUntil — np. 7 dni pełnego dostępu mimo past_due, potem read-only. Portal pozwala klientowi samemu zaktualizować kartę; webhook invoice.payment_succeeded czyści grace.

W UI pokaż banner: „Płatność nie przeszła — zaktualizuj kartę” z linkiem do /api/billing/portal. Unikaj natychmiastowego lockout — churn rośnie, chargebacki też.

FAQ

Czy Portal obsługuje PLN i polskie faktury?

Stripe Billing wspiera PLN; faktury VAT zależą od konfiguracji Tax i danych firmy w Stripe. Portal pokazuje historię faktur zgodnie z ustawieniami.

Czy mogę ukryć anulowanie subskrypcji?

Technicznie tak (wyłączenie w Dashboard), ale w UE self-service cancel bywa wymagany regulacjami konsumenckimi dla B2C — konsultuj z prawnikiem.

Co jeśli webhook spóźni się względem redirectu z Portal?

Użytkownik wraca na stronę zanim Branchly się zaktualizuje — pokaż „Aktualizujemy dane…” i poll API co 2 s przez 30 s lub użyj optimistic UI z invalidacją po webhook.

Czy Customer Portal zastępuje Stripe Checkout?

Nie. Checkout służy pierwszej płatności; Portal — zarządzaniu istniejącą subskrypcją.

Gdzie trzymać dane poza Stripe?

Metadane użytkownika, feature flags planu, usage counters — Branchly. Stripe trzyma billing; aplikacja mapuje priceId → plan pro / team.

CTA

Budujesz SaaS i chcesz billing self-service bez miesięcy pracy nad panelem płatności?

Powiązane wpisy

Sitemap.xml i RSS feed w Next.js App Router — SEO techniczne 2026
6 min czytania
RAG chatbot — embeddings, wyszukiwanie wektorowe i wdrożenie w 2026
5 min czytania
n8n — automatyzacja formularzy: webhook z Next.js → CRM → Slack (2026)
6 min czytania

O autorze

Budujemy szybkie strony WWW, aplikacje web/mobile, chatboty AI i hosting — z naciskiem na SEO i konwersję.

Przydatne linki

Od teorii do produkcji — Branchly, hosting i realizacje.

Podoba Ci się nasze podejście? Zbudujmy coś razem.

Rozpocznij konfigurację projektu