Stripe for Vibecoders: From First Charge to Subscriptions
intermediateCursorClaude Code

Stripe for Vibecoders: From First Charge to Subscriptions

By rik5 min readApril 30, 2026

Why this matters

The gap between "my AI app works" and "my AI app makes money" is one Stripe Checkout button. Most vibecoders never cross it. They tell themselves payments are a separate, scary project. They aren't. Stripe in 2026 is one route file, one webhook handler, and a customer portal link. You can ship the whole thing in an afternoon.

Stripe for vibecoders is the minimum viable payments stack: hosted Checkout (so you don't touch card data), signed webhooks (so you don't get spoofed), and the customer portal (so users self-serve cancellations and you don't get refund support tickets). That's it.

The setup

You need:

  • A Stripe account in test mode (no review needed).
  • A Next.js (or similar) app deployed somewhere with a real URL — webhooks need a public endpoint.
  • A user system with stable IDs (Supabase auth.uid() works perfectly — see Supabase for vibecoders).
  • 30 minutes.

Step 1: Get the keys + stay in test mode

Stripe Dashboard → Developers → API keys. You get:

  • STRIPE_PUBLISHABLE_KEY — public, safe to ship to the client.
  • STRIPE_SECRET_KEY — server only.
  • Plus a webhook signing secret (created in Step 3).

Keep test mode on until you've actually charged a fake card end-to-end. Stripe's test card 4242 4242 4242 4242 with any future date and any CVC is your friend.

# .env.local
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...   # filled in after step 3

Never ship the secret key in client code. If your env var name does not start with NEXT_PUBLIC_, it stays server-side. Cursor and Lovable sometimes get this wrong — always grep for STRIPE_SECRET in your built JS bundle before going live.

Step 2: Spin up Checkout in 20 lines

Hosted Checkout is the lowest-risk path. Stripe handles the form, the card data never touches your server, and you get Apple Pay / Google Pay / SEPA for free.

// app/api/checkout/route.ts
import Stripe from 'stripe'
import { createClient } from '@/lib/supabase/server'

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

export async function POST() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return new Response('unauthorized', { status: 401 })

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: 'price_xxx', quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/dashboard?checkout=success`,
    cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing`,
    customer_email: user.email,
    client_reference_id: user.id,   // critical for the webhook
  })
  return Response.json({ url: session.url })
}

Client-side, you POST and redirect:

const { url } = await fetch('/api/checkout', { method: 'POST' }).then(r => r.json())
window.location.href = url

Done. You can charge cards.

Step 3: Verify webhooks — never skip this

The single biggest Stripe mistake vibecoders make is taking webhooks at face value. Anyone can POST to your endpoint with fake JSON. The signature check is non-negotiable.

// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe'
import { headers } from 'next/headers'

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

export async function POST(req: Request) {
  const body = await req.text()  // raw body — do NOT JSON.parse before verify
  const sig = (await headers()).get('stripe-signature')!
  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
  } catch (err) {
    return new Response('signature failed', { status: 400 })
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object
    const userId = session.client_reference_id
    // mark user as paid — write to Supabase, etc.
  }
  return new Response('ok')
}

In Stripe Dashboard → Developers → Webhooks, add an endpoint pointing at https://yourdomain.com/api/webhooks/stripe, subscribe to checkout.session.completed, customer.subscription.updated, and customer.subscription.deleted. Copy the signing secret into your env. Test with stripe listen locally.

Step 4: Add the customer portal + subscriptions

Stripe's hosted customer portal lets users update cards, cancel, view invoices — without a single line of UI from you. One route:

const session = await stripe.billingPortal.sessions.create({
  customer: customerId,  // stored from the checkout webhook
  return_url: `${process.env.NEXT_PUBLIC_SITE_URL}/dashboard`,
})
return Response.json({ url: session.url })

Link to this from your dashboard's "Manage subscription" button. Stripe handles dunning, payment retries, proration. You handle nothing.

For pricing pages, define your prices once in the Stripe Dashboard with price_xxx IDs and reference them by ID in code. Don't hardcode amounts in your app — when you change pricing, you only change Stripe.

Common mistakes

  • Skipping signature verification — Anyone can hit your webhook URL. Without verification, an attacker can mark themselves as paid.
  • Parsing the request body before verifying — Stripe needs the raw body. await req.json() first will break the signature check.
  • Storing card data in your DB — Don't. Use Stripe's Customer object and store the customer_id only. PCI compliance is Stripe's problem when you do it this way.
  • Hardcoding prices in code — Move prices to Stripe Dashboard, reference by price_xxx. Future you will thank you.
  • Forgetting client_reference_id — Without it, your webhook can't tie the Stripe session back to your user. Always pass it on Checkout creation.

What's next

Pair Stripe with Vercel for hosting — webhook routes Just Work on Fluid Compute — and Supabase for the user system so the client_reference_id flow is one line. Once charges work end-to-end in test mode, flip to live mode, charge yourself a real $1, refund it, and you're shipped.

What are you building?

Claim your handle and publish your app for the world to see.

Claim your handle →

Related Articles