cd /news/ai-tools/stripe-in-a-lovable-or-bolt-app-the-… · home › topics › ai-tools › article
[ARTICLE · art-160617] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=· neutral

Stripe in a Lovable or Bolt app: the success page isn't proof of payment

A developer has documented a common flaw in AI-built Stripe integrations: apps generated by tools like Lovable and Bolt frequently unlock paid features simply when a user visits the /success page, which anyone can do by typing the URL. The writeup recommends fulfilling orders only through a server-side webhook that verifies the Stripe-Signature header with constructEventAsync on the raw body, checks payment_status, deduplicates already-fulfilled sessions, and restricts client-side updates to paid-status columns via Supabase RLS.

by read3 min views1 publishedOct 29, 2026

You asked your AI builder to "add Stripe." It worked. You paid with a test card, landed on /success, and your app unlocked the Pro stuff.

Here's the problem. Many AI-built apps unlock paid features just because someone opened the success page. Anyone can type /success into the address bar. No card needed. That's not a checkout. That's an honor system.

Stripe says this plainly in its own docs: you can't rely on the landing page alone, because a customer might pay and then lose their connection before the page loads. Webhooks are required (Stripe's fulfillment docs). So the success page is wrong both ways. It can unlock things for people who didn't pay, and it can miss people who did.

yourapp.com/success or yourapp.com/success?session_id=anything. If yes, your app is giving things away. Fix it before you launch.

Customer pays on Stripe Checkout
        ↓
Stripe sends a signed webhook (checkout.session.completed) to YOUR server
        ↓
Your server checks the signature, then checks payment_status
        ↓
Your server marks the order paid in the database
        ↓
The success page just READS that status ("Thanks! You're all set.")

The browser never decides who paid. Your server does, based on a message that only Stripe can sign.

In Supabase, that's an Edge Function (for example stripe-webhook). Have it listen for:

checkout.session.completed`` checkout.session.async_payment_succeeded (for bank payments that clear later) Use Stripe's library to verify the Stripe-Signature header with your webhook secret (whsec_...). In a Supabase Edge Function (Deno), use constructEventAsync with the raw request body:

const body = await req.text(); // raw body, not req.json()
const sig = req.headers.get("Stripe-Signature")!;
const event = await stripe.webhooks.constructEventAsync(
  body, sig, Deno.env.get("STRIPE_WEBHOOK_SECRET")!,
  undefined, Stripe.createSubtleCryptoProvider()
);

If the check fails, return 400 and do nothing. Without this, anyone can send your webhook a fake "they paid" message.

Heads up: Stripe can't send a Supabase login token, so this one function usually runs with JWT checks off (--no-verify-jwt). That's fine only because the signature check is doing the security work. Never turn off JWT checks on your other functions to "fix" a 401.

payment_status A completed session isn't always a paid one. Stripe's own sample only fulfills when payment_status is not unpaid.

Stripe can send the same event more than once. Save the Checkout Session ID with the order and skip it if you've already fulfilled it.

If your orders or profiles table has is_paid, plan, or credits, make sure users can't update it themselves. With Supabase RLS, don't give users an UPDATE policy on those columns. Only the webhook (using the secret key on the server) should change them.

Your Stripe secret key (sk_live_...) and webhook secret belong in server env vars, never in VITE_ or NEXT_PUBLIC_ variables. The publishable key (pk_...) is the only Stripe key the browser should see.

It can show a spinner and check the order status from your database. It should never write "paid" anywhere.

"Don't mark orders or plans as paid from the success page. Create a Supabase Edge Function that receives Stripe webhooks, verifies the Stripe-Signature header with constructEventAsync on the raw body, handles checkout.session.completed and checkout.session.async_payment_succeeded, checks payment_status, and updates the order using the server-side key. Skip sessions already fulfilled. Make sure users can't update is_paid, plan or credits through RLS. Show me the changes before applying them."

Read what it changes before you accept it. AI tools love to "fix" errors by turning checks off.

/success by hand unlocks nothingis_paid / plan / credits sk_ key or whsec_ secret in front-end code Is the Stripe publishable key safe to show?

Yes. pk_ keys are meant for the browser. The sk_ secret key is not.

Can't I just check session_id on the success page?

Only if your server looks that session up with Stripe and checks it. Even then, you still need the webhook, because some customers never reach the page.

I build DeployVet, an outside-in security check for AI-built sites. If you want a free outside check of your own site: https://deployvet.com/?utm_source=devto

── more in #ai-tools 4 stories · sorted by recency
── more on @stripe 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/stripe-in-a-lovable-…] indexed:0 read:3min 2026-10-29 · —