afterbuild/ops
Resource · Integrations

Stripe integration in Lovable: the checklist that works.

14 checks, in order. If you can tick every one, your integration will survive Stripe retries, card declines, and a production audit.

By Hyder ShahFounder · Afterbuild LabsLast updated 2026-04-15

TL;DR (55 words)

Move the secret key server-side, verify webhook signatures, make the webhook idempotent with an events table and unique constraint on `event.id`, handle the four core events, add a daily reconciliation cron, and test with `stripe trigger`. Stripe Checkout beats Elements for vibe-coded apps. These 14 checks cover the integration.

By Hyder Shah · Published 2026-04-15 · Updated 2026-04-15

Why this checklist exists

Stripe publishes excellent documentation. It's not the documentation that's missing — it's the implementation discipline. AI builders scaffold a webhook route that works once and breaks on Stripe's first retry. The Stripe benchmark on AI-built integrations documented this plateau directly: repeated prompting converges on broken-but-superficially-working patterns. The checklist below is what we use on every integration fix; ticking every item is the bar for “ready to take money.”

The 14-point checklist

  1. Secret key is server-side only. `process.env.STRIPE_SECRET_KEY` read from a server route. Never imported by a client component. Grep the repo for the key name; it should appear in `/api/` or `/app/api/` paths only.
  2. Publishable key is the only Stripe key in client code.It's public-safe by design; the secret key isn't.
  3. Test mode and live mode keys are separate env vars. Never swap them with a feature flag. Use `STRIPE_SECRET_KEY_TEST` and `STRIPE_SECRET_KEY_LIVE`, or environment-scoped `STRIPE_SECRET_KEY` in Vercel preview vs production.
  4. Checkout is Stripe Checkout, not Elements — at least until a developer is on the team. Checkout is a hosted page; Stripe handles PCI, 3DS, SCA, and input validation for you.
  5. Webhook endpoint verifies the signature. Every Stripe request includes a `Stripe-Signature` header. Verify it with your webhook secret using `stripe.webhooks.constructEvent`. Reject any request where verification fails.
  6. Webhook endpoint is idempotent.Maintain an events table with a unique constraint on `event.id`. On each webhook, insert the event ID first — if it's a duplicate, return 200 and don't re-process. This is the single most important fix.
  7. Handler processes `checkout.session.completed`. On this event, create or update the subscription in your database. Mark the user as paid. Send the welcome email.
  8. Handler processes `invoice.payment_failed`. On this event, notify the user, update subscription state to `past_due`, and gate access after a grace period.
  9. Handler processes `customer.subscription.updated`. On upgrades, downgrades, and plan changes, sync the new state to your database.
  10. Handler processes `customer.subscription.deleted`. On cancellations or end-of-billing-period terminations, mark the subscription as cancelled.
  11. Handler returns 2xx only on success; 5xx on failure.Stripe retries on any non-2xx or timeout. Never return 200 if the event didn't process correctly; return 5xx and let Stripe retry.
  12. Failed webhook events go to a dead-letter log.Sentry, PostHog, or an explicit `failed_webhook_events` table. You need to know which events didn't process.
  13. Daily reconciliation cron.Query Stripe's API for active subscriptions and compare against your database. Any drift is logged and alerted. This is the check that catches the bugs the webhook misses.
  14. `stripe trigger` tests in CI. Use the Stripe CLI to simulate every event the handler processes. Assert your endpoint returns 200 and your database reflects the expected state.

The anatomy of an idempotent webhook handler

The pattern, described in plain English:

  1. Request arrives. Read the raw body and the `Stripe-Signature` header.
  2. Verify the signature. If invalid, return 400.
  3. Parse the event. Extract `event.id` and `event.type`.
  4. Insert `event.id` into the `stripe_events` table with a unique constraint. If insert fails (duplicate), return 200 — you've already processed this event.
  5. Dispatch to the handler for the event type.
  6. Handler runs in a database transaction. Either everything succeeds or nothing does.
  7. On success, return 200. On failure, return 500 so Stripe retries.

This is the shape of the handler. Most AI-generated versions skip steps 2, 4, and 6 — which is why they break.

What to test before going live

ScenarioHow to simulateExpected outcome
Happy-path checkoutTest card 4242 4242 4242 4242Subscription active, welcome email sent
Card declineTest card 4000 0000 0000 0002No subscription, user stays unpaid
3DS challengeTest card 4000 0025 0000 3155Checkout completes after challenge
Webhook retry`stripe trigger` twiceHandler processes once (idempotent)
Subscription upgradeChange plan in dashboardDatabase reflects new plan
CancellationCancel in Stripe dashboardAccess gated at period end
Failed payment retry`stripe trigger invoice.payment_failed`User notified, grace period starts

The most common Lovable Stripe bug

A webhook handler that writes to the database outside a transaction. The event inserts a subscription row, then crashes before updating the user row. Stripe retries. The duplicate subscription row inserts again; the user still isn't updated. By the end of the day there are three subscription rows for one user, all pointing at different states, and the app bills them three times. This is the pattern that caused the $8,000 double-charge incident in the Ledgerlark — fintech MVP rescued from Lovable.

The fix is two lines of code: wrap every handler in a transaction, and return 5xx on any error so Stripe retries cleanly. The 14 checks above cover both.

When to bring in an engineer

If any of the 14 checks above reads as jargon, you're not going to ship a working Stripe integration by prompting. Book our Integration Fix. It's a fixed-fee engagement that closes all 14 checks in 5–10 working days for $1,500–$2,500. Cheaper than a single bad charge.

Related reading

FAQ
Why do Lovable Stripe integrations break?
Three reasons: (1) webhooks aren't idempotent, so Stripe's retries cause double-charges or state drift; (2) signatures aren't verified, so the endpoint accepts spoofed requests; (3) secret keys are sometimes committed to client-side code. The Stripe benchmark on AI-built integrations found these patterns in the majority of AI-generated endpoints.
What's the minimum viable Stripe setup?
Server-side secret key, signed webhook endpoint with an events table and unique constraint on event.id, handlers for checkout.session.completed, invoice.payment_failed, customer.subscription.updated, and customer.subscription.deleted, plus a daily reconciliation cron. Anything less drifts from Stripe as source of truth.
Does Lovable handle webhooks by default?
It scaffolds a webhook route, but the scaffolded version typically returns 200 on receipt without verifying signatures or persisting events. It works once, then breaks as soon as Stripe retries an event — which Stripe does on any 5xx or timeout. The fix is a rewrite of the webhook handler.
How do I test the full Stripe integration?
Use `stripe trigger` to fire each webhook event in a test environment. Test the happy path (checkout → subscription active), the unhappy path (card decline → subscription not created), the retry path (simulate a 500 from your endpoint and verify the retry is handled idempotently), and the upgrade/downgrade/cancellation flows.
What's the most expensive Stripe bug I should avoid?
A double-charge caused by non-idempotent webhook handling. One AI-built app we audited had double-charged 12 pilot customers before anyone noticed. Refunds plus apology plus churn from the affected users totalled about $8,000 in avoidable cost — which is four to five times what fixing the integration would have cost pre-launch.
Should I use Stripe Checkout or Stripe Elements?
For a vibe-coded app, Stripe Checkout — the hosted page — is almost always the right choice. It handles PCI compliance, 3DS, SCA, and input validation for you. Elements gives you more design control but requires more correct integration work. Save Elements for after you have a developer on the team.
How long does a proper Stripe integration take?
A single-integration fix (Stripe only): 3–5 working days for an engineer, $1,500–$2,500 fixed fee. If auth, RLS, and Stripe are all broken (the typical Lovable pattern), include it in the 3–4 week production-readiness pass.
Where can I get the full spec?
Stripe's own documentation on webhook best practices is the gold standard. Our Integration Fix service page lists every check we perform, and the checklist in this article is what we ship. For a spec review of your existing integration, book the free diagnostic.
Next step

Stripe not working as expected?

Book a free 30-minute diagnostic. We'll audit the integration against all 14 checks.

Book free diagnostic →