Stripe says “paid”, your app says “free”: why the feature didn’t unlock
Quick answer: Why did a Stripe payment succeed but my app didn’t unlock the feature?
Stripe confirms the payment, but your app usually only finds out through a webhook: a message Stripe sends to your backend, often a Supabase Edge Function, which then updates the user’s record. If the webhook URL is wrong, a secret is missing, the signature check fails, or the database update is blocked, the payment succeeds while access never changes.
This is the bug that costs real money. A customer pays, Stripe shows the payment as succeeded, the money is on its way to you… and your app still shows the free plan. The customer emails, annoyed. Or worse, they don’t, and just ask for a refund.
How payment → access actually works
In most AI-built apps it’s a chain of five links:
- Stripe takes the payment (Checkout or a payment link).
- Stripe sends a webhook, an HTTPS message saying “this checkout completed” or “this subscription is active”.
- Your backend receives it, usually a Supabase Edge Function in Lovable and Bolt apps.
- The function updates a database row:
plan = 'pro',subscription_status = 'active'or similar. - Your app reads that row and unlocks the feature.
Stripe only controls step 1 and 2. The redirect back to your “thank you” page doesn’t unlock anything by itself. If any link from 2 to 5 breaks, the payment succeeds and nothing changes in your app.
One Lovable-specific detail. If you use Lovable’s built-in payments, Lovable registers the webhook endpoints for you, and its docs warn that deleting your app’s endpoint in Stripe means “purchases still succeed, but subscriptions never activate and features never unlock.” If you connected your own Stripe account, Lovable says the integration doesn’t use webhooks by default and checks status with Stripe directly, unless you asked it to add them. Know which setup you have before you debug.
The 4 places it usually breaks
1. The webhook points to the wrong place
The endpoint in Stripe still points to an old preview URL, a deleted function, or a project you’ve since replaced. Or it’s registered in test mode only, and you’ve gone live. Test and live are separate in Stripe: separate endpoints, separate events, separate signing secrets.
2. Secrets missing in the Edge Function
The function needs at least your Stripe secret key and the webhook signing secret (starts with whsec_). Supabase’s docs are clear that local and production secrets are loaded differently: a .env file on your machine doesn’t reach the deployed function, so production secrets have to be set separately in the dashboard or CLI.
3. The signature check rejects the message
Your function should verify that each webhook really came from Stripe. That’s important, because without it anyone could send a fake “paid” message. But it’s also a common failure point. Stripe’s troubleshooting guide says the most common cause is the wrong signing secret: the test secret in live mode, the Stripe CLI’s secret instead of the Dashboard endpoint’s, or an old secret after it was rolled. The second most common is code that parses or changes the request body before checking it.
4. The database update is blocked
The webhook arrives, the signature is fine, and the update still does nothing. A webhook request has no logged-in user, so if the function writes with the user-level key, RLS policies that check auth.uid() match nothing, and an update quietly affects zero rows (see empty array). And if you add a new subscriptions table after October 30, 2026, it may fail with 42501 until access is granted (see our October 30 checklist). The function can use the secret key server-side, but that key must never end up in the frontend (here’s why).
How to see which one it is
You don’t need to read code to narrow it down. In Stripe, open Workbench → Webhooks, select your endpoint and open the Event deliveries tab. Each event is marked Delivered, Pending or Failed, and clicking one shows the HTTP status your function returned:
| What Stripe shows | Points to |
|---|---|
| No endpoint at all in this mode, or no events for the payment | Break 1: wrong place or wrong mode |
404 | Break 1: the URL doesn’t exist (old or deleted function) |
401 or 403 | The function rejects requests without a user login. Supabase’s docs note that Stripe webhook functions must be publicly reachable, because Stripe doesn’t have your users’ tokens |
400 | Often break 3: signature check failed (check the function’s logs in Supabase) |
500 | Break 2 or 4: the function crashed, often on a missing secret or a failed database write |
200 Delivered, but the user is still on free | Break 4: the update ran but changed nothing, or updated the wrong row |
Not sure which of these is your problem? The free health check looks at your live app in seconds, or request a free scan and we’ll tell you what we find.
Test mode worked, live mode doesn’t
This is the most common version of the story. Everything was tested with 4242… cards, then the app switched to live keys and the first real customers didn’t get access. Usually the live endpoint was never created, or it was created but the function still has the test signing secret. Stripe generates a different secret for each endpoint and mode, so the test one will never verify a live event.
What about the customers who already paid?
They were charged. The payment is real and visible in Stripe. Fixing the webhook won’t automatically fix their accounts, so make a list from Stripe’s payments and handle each one: give them access, or refund them, and let them know. Stripe does retry failed deliveries (more below), so some may catch up by themselves once the chain works. Don’t count on it for anything older than a few days.
Keep it from breaking again: Guard reviews every push
According to Stripe’s webhook docs, in live mode Stripe retries a failed event for up to three days with exponential backoff (sandbox events only get three retries over a few hours). After that, retries stop. You can still resend an event manually from the Dashboard for up to 15 days. So a webhook that breaks quietly on a Friday can cost you a weekend of customers before anyone notices, which is why it pays to catch the break before it ships.
The payment flow is also the part of the app that breaks most easily when you keep building: a renamed function, a new table, a rotated key. That’s what the $79/month Guard is for. We review every push so payment-flow regressions get caught early, not after customers complain.
Get it fixed
Customers paying and not getting access?
The $199 Fix: we repair the payment flow end to end, as a tested pull request within 72 hours. Not fixed, full refund. Then Guard ($79/month) reviews every push so the payment flow doesn’t quietly break again.
FAQ
Did my customer get charged?
Yes. If Stripe shows the payment as succeeded, the money was taken. The problem is only that your app never found out.
Will Stripe resend the webhook?
In live mode Stripe retries failed deliveries automatically for up to three days. You can also resend an event manually from the Dashboard for up to 15 days after it was created.
Why did it work in test mode?
Test and live mode have separate webhook endpoints and separate signing secrets. Going live often means the live endpoint is missing or the function still uses the test secret.
Is it safe to give customers access manually meanwhile?
Yes, as a stopgap. Check the payment in Stripe first, then upgrade the account by hand, and keep a list so you can confirm everything matches once the webhook works.
Can the free health check find this?
Partly. It spots Stripe live secret keys exposed in your JavaScript, which is a separate and serious problem. It can’t see the webhook flow, which needs a look at the repo and the Stripe setup, like the free full scan.
Related guides
- Supabase returns an empty array but the table has data? It’s usually RLS
- Supabase’s October 30 change is live: a 5-minute checklist for new tables
- Leaked your Supabase service_role key in a vibe-coded app? What to do now
- Is your Lovable app leaking user data? How to tell if your Supabase RLS is broken
- Your Supabase Edge Function works locally but fails in production: why
Launching soon? Run through our launch checklist for AI-built apps: 14 checks, each linked to the full guide.