Skip to main content
RapidDev - Software Development Agency

How to Build a Payment Gateway Integration with Lovable

Integrate Stripe into Lovable using the native Stripe connector, PaymentIntent flow, and Edge Function webhooks. You'll build a payment dashboard with products, customer records, and transaction history — powered by Supabase and verified webhook delivery using constructEventAsync on Deno. Works only in deployed mode, not preview.

What you'll build

  • Stripe native connector configured in Lovable Cloud tab with publishable and secret keys in Secrets
  • Products catalog page using shadcn/ui Cards with price display and a Buy button per product
  • PaymentIntent creation via Supabase Edge Function that returns a client_secret to the frontend
  • Stripe Elements checkout form embedded in a shadcn/ui Dialog using the official Stripe.js SDK
  • Supabase payments table recording every transaction with status, amount, and Stripe IDs
  • Webhook Edge Function using constructEventAsync to verify Stripe signatures and update payment status
  • Transaction history DataTable showing all payments with status Badges and date filters
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate13 min read2–3 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Integrate Stripe into Lovable using the native Stripe connector, PaymentIntent flow, and Edge Function webhooks. You'll build a payment dashboard with products, customer records, and transaction history — powered by Supabase and verified webhook delivery using constructEventAsync on Deno. Works only in deployed mode, not preview.

What you're building

Stripe's native connector in Lovable stores your Stripe publishable key and secret key in the Cloud tab → Secrets and makes them available to Edge Functions automatically. The payment flow works in two halves: the frontend collects card details using Stripe Elements (a pre-built secure UI component), and a Supabase Edge Function creates the PaymentIntent server-side so your secret key never touches the browser.

Webhooks are the critical piece. When Stripe confirms a payment, it sends a POST request to your Edge Function with a signature header. You must verify this signature using constructEventAsync (the async Deno-compatible version of constructEvent) before trusting the payload. After verification, update the payment status in Supabase and trigger any downstream logic.

The Supabase schema includes a payments table (id, stripe_payment_intent_id, amount, currency, status, customer_email, metadata, created_at), a products table (id, name, price_cents, stripe_price_id, description), and a customers table (id, email, stripe_customer_id). RLS ensures users can only read their own payment records.

Final result

A fully functional Stripe payment integration with a product catalog, embedded checkout, real-time payment status, and verified webhook delivery.

Tech stack

LovableFrontend app builder
StripePayment processing (native connector)
SupabaseDatabase and Edge Functions
Stripe.jsClient-side Elements UI
shadcn/uiUI components
RechartsRevenue charts

Prerequisites

  • Lovable Pro account for Edge Function generation and deployment
  • Stripe account (free) with a publishable key and secret key from the Stripe Dashboard
  • Stripe webhook secret created at Stripe Dashboard → Webhooks → Add endpoint
  • Supabase project with URL and anon key available
  • Understanding that Stripe does NOT work in Lovable preview — only in deployed mode

Build steps

1

Set up the Stripe secrets and Supabase schema

Add your Stripe keys to Lovable's Cloud tab → Secrets so Edge Functions can access them. Then ask Lovable to create the database schema for products, customers, and payments.

prompt.txt
1Create a Stripe payment integration with this Supabase schema:
2
3Tables:
4- products: id (uuid pk), name (text), description (text), price_cents (int), currency (text default 'usd'), stripe_price_id (text), is_active (bool default true), created_at
5- customers: id (uuid pk), user_id (references auth.users), email (text), stripe_customer_id (text unique), created_at
6- payments: id (uuid pk), user_id (references auth.users), customer_id (uuid references customers), product_id (uuid references products), stripe_payment_intent_id (text unique), amount (int), currency (text), status (text default 'pending'), metadata (jsonb default '{}'), created_at, updated_at
7
8RLS policies:
9- products: public SELECT for is_active=true, authenticated INSERT/UPDATE for admin role
10- customers: users can SELECT/UPDATE their own record (user_id = auth.uid())
11- payments: users can SELECT their own payments (user_id = auth.uid())
12
13Generate TypeScript types for all tables.

Pro tip: In Cloud tab → Secrets, add STRIPE_SECRET_KEY (sk_test_...) and STRIPE_WEBHOOK_SECRET (whsec_...) WITHOUT the VITE_ prefix. Only add VITE_STRIPE_PUBLISHABLE_KEY (pk_test_...) with the VITE_ prefix since it is used on the frontend.

Expected result: The three tables are created with RLS enabled. TypeScript types are generated. The app shell renders in preview.

2

Create the PaymentIntent Edge Function

Ask Lovable to create a Supabase Edge Function that accepts a product ID, creates a Stripe PaymentIntent server-side, and returns the client_secret to the frontend. This keeps your Stripe secret key off the browser.

prompt.txt
1Create a Supabase Edge Function at supabase/functions/create-payment-intent/index.ts.
2
3The function should:
41. Parse the request body: { productId: string, customerEmail: string }
52. Look up the product in Supabase to get price_cents and name
63. Look up or create a Stripe customer using STRIPE_SECRET_KEY from Deno.env
74. Create a Stripe PaymentIntent with amount = product.price_cents, currency = product.currency, customer = stripeCustomerId, metadata = { productId }
85. Insert a row into the payments table with status = 'pending' and stripe_payment_intent_id = paymentIntent.id
96. Return { clientSecret: paymentIntent.client_secret }
10
11Include CORS headers and OPTIONS handler. Use the Stripe API via fetch to https://api.stripe.com/v1/payment_intents with Basic Auth (secret key as username, empty password).
12
13Also upsert the customer into the customers table with stripe_customer_id.

Pro tip: Call the Stripe REST API directly via fetch instead of importing the Stripe Node SDK — Deno handles fetch natively and avoids compatibility issues with npm packages in Edge Functions.

Expected result: The Edge Function deploys. Calling it with a valid productId returns a clientSecret string. A pending payment row appears in Supabase.

3

Build the product catalog and checkout UI

Ask Lovable to build the product listing page with shadcn/ui Cards and an embedded Stripe Elements checkout Dialog that uses the client_secret from the Edge Function.

prompt.txt
1Build a product catalog page at src/pages/Products.tsx.
2
3Requirements:
4- Fetch all is_active products from Supabase
5- Display each as a shadcn/ui Card with: name, description, price formatted as currency, and a 'Buy Now' Button
6- Clicking Buy Now opens a Dialog
7- Inside the Dialog:
8 1. Call the create-payment-intent Edge Function to get a clientSecret
9 2. Show a loading Skeleton while the Edge Function responds
10 3. Once clientSecret arrives, render a Stripe Elements form using loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY) and <Elements stripe={stripePromise} options={{ clientSecret }}>
11 4. Inside Elements, render a <PaymentElement /> component for the card form
12 5. Add a 'Pay' Button that calls stripe.confirmPayment with return_url pointing to /payment-success
13- Install @stripe/react-stripe-js and @stripe/stripe-js via the package manager
14- Handle loading, error, and success states with appropriate shadcn/ui Alert components

Pro tip: Set appearance options on the Elements instance to match your Tailwind theme: pass { appearance: { theme: 'stripe', variables: { colorPrimary: '#your-primary-color' } } } to the Elements options.

Expected result: The product catalog renders. Clicking Buy Now opens the Dialog with a Stripe card form. Entering test card 4242 4242 4242 4242 and any future expiry completes the payment in test mode.

4

Create the webhook handler Edge Function

Build the Stripe webhook receiver that verifies signatures and updates payment status in Supabase. This is the most critical step — without verified webhooks, your app cannot reliably confirm payments.

supabase/functions/stripe-webhook/index.ts
1// supabase/functions/stripe-webhook/index.ts
2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
4import Stripe from 'https://esm.sh/stripe@14?target=deno'
5
6const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') ?? '', {
7 apiVersion: '2023-10-16',
8 httpClient: Stripe.createFetchHttpClient(),
9})
10
11const cryptoProvider = Stripe.createSubtleCryptoProvider()
12
13serve(async (req: Request) => {
14 if (req.method !== 'POST') {
15 return new Response('Method not allowed', { status: 405 })
16 }
17
18 const body = await req.text()
19 const signature = req.headers.get('stripe-signature') ?? ''
20 const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET') ?? ''
21
22 let event: Stripe.Event
23 try {
24 event = await stripe.webhooks.constructEventAsync(
25 body,
26 signature,
27 webhookSecret,
28 undefined,
29 cryptoProvider
30 )
31 } catch (err) {
32 const msg = err instanceof Error ? err.message : 'Webhook signature failed'
33 console.error('Webhook verification failed:', msg)
34 return new Response(JSON.stringify({ error: msg }), { status: 400 })
35 }
36
37 const supabase = createClient(
38 Deno.env.get('SUPABASE_URL') ?? '',
39 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
40 )
41
42 if (event.type === 'payment_intent.succeeded') {
43 const pi = event.data.object as Stripe.PaymentIntent
44 await supabase
45 .from('payments')
46 .update({ status: 'succeeded', updated_at: new Date().toISOString() })
47 .eq('stripe_payment_intent_id', pi.id)
48 }
49
50 if (event.type === 'payment_intent.payment_failed') {
51 const pi = event.data.object as Stripe.PaymentIntent
52 await supabase
53 .from('payments')
54 .update({ status: 'failed', updated_at: new Date().toISOString() })
55 .eq('stripe_payment_intent_id', pi.id)
56 }
57
58 return new Response(JSON.stringify({ received: true }), { status: 200 })
59})

Pro tip: Register this webhook URL in the Stripe Dashboard → Webhooks → Add endpoint AFTER deploying your app. The URL is https://your-project.supabase.co/functions/v1/stripe-webhook. Select the payment_intent.succeeded and payment_intent.payment_failed events.

Expected result: A test webhook event sent from the Stripe Dashboard shows a 200 response. The corresponding payment row in Supabase updates from pending to succeeded.

5

Build the payment history dashboard

Ask Lovable to create a transaction history page with filtering, status badges, and a revenue chart using Recharts.

prompt.txt
1Build a payment history page at src/pages/Payments.tsx.
2
3Requirements:
4- Fetch the current user's payments from Supabase joined with products (to show product name)
5- Render as a shadcn/ui DataTable with columns: date (formatted), product name, amount (currency formatted), status (Badge: succeeded=green, pending=yellow, failed=red), Stripe ID (truncated monospace)
6- Add a date range Popover filter (last 7 days / last 30 days / all time)
7- Add a status Select filter (all / succeeded / pending / failed)
8- Above the table, show three summary Cards: Total Revenue (sum of succeeded), Pending Amount, Failed Transactions count
9- Add a BarChart using Recharts showing daily revenue for the last 30 days, grouped by date
10- Show an empty state illustration with 'No payments yet' when the table is empty

Expected result: The payments page shows all transactions for the logged-in user. Filters update the table in real time. The revenue chart shows bars for each day with completed payments.

Complete code

supabase/functions/create-payment-intent/index.ts
1import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
2import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
3
4const corsHeaders = {
5 'Access-Control-Allow-Origin': '*',
6 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
7 'Content-Type': 'application/json',
8}
9
10serve(async (req: Request) => {
11 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
12
13 try {
14 const { productId, customerEmail } = await req.json()
15 const stripeKey = Deno.env.get('STRIPE_SECRET_KEY') ?? ''
16
17 const supabase = createClient(
18 Deno.env.get('SUPABASE_URL') ?? '',
19 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
20 )
21
22 const { data: product, error: productError } = await supabase
23 .from('products')
24 .select('id, name, price_cents, currency')
25 .eq('id', productId)
26 .eq('is_active', true)
27 .single()
28
29 if (productError || !product) {
30 return new Response(JSON.stringify({ error: 'Product not found' }), { status: 404, headers: corsHeaders })
31 }
32
33 const customerRes = await fetch('https://api.stripe.com/v1/customers', {
34 method: 'POST',
35 headers: {
36 Authorization: `Basic ${btoa(stripeKey + ':')}`,
37 'Content-Type': 'application/x-www-form-urlencoded',
38 },
39 body: new URLSearchParams({ email: customerEmail }),
40 })
41 const customer = await customerRes.json()
42
43 const piRes = await fetch('https://api.stripe.com/v1/payment_intents', {
44 method: 'POST',
45 headers: {
46 Authorization: `Basic ${btoa(stripeKey + ':')}`,
47 'Content-Type': 'application/x-www-form-urlencoded',
48 },
49 body: new URLSearchParams({
50 amount: String(product.price_cents),
51 currency: product.currency,
52 customer: customer.id,
53 'metadata[productId]': product.id,
54 }),
55 })
56 const pi = await piRes.json()
57
58 await supabase.from('payments').insert({
59 stripe_payment_intent_id: pi.id,
60 amount: product.price_cents,
61 currency: product.currency,
62 status: 'pending',
63 metadata: { productId: product.id, productName: product.name },
64 })
65
66 return new Response(JSON.stringify({ clientSecret: pi.client_secret }), { headers: corsHeaders })
67 } catch (err) {
68 const message = err instanceof Error ? err.message : 'Internal error'
69 return new Response(JSON.stringify({ error: message }), { status: 500, headers: corsHeaders })
70 }
71})

Customization ideas

Multiple currency support

Add a currency_code column to your products table and a currency selector on the product card. Pass the selected currency to the create-payment-intent Edge Function. Stripe handles currency conversion automatically — just ensure the amount is in the smallest unit for each currency (cents for USD, pence for GBP, yen for JPY which has no sub-units).

Payment receipts via email

Add a step in the webhook handler: when payment_intent.succeeded fires, call Resend (or another email API) from the Edge Function to send a receipt email. Include the product name, amount paid, date, and a Stripe receipt URL from the PaymentIntent object's receipt_url field.

Saved payment methods

Extend the checkout flow to offer 'Save card for future payments' using Stripe SetupIntents. Add a payment_methods table in Supabase and a Saved Cards section in the customer portal. On checkout, let returning customers select a saved card instead of entering details again.

Admin revenue analytics

Add an admin-only page that shows all payments across all users (using service role queries). Include a Recharts AreaChart for cumulative revenue, a PieChart for revenue by product, and a top customers table. Protect this route with a role check against a user_roles table.

Refund workflow

Add a Refund button to each succeeded payment in the admin dashboard. The button calls a refund Edge Function that posts to Stripe's refund API, then updates the payment status in Supabase to refunded. Show refund status in the DataTable with a yellow Badge.

Common pitfalls

Pitfall: Testing Stripe in the Lovable preview instead of deployed mode

How to avoid: Click the Publish icon (top-right) to deploy your app before testing any payment flows. Use Stripe test mode keys (pk_test_... and sk_test_...) during development so no real charges occur.

Pitfall: Using constructEvent instead of constructEventAsync in Deno

How to avoid: Always use stripe.webhooks.constructEventAsync with a Stripe.createSubtleCryptoProvider() as the fourth argument. This uses the Web Crypto API which is available in Deno.

Pitfall: Reading the request body as JSON before passing it to constructEventAsync

How to avoid: Always read the body as text first: const body = await req.text(). Pass this string to constructEventAsync. Never call req.json() before webhook verification.

Pitfall: Not handling idempotency in the webhook handler

How to avoid: Add a webhook_events table with a stripe_event_id unique column. At the start of the webhook handler, INSERT the event ID. If the INSERT fails with a unique constraint error, return 200 immediately without processing — the event was already handled.

Best practices

  • Always create PaymentIntents server-side in an Edge Function. Never pass your Stripe secret key to the frontend or create payment intents from client-side code.
  • Use Stripe test mode during all development and staging work. Switch to live keys only when deploying to production, and keep them in a separate set of Secrets.
  • Verify every incoming webhook with constructEventAsync before trusting the payload. An unverified webhook could be forged to mark payments as succeeded without actual payment.
  • Store stripe_payment_intent_id in your Supabase payments table as the canonical reference. Always reconcile payment status from Stripe webhooks, not from the client-side payment confirmation callback.
  • Add a Stripe idempotency key to PaymentIntent creation requests when retrying. This prevents duplicate charges if the network fails between your Edge Function and Stripe.
  • Use Stripe's automatic payment methods feature (automatic_payment_methods: { enabled: true }) to accept Apple Pay, Google Pay, and local payment methods without extra code.
  • Enable Stripe Radar fraud rules in your Stripe Dashboard to block suspicious payments before they reach your webhook handler. This reduces chargebacks with zero code changes.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a Stripe payment integration in a Lovable app using Supabase Edge Functions on Deno. I need to create a PaymentIntent server-side and verify webhooks. Explain the difference between constructEvent and constructEventAsync and why I must use the async version in Deno. Also show me how to call the Stripe REST API directly using fetch with Basic Auth instead of the Stripe Node SDK, since Deno handles fetch natively.

Lovable Prompt

Add a payment success page at /payment-success. Read the payment_intent query parameter from the URL (Stripe redirects here after confirmation). Call a Supabase Edge Function with that payment_intent ID to fetch the payment details. Show a shadcn/ui Card with a green checkmark icon, the product name, amount paid, and date. Add a 'View All Payments' Button linking to /payments and a 'Back to Products' Button. Handle the case where the payment is still pending by showing a Skeleton and polling every 2 seconds until status becomes succeeded.

Build Prompt

In Supabase, create a scheduled Edge Function that runs daily at midnight. It should query your payments table for rows where status = 'pending' AND created_at < now() - interval '24 hours'. For each one, call the Stripe API to check the actual PaymentIntent status and update your Supabase row accordingly. This reconciles any payments where the webhook was missed. Return a summary JSON with counts of updated and unchanged records.

Frequently asked questions

Why does Stripe not work in the Lovable preview?

Stripe.js requires a real HTTPS domain to load and the PaymentIntent confirmation requires a return_url pointing to your deployed app. The Lovable preview iframe runs on a temporary subdomain and does not fully support Stripe's browser security requirements. Always test Stripe flows after clicking Publish.

What is the difference between a PaymentIntent and a Checkout Session?

A PaymentIntent gives you full control over the checkout UI by embedding Stripe Elements directly in your page. A Checkout Session redirects users to a Stripe-hosted page with less customization. PaymentIntents are more complex to implement but give you a seamless, branded experience. Checkout Sessions are faster to build. This guide uses PaymentIntents for the embedded experience.

How do I switch from Stripe test mode to live mode?

Replace the test keys in Cloud tab → Secrets with your live Stripe keys: VITE_STRIPE_PUBLISHABLE_KEY becomes pk_live_... and STRIPE_SECRET_KEY becomes sk_live_.... Also update the STRIPE_WEBHOOK_SECRET to the live webhook's signing secret from the Stripe Dashboard. Republish your app after updating Secrets.

Can I accept payments in multiple currencies?

Yes. Pass the currency code to the create-payment-intent Edge Function and store it on the payment record. Stripe automatically formats amounts for each currency. Remember that some currencies like JPY (Japanese yen) do not have sub-units, so pass the amount in whole units rather than cents for those currencies.

What happens if a webhook is not delivered?

Stripe retries failed webhook deliveries for up to 72 hours with exponential backoff. If your Edge Function is down or returns a non-2xx status, Stripe will retry. You can also replay specific events from the Stripe Dashboard → Webhooks → Event log. Build a daily reconciliation job as a backup to catch any permanently missed events.

How do I issue a refund?

Call the Stripe Refunds API from an Edge Function with the payment_intent_id or charge_id. POST to https://api.stripe.com/v1/refunds with amount and payment_intent as form data using Basic Auth with your secret key. After a successful refund, update the payment row status in Supabase to refunded. Stripe also sends a charge.refunded webhook event you can listen for.

Is there help available if I need a more complex payment integration?

RapidDev specializes in building production payment systems in Lovable, including multi-vendor payouts, subscription billing, and complex checkout flows. Contact us if you need custom payment architecture beyond what this guide covers.

How do I test webhooks locally before deploying?

Lovable does not support local development. The recommended approach is to deploy to a staging version of your app, register the staging Edge Function URL in your Stripe webhook configuration, and test using the Stripe Dashboard's 'Send test webhook' button. You can also use the Stripe CLI to forward events to a URL if you have a separate dev environment.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.