Lovable Stripe payments: choosing the right setup and making it reliable
Lovable offers built-in payments (any paid plan, Paddle or Stripe, webhooks managed for you) or your own Stripe account connected with a restricted or secret key. With your own Stripe, webhooks are off by default and subscriptions are checked by email at sign-in, so add a verified webhook and store subscription status before you take real money.
Symptom
Payments in a Lovable app usually look fine in a demo and go wrong in one of these ways once real customers arrive:
- Someone paid, but their account didn't upgrade.
- Someone cancelled in Stripe and still has access weeks later.
- The checkout button does nothing, because the browser blocked the new tab.
- The Manage subscription button fails in the editor preview.
- You tested in the preview with a live key and charged a real card.
On built-in payments, the error you are most likely to see in logs is this one, which Lovable's own troubleshooting covers:
Why it happens
Lovable has two separate payment integrations, and most problems come from not knowing which one you are on.
| Built-in payments | Your own Stripe account | |
|---|---|---|
| Plan | Any paid plan | Any plan, including Free |
| Provider | Paddle or Stripe, account created by Lovable | Your existing Stripe account |
| Preview | Test mode, test cards only | No separate test environment: a live key means real charges |
| Webhooks | Registered by Lovable, two per environment | Off by default, you add them |
| Where you manage products | Lovable, not the provider dashboard | Stripe dashboard |
Your own Stripe account. You create a restricted key (rk_) in Stripe, or use a secret key (sk_), and paste it into Lovable's Connect Stripe form, never the chat. Lovable stores it as a backend secret called STRIPE_SECRET_KEY. For subscriptions, it builds Checkout for each plan and a check at sign-in that asks Stripe whether the user is subscribed, matching users by email address. Webhooks are optional and off by default. That means your app only finds out about a payment, renewal or cancellation when it asks. A customer who changes their email, or cancels mid-session, can fall out of step with what Stripe knows.
Built-in payments. Lovable sets up the provider account, registers two webhook endpoints per environment (one for your app, one monitoring copy for Lovable) and keeps test and live separate. The rules are strict: don't create webhooks yourself, don't delete either endpoint, and don't edit products in the provider dashboard, because that causes ID mismatches. Publishing syncs products and prices from test to live, but discounts don't sync. There is one active subscription per user per environment by default, and a project with payments enabled can't be remixed.
The fix
1. Decide which route you are on, deliberately
If you sell software or a digital product to a global audience, built-in payments with Paddle make Paddle the merchant of record, at 5% plus 50 cents per transaction (a flat 10% on transactions under 10 dollars), with no extra Lovable fee. If you already have a Stripe account, need it on the Free plan, or want full control of your webhooks and data, connect your own. You can't mix them in one project, and switching later means recreating products and prices.
2. Own Stripe: add a verified webhook
Ask Lovable to write the webhook function, as its docs suggest. Then in Stripe's Workbench, open Webhooks, create an event destination pointing at that function's URL, and give Lovable the signing secret when it asks. Subscribe only to the events you handle. For subscriptions that means at least customer.subscription.created, customer.subscription.updated and customer.subscription.deleted, plus invoice.payment_failed if you email customers about failed payments.
Compare what Lovable writes against this. The two things that matter are reading the raw body and verifying the signature before trusting anything:
// supabase/functions/stripe-webhook/index.ts
import Stripe from "npm:stripe@^23"; // match the version your other functions use
import { createClient } from "npm:@supabase/supabase-js@2";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);
const cryptoProvider = Stripe.createSubtleCryptoProvider();
// The service role key bypasses RLS, so only use it server-side.
// SUPABASE_SERVICE_ROLE_KEY is Supabase's legacy key name; projects on the
// new API keys get secret keys in SUPABASE_SECRET_KEYS instead.
const admin = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);
Deno.serve(async (req) => {
const signature = req.headers.get("Stripe-Signature");
const body = await req.text(); // raw body, not req.json()
let event: Stripe.Event;
try {
event = await stripe.webhooks.constructEventAsync(
body,
signature!,
Deno.env.get("STRIPE_WEBHOOK_SECRET")!, // use the name Lovable stored the signing secret under
undefined,
cryptoProvider,
);
} catch (err) {
return new Response(`Webhook error: ${(err as Error).message}`, { status: 400 });
}
if (event.type.startsWith("customer.subscription.")) {
const sub = event.data.object as Stripe.Subscription;
const userId = sub.metadata.user_id; // set when you create the Checkout Session
if (userId) {
const { error } = await admin.from("subscriptions").upsert({
user_id: userId,
stripe_customer_id: String(sub.customer),
stripe_subscription_id: sub.id,
status: sub.status,
updated_at: new Date().toISOString(),
});
if (error) return new Response("Database error", { status: 500 });
}
}
return Response.json({ received: true });
});Stripe can't send a user's login token, so the function has to accept calls without one. In Supabase that is set per function:
# supabase/config.toml
[functions.stripe-webhook]
verify_jwt = falseThe signature check is what makes that safe. Without it, anyone could post a fake "subscription active" event.
For metadata.user_id to exist, the checkout function must pass it when it creates the session, with subscription_data: { metadata: { user_id: user.id } }. Matching on user ID instead of email removes the email-change problem.
3. Read access from your database, not a success page
Store the status where your app and its row level security can see it, and let only the webhook write to it:
create table public.subscriptions (
user_id uuid primary key references auth.users (id) on delete cascade,
stripe_customer_id text unique,
stripe_subscription_id text unique,
status text not null,
updated_at timestamptz not null default now()
);
alter table public.subscriptions enable row level security;
-- Users can read their own row. No insert or update policy:
-- only the webhook, using the service role, writes here.
create policy "Users can read their own subscription"
on public.subscriptions
for select
to authenticated
using ( (select auth.uid()) = user_id );Grant access when status is active or trialing. Stripe's guidance is to revoke it when a subscription becomes canceled or unpaid, and to tell the customer when it goes past_due.
4. Built-in payments: fix the webhook, don't rebuild it
If you see "Invalid webhook signature", Lovable's fix is to check the handler exists and no duplicate endpoint points at the same URL. If every delivery in one environment fails, ask Lovable to rotate that environment's webhook secret, then republish straight away. Events missed during rotation aren't redelivered, so check the provider dashboard for payments made in that window. Don't add your own endpoint alongside Lovable's.
5. Go live in the right order
With your own Stripe: submit a live key in the Update Stripe secret key form, ask Lovable to switch to live price IDs, then make a real purchase on the published app. With built-in Stripe: claim the sandbox account Lovable created, finish onboarding in Stripe, then pass the readiness check. Both providers' readiness checks want a privacy policy, terms of service and a refund policy before live checkout works.
How to prevent it
- Keep a test key in the project until launch day. Lovable's test card is 4242 4242 4242 4242 with any future expiry and any 3-digit CVC.
- Test the unhappy paths: cancel, fail a renewal, refund, and close the tab before the success page loads.
- Activate the customer portal in Stripe and test it on the published app. It won't work in the preview, and popup blockers stop it too.
- If checkout breaks, read the function logs under More, then Settings, then Logs (on your own Supabase, under Edge Functions in the Supabase dashboard), and Stripe's own logs under Developers, then Logs.
- Disconnecting payments doesn't cancel subscriptions or refund anyone. On your own Supabase, the key also stays in Supabase until you delete it there.
When to get help
Payments are where a small mistake costs real money, and where AI-built code most often skips the boring parts like refunds and failed renewals. My guide to getting from prototype to product puts payments in context with everything else a launch needs, and adding Stripe to an AI-built app covers the patterns beyond Lovable. Payments also lean on login, so if users can't stay signed in, fix that first with the Lovable auth guide.
For the wider Lovable checklist, read taking a Lovable app to production. The free vibe-code health check gives you a first read, and if the payment flow needs untangling a Rescue Sprint fixes it at a fixed price agreed after the check.
- Lovable docs: Connect your own Stripe account, checked 10 November 2026
- Lovable docs: Built-in payments, checked 10 November 2026
- Supabase docs: Edge Function secrets, checked 10 November 2026
- stripe-node changelog, checked 10 November 2026
- Supabase docs: Function configuration, checked 10 November 2026
- Stripe docs: Using webhooks with subscriptions, checked 10 November 2026
The launch checklist for founders, as a PDF
About 40 checks across legal, payments, domain and email, analytics, backups and support.
Questions founders ask
Can I use Stripe on Lovable's Free plan?
Yes, by connecting your own Stripe account. Built-in payments need a paid Lovable plan.
Are payments in the Lovable preview real?
With built-in payments, the preview runs in test mode. With your own Stripe account, the preview has no separate test environment, so a live key in the preview takes real money.
Why does the Manage subscription button do nothing in the preview?
Lovable's docs say the Stripe customer portal does not work in the preview panel. Activate the portal in Stripe, then test it on the published app in a normal browser tab.
Can I use built-in payments and my own Stripe account together?
No. A Lovable project can use one or the other. Switching means recreating products and prices, and existing subscribers do not move across.

Ant
Product designer who builds MVPs, based in Nottingham. 10+ years designing digital products, and the only person at BlackFig. More about me