Blog

How I Implemented Stripe Subscriptions and Credit Top-Ups in Next.js 16

A production pattern for combining Stripe subscriptions, one-time credit purchases, idempotent webhook fulfillment, and Supabase-backed access control.

How I Implemented Stripe Subscriptions and Credit Top-Ups in Next.js 16
1 min read2026-04-28
By Published Updated

How I Implemented Stripe Subscriptions and Credit Top-Ups in Next.js 16

Yes: combine Stripe Checkout modes with strict metadata, verify webhook signatures, and make fulfillment idempotent in your database. In this codebase, subscriptions unlock dashboard access, while one-time payments purchase WhatsApp credits. A service-role Supabase client performs secure, server-only billing writes and updates.

Stripe Billing Flow

Why this pattern works

  • Subscription checkout and top-up checkout run through separate modes (subscription vs payment), but share tenant metadata.
  • Webhook fulfillment uses signature verification plus database idempotency keys.
  • Middleware reads subscription state to gate protected dashboard routes.

Core checkout creation pattern

const session = await stripe.checkout.sessions.create({
  customer: customerId,
  mode: "subscription",
  line_items: [{ price: basePriceId, quantity: 1 }],
  metadata: {
    salonId: salon.id,
    planType,
  },
  success_url: `${baseUrl}/dashboard/settings/billing?success=true&session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${baseUrl}/dashboard/settings/billing?canceled=true`,
});
const session = await stripe.checkout.sessions.create({
  customer: customerId,
  mode: "payment",
  line_items: [
    {
      price_data: {
        currency: "aed",
        product_data: { name: `${credits} WhatsApp Credits` },
        unit_amount: credits * CREDIT_PRICE_FILS,
      },
      quantity: 1,
    },
  ],
  metadata: { salonId: profile.salon_id, type: "credits", amount: credits.toString() },
});

Webhook idempotency guard

const idempotencyKey = `Stripe session ${session.id}`;
const { data: existing } = await serviceSupabase
  .from("credit_transactions")
  .select("id")
  .eq("salon_id", salonId)
  .eq("description", idempotencyKey)
  .limit(1);

if (!existing || existing.length === 0) {
  // fulfill once: update credits and write transaction
}

Practical lesson

If billing affects access control, treat checkout success and webhook completion as two related but separate events. This codebase uses webhook truth for final state and a post-checkout sync helper for immediate UX continuity.

Read the full implementation context in the case study: /case-studies/cooard-salon-platform

Related reading

Why We Used App Router Server Components for a Multi-Tenant Salon Platform

How route groups, server-side data orchestration, and middleware produced predictable role-aware UX at scale.

Continue reading

How We Used Agentic Workflows and AI Automation Without Risking Core Transactions

A blueprint for using AI in delivery and content automation while keeping booking, billing, and access control deterministic.

Continue reading

How To Design Multi-Tenant RBAC with Supabase RLS and Next.js Middleware

A practical MSIS-aligned model for tenant isolation, role controls, and safe privileged operations in a Next.js + Supabase SaaS.

Continue reading