Skip to main content

Stripe

Stripe Charged Them. The Lovable App Still Said Free.

Stripe Checkout can succeed while a Lovable app still shows the free plan. Read the same row the UI gates on, then compare it to the twin subscription.

2026-09-21FetchSandbox Engineering

Checkout returned success. Stripe showed paid. Someone in r/lovable still could not get past the paywall on an annual SMS product. Another thread: they paid to become a provider and landed back on customer. A third: "Stripe payment goes through, feature doesn't unlock. Almost always the webhook."

The charge is not the product. The product is the row the UI reads on the next render.

Short answer

After a Lovable Stripe checkout, do not trust the success page. Read the same columns the pricing gate uses for that authenticated user — profiles.role, subscriptions.status, is_pro, whatever the agent named. Then read the Stripe customer or subscription on a twin. If Stripe says active and the gate still says free, the join is wrong. Fix the write, not the Checkout button.

Lovable's Stripe docs already say subscriptions should link to the Supabase Auth id. Generated code often writes one table and reads another.

The join that Lovable apps miss

The happy path the agent ships looks like this:

Checkout Session  →  200 page  →  user thinks they are Pro

The path the UI actually uses:

auth.uid()  →  profiles.role / subscriptions.tier  →  show Pro or Free

Those meet only if checkout.session.completed writes the same user id the session cookie has.

Common splits on r/lovable and in generated Edge Functions:

Twin / Stripe saysUI readsTypical miss
checkout.session.completed, paidprofiles.role = 'free'Handler upserted subscriptions, gate reads profiles
Customer cus_... existsNo row for auth.uid()client_reference_id or metadata.user_id never set
subscriptions.status = activeClient still "pending"Frontend never refetches after return URL
Provider paidRole flipped back to customerWebhook never deployed, or secrets were never added
Event 200 in StripeFeature still lockedJWT 401 on an earlier attempt, or a different function slug

Lovable's Stripe guide lists customer.subscription.created / updated / deleted. RapidDev's membership write-up says the webhook upserts subscriptions and RLS reads that table. If the agent gated /app on profiles.is_pro instead, the upsert is theater.

A 200 from the webhook only means the function returned 200. It does not mean the gate row changed.

How to prove unlock on a twin

Use a Stripe twin so you are not debugging live money or burning Lovable credits on another "please fix Stripe" prompt.

  1. Add the Lovable custom MCP connector or mint the twin from Cursor
  2. Point the Checkout Edge Function at sandbox_base_url + sandbox_api_key
  3. Put the Auth user id on the session (client_reference_id or metadata.supabase_user_id)
  4. Complete checkout against the twin
  5. Ask Lovable, in the same project:
After this checkout, SELECT the exact columns the pricing page
uses to hide the upgrade button for my user. Paste the row.
  1. On the twin, read the customer / subscription / PaymentIntent for that session
  2. They must agree. If they do not, stop. Do not publish.
Stripe twin subscription.status = active
profiles.role                 = free

That pair is the receipt. "Webhook said 200" is not.

If the POST never reached the function, you have a 401 JWT redeploy, not an entitlement bug.

What "paid" does not mean

  • The success redirect ran. Stripe can redirect before your handler finishes.
  • A subscriptions row exists. The UI may ignore it.
  • Test card 4242 worked. Cancel and customer.subscription.deleted are still untested.
  • You asked Lovable "did you connect Stripe?" and it said yes. In r/lovable, people later found STRIPE_WEBHOOK_SECRET was never added.

Cancel, past_due, and plan change are separate events. Prove those the same way: change state on the twin, read the gate row again.

Broader loop for AI-built payments: integration testing for AI-built apps. Preview storefronts that only need a charge plus a receipt email are a different join — see the Cozy Closet twin checkout.

Questions about Lovable Stripe unlock

Why did Stripe take payment but Lovable still shows free?

The Checkout session succeeded. The row your React gate reads did not change, or it changed for a different user id. Compare those two facts. Do not compare the success URL to the Stripe dashboard.

The subscription is in Supabase. Why is the app blind?

The write table is not the read table. Or RLS hides the new row from the signed-in role. SELECT as that user, not as the service role in the dashboard.

Do I need live Stripe to test unlock?

No. A twin Checkout still creates a customer and a paid session you can read back. Live keys are the last check, not the first.

Can the Lovable MCP connector prove the join?

It can boot the twin and run checkout. You still have to read the gate columns in your database. The connector does not magically know which column the agent used for isPro.

Is a webhook 200 enough?

No. 200 means the function finished. Unlock means the next page load sees Pro for auth.uid().