If you can dream it we can build it
Build software 10X faster with the power of low-code and our agile strategies.
2026-11-03
%20(1).png)
To add Stripe payments to a Lovable app, store your Stripe secret key as a server-side secret, create Checkout Sessions in a Supabase Edge Function, confirm payments with a signature-verified Stripe webhook, choose subscription or one-time billing before you build the pricing flow, and test the whole path in Stripe test mode before you switch to live keys. The order matters. Most payment bugs come from exposing secret keys in the browser or granting access based on the redirect instead of the webhook.
Stripe issues two kinds of keys. The publishable key (pk_test_... or pk_live_...) is designed for the browser. The secret key (sk_test_... or sk_live_...) can create charges, read customer data, and issue refunds. It must never appear in frontend code, in any environment variable exposed to the browser (such as a Vite variable with the VITE_ prefix), or in your Git repository.
Store the secret key as a Supabase secret:
supabase secrets set STRIPE_SECRET_KEY=sk_test_...
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...Edge Functions read these with Deno.env.get("STRIPE_SECRET_KEY"). Before each deploy, search your built frontend bundle for sk_. If a secret key shows up there, roll it in the Stripe dashboard immediately.
When a user clicks a buy or subscribe button, the frontend calls an Edge Function. That function creates a Stripe Checkout Session and returns its URL, and the browser redirects to Stripe's hosted payment page. The function should do three things:
client_reference_id so the webhook can link the payment to the right account.import Stripe from "npm:stripe@14";
import { createClient } from "npm:@supabase/supabase-js@2";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);
const ALLOWED_PRICES = ["price_123", "price_456"]; // your Stripe price IDs
Deno.serve(async (req) => {
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: req.headers.get("Authorization")! } } }
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response("Unauthorized", { status: 401 });
const { priceId } = await req.json();
if (!ALLOWED_PRICES.includes(priceId)) {
return new Response("Invalid price", { status: 400 });
}
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price: priceId, quantity: 1 }],
client_reference_id: user.id,
customer_email: user.email,
success_url: "https://YOUR-APP.lovable.app/success",
cancel_url: "https://YOUR-APP.lovable.app/pricing",
});
return Response.json({ url: session.url });
});Add CORS headers and handle the preflight OPTIONS request, or the browser will block the call. Change mode to "subscription" for recurring billing (see Step 4).
Do not grant access on the success redirect. Users can close the tab before the redirect loads, and anyone who guesses the success URL can load it. Stripe sends a server-to-server event when payment completes, and that event is the source of truth.
Create a second Edge Function, for example stripe-webhook. Then:
checkout.session.completed.verify_jwt = false for this function in your Supabase config. Stripe does not send a Supabase JWT, so the default check would reject every event.Stripe-Signature header before you trust anything in the payload. Parsing the body as JSON first breaks verification.const signature = req.headers.get("Stripe-Signature")!;
const body = await req.text();
const event = await stripe.webhooks.constructEventAsync(
body,
signature,
Deno.env.get("STRIPE_WEBHOOK_SECRET")!
);
if (event.type === "checkout.session.completed") {
const session = event.data.object;
// Mark client_reference_id as paid using a service-role Supabase client
}
return new Response("ok", { status: 200 });Write the database update with the service-role key, which bypasses row-level security. Keep that client inside the webhook function and never expose it to the frontend.
The billing mode determines your database schema, your webhook handlers, and your pricing page, so decide it first.
mode: "payment"). A purchase grants access once. You need one webhook handler, checkout.session.completed, and a table that records paid orders. Refunds are handled in the Stripe dashboard or through the API, and you must revoke access when they happen.mode: "subscription"). Stripe creates a Customer and a Subscription object. Access must stay in sync with the subscription's status, so handle invoice.paid, invoice.payment_failed, and customer.subscription.updated and customer.subscription.deleted. Stripe's Customer Portal lets users change plans and update cards without you building those screens.Test mode uses separate keys, separate objects, and a separate webhook endpoint with its own signing secret. Walk through the full flow before going live:
STRIPE_WEBHOOK_SECRET.4242 4242 4242 4242, then confirm the database updates from the webhook, not from the success page.stripe trigger checkout.session.completed.When every path works, create a live-mode webhook endpoint with its own signing secret, set the live secret key, and switch the price IDs to live prices. Test and live objects do not carry over between modes.
VITE_ end up in places you cannot easily clean up. Keep secrets in Supabase and rotate any key that has been exposed.Basic one-time checkout is a reasonable weekend project for a developer who understands server-side secrets. The effort grows when you add subscription upgrades and downgrades with proration, tax collection, multiple currencies, marketplace payouts, or regulated flows such as identity verification. Errors in those paths cost money and can expose customer data. Revex designed and built Perkster, a fintech rewards app, and our fintech app development page lists the Stripe and Supabase work we do for financial products.
Yes. The same rules apply: the secret key and Checkout Session creation must run on a server, and payment status must come from a verified webhook. Supabase Edge Functions are a convenient server runtime inside a Lovable project, but any server-side environment works.
Yes. The redirect to your success page is not reliable enough to grant access. The checkout.session.completed webhook confirms that payment succeeded and is the event your database should act on.
Only the publishable key. The secret key must stay on the server, in Supabase secrets. If you use Stripe's hosted Checkout with a redirect, the frontend does not need any Stripe key at all.
Use test mode with test API keys, test card numbers, and a test-mode webhook endpoint. Stripe's CLI can trigger events locally. Switch to live keys and a live webhook only after the full flow works in test mode.
It depends on where you sell and what you sell. Stripe Tax is an optional product that calculates and reports sales tax and VAT. Confirm your obligations with an accountant or tax advisor before launch.
Revex Agency
Revex is a high-end no-code and AI software development agency that helps startups and enterprises build and launch custom digital products up to 10x faster.
.png)
.png)
Build software 10X faster with the power of low-code and our agile strategies.