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 says | UI reads | Typical miss |
|---|---|---|
checkout.session.completed, paid | profiles.role = 'free' | Handler upserted subscriptions, gate reads profiles |
Customer cus_... exists | No row for auth.uid() | client_reference_id or metadata.user_id never set |
subscriptions.status = active | Client still "pending" | Frontend never refetches after return URL |
| Provider paid | Role flipped back to customer | Webhook never deployed, or secrets were never added |
Event 200 in Stripe | Feature still locked | JWT 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.
- Add the Lovable custom MCP connector or mint the twin from Cursor
- Point the Checkout Edge Function at
sandbox_base_url+sandbox_api_key - Put the Auth user id on the session (
client_reference_idormetadata.supabase_user_id) - Complete checkout against the twin
- 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.
- On the twin, read the customer / subscription / PaymentIntent for that session
- 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
subscriptionsrow exists. The UI may ignore it. - Test card
4242worked. Cancel andcustomer.subscription.deletedare still untested. - You asked Lovable "did you connect Stripe?" and it said yes. In r/lovable, people later found
STRIPE_WEBHOOK_SECRETwas 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().