Article
Lovable Stripe payments not working in production: what to check, in order
Published on
Stripe in a Lovable app usually works the first time, in test mode, with the
4242 4242 4242 4242 card. The trouble starts when you switch to real money.
Checkout throws an error, or the customer pays and Stripe shows the charge but
your app never unlocks the plan, or the "Manage subscription" button does
nothing.
These failures have a short list of causes, and most of them sit between the two Stripe modes rather than in your code. This is the order we check them in when a Lovable app (or a Bolt or v0 app using the same Supabase pattern) stops taking payments after launch.
First, know what you are debugging. According to Lovable's own documentation,
the Stripe integration runs your Stripe calls from edge functions, stores the
key as a backend secret called STRIPE_SECRET_KEY, and does not use webhooks
by default: for subscriptions, the app asks Stripe at sign-in whether the
user has an active subscription, and it matches the user by email. If you, or
the AI, added webhooks later, sections 5 and 6 apply to you. If not, the cause
is almost certainly in sections 1 to 4.
1. The app still has test keys or test price IDs
Stripe test mode and live mode are separate worlds. A product you created in
test mode does not exist in live mode, and its price ID (price_...) is
different. Lovable's going-live steps are three: submit a live key in the
"Update Stripe secret key" form, switch every price ID in the code to the live
one, and make one real purchase on the published app.
The second step is the one people skip. The symptom is an error from the checkout function along the lines of:
No such price: 'price_1Q...'; a similar object exists in test mode,
but a live mode key was used to make this request.
Stripe is telling you exactly what happened. Copy the live price IDs from the
Stripe dashboard (with the "Test mode" toggle off) and replace the test ones,
wherever the AI put them: a constants file, the checkout edge function, or
rows in a plans table.
The opposite mistake costs more. The Lovable preview has no separate test environment, so once a live key is stored, every purchase you make while testing in the preview is a real charge. Refund yourself from the dashboard and move testing to a test key or a separate project.
2. The restricted key is missing a permission
Lovable accepts a restricted key (rk_live_...) or a secret key
(sk_live_...). Restricted keys are the safer choice, but a live restricted
key is created separately from the test one, and it is easy to give it fewer
permissions than the code needs. Creating Checkout Sessions, reading
customers and subscriptions, and creating portal sessions each need their own
permission.
Symptom: a Stripe error mentioning that the key does not have the required permissions, visible in the edge function logs and in Stripe under Developers then Logs. Fix: edit the key in Stripe and add the missing resource permission. You do not need a new key for that.
3. The customer paid with a different email
This one produces the most confusing support tickets, because nothing is broken. The payment succeeded and the subscription is active in Stripe. The app still shows the free plan.
The default Lovable subscription check matches users to Stripe customers by
email address. If the user signed up as ana@company.com and typed
ana.personal@gmail.com into Stripe Checkout, the check finds no subscription
for ana@company.com. The same happens when someone changes their login email
after paying.
Fix: have the checkout function pass the logged-in user's email to Stripe
(the customer_email parameter, or an existing customer ID) so Checkout
pre-fills it and the user cannot type another one. Better still, store the
Stripe customer ID on the user's profile row the first time you create it and
look subscriptions up by that ID, not by email. Also put the user's ID in
client_reference_id or in metadata, so you can always trace a payment back
to an account.
4. The customer portal was never activated in live mode
The "Manage subscription" button opens Stripe's hosted customer portal, and the portal has to be configured once in the Stripe dashboard. Test and live modes have separate configurations, so activating it while testing does not carry over. In live mode without a saved configuration, the portal call fails with an error saying no configuration was provided and the live mode default configuration has not been created.
Fix: with test mode off, open Settings, then Billing, then Customer portal, choose what customers may do (cancel, switch plans, update cards) and save. If the button still does nothing, try it on the published app in its own tab: Lovable notes that the portal does not work inside the preview panel, and that Checkout opens in a new tab, so a popup blocker can swallow it.
5. Webhooks: the endpoint exists in test mode only
If your app relies on webhooks (for example, checkout.session.completed
writes is_pro = true to a table), the live webhook endpoint is its own
object. Stripe does not copy test endpoints to live mode. Each endpoint also
has its own signing secret (whsec_...), so the test secret will reject every
live event.
Check, in order:
- In Stripe, live mode, Developers then Webhooks: is there an endpoint pointing at your edge function URL, subscribed to the events your code handles?
- Is the signing secret stored in your backend secrets the one from that live endpoint?
- In the endpoint's event deliveries, what status code came back? This is the fastest diagnosis on this page. A 401 points to section 6, a 400 to a signature problem, a 500 to your handler, and a 3xx to a URL that redirects (Stripe treats a redirect as a failed delivery).
6. The edge function rejects Stripe before your code runs
Two problems are specific to webhooks on Supabase edge functions, which is what Lovable Cloud and most Lovable projects with their own Supabase use.
A 401 on every delivery. Supabase edge functions require a valid JWT in
the Authorization header by default. Stripe does not send one; it signs the
request with the Stripe-Signature header instead. The webhook function has to
be deployed with JWT verification turned off, and then the signature check is
the only thing standing between the internet and your database, so it has to
be there. With the Supabase CLI:
supabase functions deploy stripe-webhook --no-verify-jwt
or, in supabase/config.toml:
[functions.stripe-webhook]
verify_jwt = false
A 400 with a signature error. Signature verification needs the raw request
body, byte for byte. If the handler calls req.json() first, the signature no
longer matches and you get No signatures found matching the expected signature for payload. In Deno, the synchronous constructEvent also fails,
because Web Crypto is asynchronous. Supabase's own example uses this shape:
const cryptoProvider = Stripe.createSubtleCryptoProvider()
const body = await req.text()
const event = await stripe.webhooks.constructEventAsync(
body,
req.headers.get('Stripe-Signature')!,
Deno.env.get('STRIPE_WEBHOOK_SIGNING_SECRET')!,
undefined,
cryptoProvider,
)
One more Lovable-specific trap: a later prompt can make the AI regenerate the
webhook function from an older pattern and quietly reintroduce req.json() or
the synchronous call. If payments
broke after a change that had nothing to do with payments, diff that function
first.
7. The database write fails silently
The webhook arrives, the signature checks out, Stripe gets its 200, and the
user's row is still unchanged. Usually the handler writes with a client that
Row Level Security blocks: a webhook has no logged-in user, so a policy like
auth.uid() = user_id can never pass. Server-side code that has to update
any user's row needs the service_role key, read from the function's secrets
and never shipped to the browser. If you see
new row violates row-level security policy in the function logs, our
guide to that RLS error
covers the safe fix.
Check two more things while you are in that handler. Stripe retries deliveries, so processing the same event twice must not grant two subscriptions or send two welcome emails: record the event ID and skip it if you have seen it. And do not unlock anything from the success page alone. The success URL is a link anyone can open; access should come from what Stripe reports, through the sign-in check or the webhook.
Where to look when none of this matches
Two logs answer most remaining questions. Stripe's Developers then Logs shows every API request your functions made and the exact error Stripe returned. The edge function logs (in Lovable Cloud's function logs, or the Supabase dashboard under Edge Functions) show what your code did with the answer. Reading both side by side for one failed purchase usually ends the search.
Payments are the one part of an app where a wrong fix costs money twice: once in lost sales while it is broken, and again if the fix grants access nobody paid for. If the AI has been patching the checkout for days and every change moves the problem somewhere else, a fixed-price audit is the cheaper route: 450 EUR, 48 working hours, and a written report of what is wrong with the payment flow and the rest of the app, with each fix priced. If you continue into the rescue (1,500-6,000 EUR, fixed scope), the audit fee is deducted.
If payments are not the only thing that changed between preview and launch, start with the six usual reasons a Lovable app breaks in production, and run the security checklist before real customers arrive. Stripe keys and webhook handlers are both on it.