---
slug: add-stripe-to-vibe-coded-app
title: "Adding Stripe to a Vibe-Coded App Without Breaking It"
excerpt: "Adding Stripe to a Lovable or Cursor app is where most vibe-coded projects break. Here's what the AI silently gets wrong and the spec rules that prevent it."
primaryKeyword: "add Stripe to Lovable app"
publishedAt: 2026-05-02
readingTimeMin: 7
author: "Robert Boylan"
tags:
  - stripe
  - billing
  - indie-dev
  - vibe-coding
  - integrations
---

It's a Sunday afternoon. Someone on Reddit posts that they shipped their first paid app, three customers signed up, and they got the Stripe email saying the payment succeeded. They're celebrating. Twelve hours later they're back, panicked: the customers are emailing because they paid but the app says they're still on the free plan. The money landed. The account didn't update. Nobody told the app the payment happened.

This is the most common way to add Stripe to a Lovable app, a Cursor project, or a Bolt.new prototype, and have it not work. Not because the code is broken. Because the AI generated the half of Stripe that's visible (the checkout form) and quietly skipped the half that isn't (the part where Stripe tells your app what just happened).

If you're a vibe-coder, meaning someone building apps mostly by describing them to an AI tool rather than hand-writing every line, billing is the integration where the gap between "looks right" and "actually works" gets the widest. Here's what the AI silently gets wrong, and the small set of spec rules that prevent most of it.

## Why Stripe is where vibe-coded apps tend to break

Most parts of an app have a fast feedback loop. You ask Cursor or Lovable to add a login form, you click the button, and either it logs you in or it throws an error you can copy back into the prompt. You can iterate in real time.

Stripe is different. The feedback loop runs through Stripe's servers, your server, and a webhook (an HTTP callback that Stripe sends to your app when something changes on their side). When any one of those three is wrong, the symptom is "the user paid but nothing happened on my side", which looks identical whether the bug is in your checkout setup, your webhook handler, or the fact that you're testing with a live key against a test product.

The AI doesn't know what's wrong because the AI can't see the Stripe dashboard, the webhook logs, or the difference between your test and live API keys. It will confidently generate code that works on the surface and silently ignores half the lifecycle events that matter for billing.

That's not the AI's fault. It's a spec problem. If you don't tell it which billing model you want, which events you handle, and what should happen when a payment fails, it will pick a default that's almost right and not flag the gaps.

## Test mode vs live mode is where most weekend builds die

Stripe has two completely separate environments: test mode and live mode. Different API keys, different products, different customers, different webhooks. You can spend your whole weekend wiring it up in test mode, flip the deploy switch, and have nothing work because nobody told the AI to swap the keys, register the live webhook, or create the products in the live dashboard too.

The AI will happily write code that reads from `STRIPE_SECRET_KEY` and call it done. What it won't do, unless you ask, is:

- Tell you that the price ID you hardcoded is a test-mode ID and won't exist in live mode
- Set up a separate webhook endpoint in the live dashboard
- Warn you that the customer portal (Stripe's hosted page where users manage their own subscriptions) needs to be configured separately in each mode
- Verify the webhook signature using the live signing secret, not the test one

The fix is not technical. The fix is to tell the spec which mode you're shipping in, and to write down that every Stripe object (product, price, webhook, customer portal config) has to exist in both. That single line in your project spec turns a class of "but it worked yesterday" bugs into a checklist.

If you're using a tool like Lovable or v0, where you're not directly writing the deploy script, you also want to be explicit that secrets are environment-specific. The AI will otherwise generate code that hardcodes a key into the file and you'll commit it to GitHub. Which leads to the second-most-common Stripe-on-vibe-coded-app failure mode, but that's a different post.

## The webhooks the AI forgets to wire up

A webhook, again, is just an HTTP callback Stripe sends to your app when something changes on their side. Someone pays, Stripe POSTs `checkout.session.completed` to your server. A subscription renews, Stripe POSTs `invoice.payment_succeeded`. A card fails, Stripe POSTs `invoice.payment_failed`. Your app's job is to listen and update its own database accordingly.

When you ask Cursor or Lovable to "add Stripe checkout", you'll almost always get code that handles `checkout.session.completed` and stops there. That's enough to mark a user as paid the first time. It is not enough to run a real subscription business.

Here's the minimum set of events a subscription app actually needs to handle:

- `checkout.session.completed`: the initial signup. Mark the user as paid, store the Stripe customer ID and subscription ID.
- `customer.subscription.updated`: plan changes, tier upgrades, downgrades, anything that changes what the user has access to.
- `customer.subscription.deleted`: cancellations and end-of-billing-period terminations. Downgrade the user.
- `invoice.payment_failed`: a renewal didn't go through. Decide whether to email them, restrict access, or both.
- `invoice.payment_succeeded`: successful renewals. Useful for resetting monthly allowances (credits, usage limits, whatever your product meters).

If you don't handle `customer.subscription.deleted`, paying customers who cancel keep their access forever. If you don't handle `invoice.payment_failed`, free riders accumulate quietly. If you don't handle `customer.subscription.updated`, an upgrade from Starter to Pro doesn't actually grant the Pro features.

None of this shows up as an error. The app keeps running. The customer just has the wrong access level. You usually only find out when one of them emails you.

## Subscription, one-time, or metered: pick before you prompt

Stripe's product model has three rough flavours, and they're not interchangeable:

- **One-time payments.** A user pays once and gets the thing. Easy to wire up. Boring tax-wise. Limited revenue ceiling per customer.
- **Subscriptions.** A user pays on a recurring schedule (monthly, yearly, whatever). This is what most SaaS apps want. Way more lifecycle events to handle.
- **Metered or usage-based billing.** The user pays for what they consume (API calls, AI tokens, image generations). Most complicated to set up; reports usage to Stripe over time and Stripe bills against it.

The AI will guess based on words you used in the prompt. Say "subscription" and it'll generate subscription code. Say "one-off purchase" and you'll get one-time. Don't mention either and you'll get whatever the model defaulted to in its training data, which is usually one-time, because the simplest path is the most common.

A few things worth pinning down in the spec before you let the AI start writing:

- **Use price IDs, not product IDs.** Price IDs (`price_1Abc...`) are what checkout actually charges against. Product IDs (`prod_abc...`) are the parent record. The AI sometimes mixes them up; the symptom is checkout sessions failing to create with cryptic errors.
- **Decide if you'll ever change pricing.** If yes, store the price ID in an environment variable or config file, never inline. Stripe doesn't let you edit a price; you have to create a new one and update every reference.
- **Decide on the customer portal up front.** Stripe's hosted portal lets users manage their own subscription (upgrade, cancel, update card) and is roughly fifteen minutes to enable. Not enabling it means every plan change becomes a support ticket.

This is the kind of decision your AI tool can't make for you, because the answer depends on what you're charging for. But once you've decided, the AI can write the integration cleanly. [Writing acceptance criteria the AI can follow](/blog/acceptance-criteria-for-ai-coders) is the same idea applied to billing: the integration code is mostly fine if the requirements are specific.

## The five spec lines that prevent ninety percent of these issues

Most Stripe-related bugs in vibe-coded apps come from missing context, not missing knowledge. The AI knows how to write Stripe integrations. It doesn't know your specific intent. Five lines in your spec, before you start prompting, prevent most of the common failures:

1. **Billing model:** "Subscription, monthly, two tiers (Starter $9, Pro $29). No one-time payments. No metered usage."
2. **Webhook events to handle:** "checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed, invoice.payment_succeeded. Verify signatures with the webhook signing secret."
3. **What happens on payment failure:** "After 3 failed attempts, the user is downgraded to free tier and emailed a recovery link. They keep their data."
4. **Refund window and policy:** "14-day refund, full amount, processed via Stripe dashboard. Refunds trigger a downgrade via the customer.subscription.deleted webhook."
5. **Tax and currency:** "Stripe Tax enabled. Default currency USD. EU customers see VAT line item; UK customers see UK VAT. No manual tax handling in app code."

Hand those five lines to Cursor, Claude Code, or Lovable along with your normal feature description, and the integration that comes back will already account for the lifecycle events most vibe-coded apps miss. You'll still have to test it, and you'll still have to wire up the test/live mode separation. But the silent-failure surface area drops a lot.

If you don't know what to put in line 1 yet, you probably want to figure out [pricing your indie app before you start billing for it](/blog/pricing-your-indie-app). Stripe will charge whatever you point it at. The decision about what that is sits upstream of the integration.

## When to skip Stripe entirely

Stripe is a payment processor. You are still the merchant of record, which means you handle sales tax across every jurisdiction your customers live in. For an indie founder selling to customers in the US, the EU, the UK, and a handful of other countries, this gets ugly fast. VAT in 27 EU member states. Sales tax in 45 US states. UK VAT post-Brexit. Quarterly filings.

There's a class of services that act as the merchant of record for you, meaning they handle all the tax across all the jurisdictions and just pay out a clean amount to your bank. The three that come up most for indie devs:

- **Lemon Squeezy**: popular with indie devs. Handles tax, generates invoices, has a Stripe-style API.
- **Polar**: newer, focused on developer products and SaaS. Often simpler than Lemon Squeezy for digital goods.
- **Paddle**: older, more enterprise-coded, but battle-tested for SaaS.

The tradeoff is fees. Merchant-of-record services charge more than Stripe (typically 5% + 50¢ vs Stripe's 2.9% + 30¢). For a one-person company doing $50k/year, the extra few hundred dollars in fees are usually worth not spending a weekend per quarter on tax filings.

If you're early, charging in one country, or running on free time you'd rather not give to international tax law, the merchant-of-record route can be the right call. The integration code looks similar to Stripe's. The webhook event names are different but the lifecycle is the same. And you can switch later, if your scale or geography changes.

## So what does this mean for your next billing integration

Here's the honest takeaway: the integration code your AI tool generates is mostly fine. Stripe's API is well-documented, the patterns are stable, and Cursor or Lovable will produce something that works on the happy path. What's missing is almost always the spec.

You haven't decided which webhook events you handle. You haven't decided what happens when a card fails. You haven't decided whether you're handling tax yourself or letting a merchant-of-record do it. The AI fills the gaps with plausible defaults, those defaults work for the first checkout, and you find out two weeks later that paying customers are still on the free plan because nobody handled `customer.subscription.deleted`.

Most vibe-coded apps skip the billing spec because billing feels like an implementation detail. It's not. It's a product decision wearing implementation clothing. That's [exactly the kind of thing Draftlytic captures upfront](/blog/how-to-use-draftlytic), so the spec you hand to your AI tool already has the five lines that prevent the silent failures.

Pin the spec down before you start prompting Stripe code. The integration will write itself.
