2 November 2026 · 7 min read · BlitzHash Team · AI & Vibe Coding
Lovable Stripe webhook check: why a /success page is not proof of payment
If your Lovable app unlocks paid features when a customer lands on a /success page, anyone can get your product for free by typing that address into their browser. The fix is a Lovable Stripe webhook: a server-side function that receives a signed event from Stripe, verifies the signature, and only then marks the order or subscription as paid in your database. The success page should only say "thanks"; it should never be the thing that grants access.
This is one of the most common payment gaps we find in AI-built apps, because the happy path looks perfect in a demo. You click Buy, Stripe Checkout opens, you pay with a test card, you land back on the app and the premium badge appears. What the demo never tests is the person who skips the payment and goes straight to the last step. This post gives you a set of original test steps you can run yourself in under an hour, explains what a correct setup looks like in plain English, and shows where our Lovable app security audit fits if you would rather have someone verify it for you.
How payments in a Lovable app usually work
In most Lovable projects that take payments, the pieces are:
- Stripe Checkout or a payment link that collects the card.
- A success URL that Stripe sends the customer back to after paying.
- A Supabase Edge Function (or another server function) that talks to Stripe with your secret key.
- A table such as
subscriptions,ordersor aplancolumn onprofilesthat the app reads to decide what a user can see.
The weak spot is how that table gets updated. If the browser updates it, or if the success page updates it, the customer controls the outcome. If a verified webhook updates it, Stripe controls the outcome. Stripe's own fulfillment guide recommends webhooks for exactly this reason: a customer can close the tab before the redirect, and a redirect alone proves nothing.
Four ways a fake /success page leaks revenue
1. The page grants access on load
The success page runs code like "set plan to pro for the current user". Anyone who visits the URL gets pro. Test: sign up a brand-new account, do not pay, and paste the success URL into the address bar.
2. The browser writes the plan column
Even with no success page logic, if your RLS policy lets users update their own profiles row and plan is a column on that row, a user can set it themselves from the browser console using the public anon key. Our earlier post, Lovable security checklist before payments, covers this "users can edit their own role" pattern in more depth.
3. The webhook exists but does not check the signature
A function that accepts any POST that looks like a Stripe event can be called by anyone who finds its URL. Stripe signs every webhook with a Stripe-Signature header and an endpoint signing secret (it starts with whsec_). If your function does not verify that header against the raw request body, it is trusting strangers.
4. The webhook trusts the wrong field
The function verifies the signature but then takes the user ID or plan from something the customer controlled, such as a query string, without tying it to the Checkout Session. The safe pattern is to pass your internal user ID when you create the session (for example as client_reference_id or metadata on the server) and read it back from the verified event.
The 20-minute Lovable Stripe webhook test
Run this on a staging copy or in Stripe test mode. Never test with real cards on production data you cannot undo.
Step 1: The direct-visit test
1. Create a fresh account in your app. 2. Without paying, open the exact success URL your checkout redirects to (copy it from a test purchase). 3. Refresh the app. If any paid feature unlocks, you have gap #1.
Step 2: The self-upgrade test
1. Signed in as that unpaid user, open your browser developer tools. 2. Look in the Network tab for requests to your Supabase project and note which table holds the plan. 3. Ask a technical friend, or our team, to try an update to that column using the same anon key the app uses. If it succeeds, you have gap #2. The fix is a policy or column-level rule so users cannot write billing fields at all; only the server can.
Step 3: The unsigned-event test
1. Find your webhook function's URL in the Supabase Edge Functions list or in Stripe's webhook settings. 2. Send it a POST with a made-up JSON body and no signature (any HTTP tool works). 3. The correct response is an error such as 400, and no database change. If it returns success or changes a row, you have gap #3.
Step 4: The real-event test with the Stripe CLI
1. Install the Stripe CLI and run stripe listen --forward-to . 2. In another terminal run stripe trigger checkout.session.completed. 3. Confirm the function logs a verified event and updates the right row. Then confirm it handles the same event twice without double-granting, because Stripe may deliver an event more than once.
Step 5: The cancellation test
Cancel a test subscription in the Stripe dashboard. Within a few minutes, the user should lose paid access. If they keep it forever, your function is not handling customer.subscription.deleted or the equivalent event for your setup.
Step 6: The failure test
Temporarily break the function (for example, a wrong secret in staging) and make a test purchase. Stripe will show failed deliveries and retry them. Fix the function and confirm the retried event eventually lands and updates the user. This proves you will not lose orders during a short outage.
What a correct setup looks like
In plain English, a safe payment flow in a Lovable app has these properties:
- The Stripe secret key and webhook signing secret live only in server-side secrets, never in the front-end code or a public environment variable.
- The webhook verifies the signature using the raw request body before reading anything.
- The webhook is the only writer of billing fields. Users have no policy that lets them change plan, credits or subscription status.
- Events are processed idempotently: the function records the event ID and ignores repeats.
- The success page only reads the current status and shows a friendly message, possibly with a "your payment is processing" state while the webhook catches up.
- Cancellations, refunds and failed renewals are handled, not just the first payment.
What to ask the AI builder to change
If you are fixing this with Lovable chat, be specific. Prompts that tend to work better than "make payments secure":
- "Move all writes to the subscription or plan fields into the Stripe webhook Edge Function. Remove any client-side code that sets plan."
- "Verify the Stripe-Signature header with the webhook signing secret from server secrets, using the raw body. Return 400 if verification fails."
- "Store processed Stripe event IDs in a table and skip events already processed."
- "Add RLS so authenticated users can read their own subscription row but cannot insert, update or delete it."
Then rerun the six tests above. The AI writing the code is not the same as the code being verified.
Founder takeaways
- A success page is a thank-you note, not a receipt.
- Only a signed, verified webhook should change what a customer has paid for.
- Users should never be able to write billing fields, even their own.
- Test the unpaid visitor, the forged request and the cancelled subscriber, not just the happy path.
- Keep Stripe secrets on the server only. If a secret ever appeared in browser code, rotate it.
- Run the free self-check afterwards to catch other access gaps outside payments.
FAQ
Does Lovable set up Stripe webhooks for me?
Lovable can generate Stripe integration code, usually with Supabase Edge Functions. Whether signature checks, idempotency and cancellation handling are all present depends on the prompts and the version of your project, so run the tests rather than assuming.
Is it enough to check the Checkout Session on the success page?
Retrieving the session on the server from the success page is better than trusting the URL, but customers can close the tab before the redirect. Stripe recommends webhooks so fulfillment happens even when the customer never returns.
What if someone already got paid features for free?
Compare your users with paid status in the database against active subscriptions in Stripe. Fix the gap first, then decide how to handle affected accounts.
---
Payments are where a small gap becomes lost revenue. If you would like a senior engineer to run these checks with you, start with our Lovable app security audit. Book a free technical assessment and we will confirm whether your checkout, webhook and billing fields hold up before real customers find the gaps.
Sources
- Stripe docs: Receive Stripe events in your webhook endpoint
- Stripe docs: Fulfill orders
- Lovable docs: Stripe integration
- Supabase: Row Level Security
