Guide · Stripe + Supabase

Customers paid, but they're still on the free plan.

Updated 26 October 2026 · Postlaunch

Short answer

Stripe took the money, but your app never heard about it. In Lovable, Bolt and Supabase apps the cause is almost always one of nine things: webhooks were never set up, the webhook function rejects Stripe with a 401, the signature check fails, the wrong events are subscribed, the user isn't linked to the Stripe customer, the update hits the wrong table or is blocked by RLS, or test and live keys are mixed up. Start with Stripe's delivery log: it tells you which one.

Step 1: read Stripe's delivery log

In the Stripe dashboard open Developers → Webhooks, pick your endpoint and look at recent deliveries. The status code tells you where to look:

The nine usual causes

1. Webhooks were never set up

Lovable's Stripe integration doesn't use webhooks by default; its docs say the app checks status directly with Stripe unless you ask for webhooks. That can work for checkout, but cancellations, failed renewals and plan changes made in the customer portal never reach your database. Ask for a webhook endpoint, or add one yourself.

2. The webhook function returns 401

Supabase Edge Functions check for a Supabase login token by default. Stripe doesn't send one, so every delivery is rejected. Turn off JWT verification for that one function (verify_jwt = false under [functions.stripe-webhook] in supabase/config.toml, or deploy with --no-verify-jwt) and rely on the Stripe signature check instead.

3. The signature check fails

Stripe signs the raw request body. If the function parses the JSON first, the check fails. In an Edge Function, read the body with await req.text() and verify it with stripe.webhooks.constructEventAsync(body, signature, secret, undefined, Stripe.createSubtleCryptoProvider()). Also check that the signing secret (whsec_…) belongs to this exact endpoint and mode.

4. The wrong events are subscribed

Most subscription apps need at least checkout.session.completed, customer.subscription.updated and customer.subscription.deleted, and often invoice.paid and invoice.payment_failed. Missing the last two means failed renewals never downgrade anyone.

5. The user isn't linked to the Stripe customer

The webhook has to know which of your users paid. Pass your user ID when you create the checkout session (for example client_reference_id or metadata) and store the Stripe customer ID on the user, so later subscription events can be matched.

6. It writes to the wrong table or column

AI builders often create a subscriptions table while the app checks profiles.plan, or the reverse. Find the exact field your paywall reads and make the webhook update that one.

7. Row-Level Security blocks the write

If the function writes with the public key, RLS can silently block the update. Webhook handlers should write with a server-side secret key kept in function secrets, never in the browser, and users should not be able to change their own plan.

8. Test and live mode are mixed

Test and live mode have separate keys, endpoints and signing secrets. A live checkout sending events to a test endpoint (or the reverse) looks like "nothing happens".

9. Duplicate or out-of-order events

Stripe can deliver an event more than once and not always in order. Store event IDs you've processed and base the plan on the subscription's current status, not on which event arrived last.

After the fix: recover the customers you missed

Resend failed events from the endpoint's page in the Stripe dashboard (or with the Stripe CLI), then compare Stripe's active subscriptions with the plans in your database. Anyone who paid but stayed on free, or cancelled but kept Pro, should be corrected and, where it matters, contacted.

Want it done for you?

$399 Payments fixed · fixed price · 2 business days after access

We verify webhooks, sync plans to the field your app actually checks, handle cancel, upgrade and downgrade, set up the customer portal, replay missed events and give you a list of affected customers. Works for Lovable, Bolt, Replit, Cursor and v0 apps. If we can't fix it, you don't pay.

Questions

Why does my Stripe payment work but my app still shows the free plan?

The payment succeeded in Stripe, but your app never recorded it. Usually Stripe's webhook never reached your app, it was rejected (401 or signature error), or it updated a different table or column than the one your app checks.

Does Lovable set up Stripe webhooks automatically?

Not with the Stripe integration by default. Lovable's docs say the app checks payment status directly with Stripe unless you ask for webhooks. Lovable's newer built-in payments feature does register webhooks for you.

How do I recover payments that were missed?

Fix the endpoint first, then resend the failed events from the Stripe dashboard or with the Stripe CLI, and compare Stripe's list of active subscriptions with the plans stored in your database.

How much does it cost to have this fixed?

Our Payments fixed service is $399 at a fixed price, done in 2 business days after access, including replaying missed events and a list of affected customers. If we can't fix it, you don't pay.

Sources: Lovable Stripe docs · Supabase: Stripe webhooks · Supabase: securing Edge Functions · Stripe webhooks