A broken checkout costs you money every hour it stays broken. Lovable apps also tend to break in ways that look fine on the surface: the button renders, but nothing happens. Here's how the pieces fit together and how to test each one.
How Stripe is wired in a typical Lovable app
Your frontend never talks to Stripe with a secret key. Instead:
- Your app calls a backend function (often named something like
create-checkout) running on Lovable Cloud or your Supabase project. - That function creates a Checkout Session with your Stripe secret key and returns a URL.
- The user pays on Stripe's hosted page and is redirected back to your success URL.
- Optionally, a webhook function (e.g.
stripe-webhook) receives the "payment succeeded" event and updates your database.
Lovable's docs note that by default its Stripe integration doesn't use webhooks; your app checks status directly with Stripe. If you added webhooks, step 4 is a fourth thing that can break.
Symptom 1: clicking "Buy" does nothing
- Popup blocker. Lovable's docs say checkout usually opens in a new tab, which popup blockers can stop. Allow popups and try again.
- Testing inside the Lovable preview. Test on your published URL in a normal browser tab. The customer portal in particular doesn't work inside the preview panel.
- A JavaScript error. Open DevTools → Console and click the button. A red error here, such as a failed fetch to the function, tells you where to look next.
Symptom 2: checkout fails with an error
The error is coming from your checkout function. Read the details in two places:
- Function logs: the Cloud view on Lovable Cloud projects, or Supabase Dashboard → Edge Functions → Logs.
- Stripe Dashboard → Developers → Logs: every API request Stripe received, with Stripe's own error message.
Common culprits:
- the secret key is missing from function secrets
- a test-mode key is paired with a live-mode price ID, or the other way round
- a price ID was deleted or archived
Paste the exact error into Lovable and ask it to fix the function.
Symptom 3: payment succeeds but the app still says "free plan"
This is almost always the webhook.
- No endpoint. In Stripe → Developers → Webhooks, add an endpoint pointing to your function URL. Subscribe it to the events your code handles, e.g.
checkout.session.completedand, for subscriptions, thecustomer.subscription.*events. - 401 errors on delivery. Supabase Edge Functions check for a user JWT by default, and Stripe doesn't send one. Disable JWT verification only for the webhook function, e.g.
[functions.stripe-webhook] verify_jwt = falseinsupabase/config.toml. Putting it in config means a redeploy won't quietly switch it back on. - 400 "No signatures found matching…" The signing secret doesn't match. Each endpoint has its own
whsec_secret, test and live mode are separate, and the Stripe CLI's secret is different again. Also verify against the raw request body (await req.text()) usingconstructEventAsyncon Deno.
After fixing, click Resend on a failed delivery in Stripe and confirm it now returns 200.
Symptom 4: wrong page after payment
Check the success_url and cancel_url in your checkout function. If they still point at a preview domain or localhost, real customers end up on a dead page right after paying.
Symptom 5: it worked last week
A regenerated function, a rotated key, or a switch to live mode can each break things silently. That's exactly why a quiet weekly check of your pricing page and checkout button is worth having.
How FlowCanary's free check detects this
FlowCanary's free scan can't make a payment, and it never will. It does catch the surface signs of a broken checkout:
/pricingreturning an error or rendering blank- JavaScript console errors on page load
- failed network requests on your homepage and key pages
- broken links on your homepage
You get a paste-ready fix prompt for each issue. Our upcoming daily monitoring will walk your checkout flow up to the Stripe page on a schedule and alert you when it breaks.