Vibe Coding Codex Β· CH 13

Chapter 13: The Complete Stripe Integration Guide for Vibe Coders

⏱️ 4 min read·Updated Jul 2026

Payments: The Most Unforgiving Part of Your Stack

Every other bug in your app is recoverable. A 500 error on your dashboard? Log it, fix it, nobody loses money. A bug in your payment processing? Users are charged without receiving the product. Or not charged when they should be. Or subscriptions are activated without payment.

Stripe is forgiving of bad code in most scenarios. But there are specific integration patterns where mistakes have real financial consequences. This chapter gives you the production-safe patterns for every scenario.

---

The Four Stripe Integration Patterns

Pattern 1: One-Time Payment (Checkout)

The simplest integration. User pays once, gets access to something.

javascript
// /api/checkout/route.js
import Stripe from 'stripe';
import { createServerClient } from '@supabase/ssr';

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

export async function POST(request) {
  const supabase = createServerClient(/* ... */);
  const { data: { user } } = await supabase.auth.getUser();
  
  if (!user) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [{
      price: process.env.STRIPE_PRICE_ID, // Your price ID from Stripe Dashboard
      quantity: 1,
    }],
    mode: 'payment',
    success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
    metadata: {
      user_id: user.id, // Critical: needed in webhook to identify the purchaser
    },
  });

  return Response.json({ url: session.url });
}

The critical detail: Always include metadata.user_id. Without it, your webhook cannot know which user to credit.

Pattern 2: Subscription (Recurring Billing)

For monthly/annual plans. More complex but same core principle.

javascript
// Key difference: mode is 'subscription' not 'payment'
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: priceId, quantity: 1 }],
  success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard`,
  cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
  client_reference_id: user.id, // Alternative to metadata for subscriptions
  customer_email: user.email,   // Pre-fills email in Checkout
});

Pattern 3: The Webhook Handler (Most Critical)

This is where most vibe coders make mistakes. The webhook is the only source of truth for payment status. Never grant access based on a successful redirect URL β€” those are manipulable. Only grant access after your webhook confirms the payment.

```javascript // /api/webhooks/stripe/route.js import Stripe from 'stripe';

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

export async function POST(request) { const body = await request.text(); // Must be raw text, not parsed JSON const signature = request.headers.get('stripe-signature');

let event; try { event = stripe.webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET // From Stripe Dashboard or CLI ); } catch (err) { console.error('Webhook signature verification failed:', err.message); return Response.json({ error: 'Invalid signature' }, { status: 400 }); }

switch (event.type) { case 'checkout.session.completed': { const session = event.data.object; const userId = session.metadata?.user_id || session.client_reference_id; // ALWAYS check payment_status before granting access if (session.payment_status === 'paid') { await grantAccess(userId, session); } break; } case 'customer.subscription.deleted': { const subscription = event.data.object; await revokeAccess(subscription.id); break; } case 'invoice.payment_failed': { const invoice = event.data.object; await handlePaymentFailure(invoice.subscription); break; } }

return Response.json({ received: true }); }

code
**The key pattern:** Always use `constructEvent()` for signature verification. Never skip this. It prevents attackers from sending fake webhook events to your endpoint.

### Pattern 4: Customer Portal (Subscription Management)

Let users manage their own subscriptions (cancel, change plan, update payment method) without you building this UI:
javascript // /api/customer-portal/route.js export async function POST(request) { // Get the user's Stripe customer ID from your database const { data: profile } = await supabase .from('profiles') .select('stripe_customer_id') .eq('id', user.id) .single();

const portalSession = await stripe.billingPortal.sessions.create({ customer: profile.stripe_customer_id, return_url: ${process.env.NEXT_PUBLIC_BASE_URL}/dashboard, });

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

code
---

## Local Testing With Stripe CLI
bash

Install Stripe CLI, then:

stripe listen --forward-to localhost:3000/api/webhooks/stripe

In another terminal, trigger test events:

stripe trigger checkout.session.completed stripe trigger customer.subscription.deleted
code
The CLI outputs a webhook signing secret for local use. Add it to `.env.local` as `STRIPE_WEBHOOK_SECRET`.

---

## The Idempotency Problem

Stripe will retry webhooks if your endpoint returns an error. This means your event handler might be called 2-3 times for the same event. Your handlers must be **idempotent** β€” running them multiple times produces the same result as running them once.
javascript // BAD β€” running twice adds credits twice await supabase.rpc('add_credits', { user_id: userId, amount: 50 });

// GOOD β€” use upsert with the Stripe session ID as a unique key await supabase .from('credit_transactions') .upsert( { stripe_session_id: sessionId, user_id: userId, amount: 50 }, { onConflict: 'stripe_session_id' } // Prevents duplicate inserts ); ```

Always store the Stripe event ID or session ID as a unique key in your database. If the same event arrives twice, the second upsert is a no-op.

Finished reading this chapter?

Mark it completed to update your AI Builder skill profile score on your dashboard.

πŸ“‹

Download Printable Chapter Checklist

Get a print-friendly, formatted checklist of workspace actions to apply this chapter offline.