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
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
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.
1Create a Stripe payment integration with this Supabase schema:23Tables: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_at5- customers: id (uuid pk), user_id (references auth.users), email (text), stripe_customer_id (text unique), created_at6- 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_at78RLS policies:9- products: public SELECT for is_active=true, authenticated INSERT/UPDATE for admin role10- customers: users can SELECT/UPDATE their own record (user_id = auth.uid())11- payments: users can SELECT their own payments (user_id = auth.uid())1213Generate 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.
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.
1Create a Supabase Edge Function at supabase/functions/create-payment-intent/index.ts.23The function should:41. Parse the request body: { productId: string, customerEmail: string }52. Look up the product in Supabase to get price_cents and name63. Look up or create a Stripe customer using STRIPE_SECRET_KEY from Deno.env74. 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.id96. Return { clientSecret: paymentIntent.client_secret }1011Include 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).1213Also 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.
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.
1Build a product catalog page at src/pages/Products.tsx.23Requirements:4- Fetch all is_active products from Supabase5- Display each as a shadcn/ui Card with: name, description, price formatted as currency, and a 'Buy Now' Button6- Clicking Buy Now opens a Dialog7- Inside the Dialog:8 1. Call the create-payment-intent Edge Function to get a clientSecret9 2. Show a loading Skeleton while the Edge Function responds10 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 form12 5. Add a 'Pay' Button that calls stripe.confirmPayment with return_url pointing to /payment-success13- Install @stripe/react-stripe-js and @stripe/stripe-js via the package manager14- Handle loading, error, and success states with appropriate shadcn/ui Alert componentsPro 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.
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.
1// supabase/functions/stripe-webhook/index.ts2import { 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'56const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') ?? '', {7 apiVersion: '2023-10-16',8 httpClient: Stripe.createFetchHttpClient(),9})1011const cryptoProvider = Stripe.createSubtleCryptoProvider()1213serve(async (req: Request) => {14 if (req.method !== 'POST') {15 return new Response('Method not allowed', { status: 405 })16 }1718 const body = await req.text()19 const signature = req.headers.get('stripe-signature') ?? ''20 const webhookSecret = Deno.env.get('STRIPE_WEBHOOK_SECRET') ?? ''2122 let event: Stripe.Event23 try {24 event = await stripe.webhooks.constructEventAsync(25 body,26 signature,27 webhookSecret,28 undefined,29 cryptoProvider30 )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 }3637 const supabase = createClient(38 Deno.env.get('SUPABASE_URL') ?? '',39 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''40 )4142 if (event.type === 'payment_intent.succeeded') {43 const pi = event.data.object as Stripe.PaymentIntent44 await supabase45 .from('payments')46 .update({ status: 'succeeded', updated_at: new Date().toISOString() })47 .eq('stripe_payment_intent_id', pi.id)48 }4950 if (event.type === 'payment_intent.payment_failed') {51 const pi = event.data.object as Stripe.PaymentIntent52 await supabase53 .from('payments')54 .update({ status: 'failed', updated_at: new Date().toISOString() })55 .eq('stripe_payment_intent_id', pi.id)56 }5758 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.
Build the payment history dashboard
Ask Lovable to create a transaction history page with filtering, status badges, and a revenue chart using Recharts.
1Build a payment history page at src/pages/Payments.tsx.23Requirements: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 count9- Add a BarChart using Recharts showing daily revenue for the last 30 days, grouped by date10- Show an empty state illustration with 'No payments yet' when the table is emptyExpected 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
1import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'2import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'34const corsHeaders = {5 'Access-Control-Allow-Origin': '*',6 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',7 'Content-Type': 'application/json',8}910serve(async (req: Request) => {11 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })1213 try {14 const { productId, customerEmail } = await req.json()15 const stripeKey = Deno.env.get('STRIPE_SECRET_KEY') ?? ''1617 const supabase = createClient(18 Deno.env.get('SUPABASE_URL') ?? '',19 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''20 )2122 const { data: product, error: productError } = await supabase23 .from('products')24 .select('id, name, price_cents, currency')25 .eq('id', productId)26 .eq('is_active', true)27 .single()2829 if (productError || !product) {30 return new Response(JSON.stringify({ error: 'Product not found' }), { status: 404, headers: corsHeaders })31 }3233 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()4243 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()5758 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 })6566 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.
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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation