2026-11-03

How to Add Stripe Payments to a Lovable App (2026 Guide)

Hero BG Image

How to Add Stripe Payments to a Lovable App (2026 Guide)

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.

What you need before you start

  • A Lovable project connected to Supabase, which provides the database, auth, and Edge Functions.
  • A Stripe account with test mode enabled.
  • Products and prices created in the Stripe dashboard. You will reference their price IDs in code.
  • Basic familiarity with Supabase secrets and deploying Edge Functions.

Step 1: Keep Stripe keys out of the client

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.

Step 2: Create a Checkout Session in a Supabase Edge Function

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:

  • Confirm the caller is signed in. Read the user from the request's Authorization header. Do not accept a user ID from the request body.
  • Accept only price IDs from an allowlist you control, so a user cannot submit an arbitrary price ID from the browser console.
  • Attach the Supabase user ID as 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).

Step 3: Set up a webhook endpoint with signature verification

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:

  • Add the function's URL in the Stripe dashboard under Developers, then Webhooks, and subscribe to events such as checkout.session.completed.
  • Set 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.
  • Read the raw request body and verify the 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.

Step 4: Choose subscription or one-time payment before you build

The billing mode determines your database schema, your webhook handlers, and your pricing page, so decide it first.

  • One-time payment (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.
  • Subscription (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.
  • Mixed model (a base plan plus one-time add-ons). It is workable, but entitlement logic gets complicated quickly. Plan it in the data model before you write any code.

Step 5: Test in Stripe test mode end to end

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:

  • Register a test-mode webhook endpoint pointing at your deployed function, and store its signing secret as STRIPE_WEBHOOK_SECRET.
  • Complete a checkout with Stripe's test card 4242 4242 4242 4242, then confirm the database updates from the webhook, not from the success page.
  • Test failure paths: a cancelled checkout, a declined card (Stripe documents specific test cards for declines), and an expired session.
  • Replay an event. Stripe retries webhooks that fail or time out, so the same event can arrive more than once. Record processed event IDs and skip duplicates.
  • Use the Stripe CLI to fire events locally with 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.

Three pitfalls to avoid

  1. Granting access from the redirect. The success page is a UI event, not a payment confirmation. Access should depend only on a verified webhook (or a server-side check of the session status).
  2. Leaking the secret key. Keys pasted into Lovable prompts, committed to GitHub, or prefixed with VITE_ end up in places you cannot easily clean up. Keep secrets in Supabase and rotate any key that has been exposed.
  3. Ignoring idempotency. Webhook retries and duplicate deliveries will double-credit users or send duplicate emails unless each event ID is recorded and checked before processing.

When to hire an agency

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.

Frequently asked questions

Can I add Stripe to a Lovable app without Supabase?

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.

Do I need a webhook for one-time payments?

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.

Which Stripe key can go in the frontend?

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.

How do I test payments without real money?

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.

Do I need Stripe Tax?

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

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.

Book a free strategy callImage Gradient
Image Gradient

Read more articles

If you can dream it we can build it

Build software 10X faster with the power of low-code and our agile strategies.

Logo 01Logo 01Logo 01Logo 01Logo 01