Feature spec
IntermediateCategory
payments-commerce
Build with AI
3–6 hours with Lovable or V0
Custom build
1–2 weeks custom dev
Running cost
$0 until first charge; then 2.9% + $0.30 per transaction
Works on
Everything it takes to ship a Payment Gateway — parts, prompts, and real costs.
A payment gateway needs five pieces: Stripe Elements or Checkout for the UI, a server-side Payment Intent handler, a webhook listener for fulfillment, an orders table in Supabase, and a confirmation email. With Lovable or V0 you can ship a working checkout in 3–6 hours. Running costs start near $0 and scale with transaction volume — Stripe charges 2.9% + $0.30 per charge, nothing until you earn.
What a Payment Gateway Feature Actually Is
A payment gateway is the infrastructure that moves money from your customer's card or wallet to your bank account. In 2026 that means Stripe: it handles card tokenization, Apple Pay and Google Pay, 3D Secure authentication for EU cards, and fraud detection — without your server ever touching raw card numbers. The real product decisions are what happens around the payment: how you create the Payment Intent server-side, how your webhook listener fulfills orders reliably, and how you handle the edge cases that generate support tickets (declined cards, incomplete payments, duplicate webhook delivery). Get those right and the checkout just works.
What users consider table stakes in 2026
- Stripe Elements or Stripe Checkout embedded on the page — no unexpected full-page redirects before the user decides to pay
- Apple Pay and Google Pay one-tap buttons visible on mobile browsers and Safari without any extra configuration
- Real-time card validation feedback as the user types — card number, expiry, and CVC errors shown inline before the submit button is pressed
- Clear, human-readable error messages for declined cards with a specific retry prompt (not just 'payment failed')
- Order confirmation screen shown immediately after payment with a receipt email arriving within 60 seconds
- Payment history accessible inside the user's account settings showing amount, date, status, and a link to the Stripe receipt
Anatomy of the Feature
Five components, two of which AI tools routinely get wrong. The webhook listener and idempotency logic are where first builds break under real traffic.
Checkout UI
UIStripe Elements renders the card field, Apple Pay and Google Pay buttons, and the 3DS confirmation modal directly in your page. Stripe Checkout is an alternative hosted page Stripe controls — lower customization, faster to ship.
Note: Stripe Elements requires HTTPS and will not initialize on http://. Test on your published URL, not localhost or a Lovable preview iframe.
Payment Intent handler
BackendA Supabase Edge Function or Next.js API route calls stripe.paymentIntents.create() server-side with the amount in cents, currency, metadata (user_id, order_id), and optionally a Stripe Customer ID for saved payment methods. The client secret returned is passed to Stripe Elements to complete the payment on the client.
Note: Never create Payment Intents client-side — amount and currency can be manipulated. Always server-side.
Webhook listener
BackendAn Edge Function or API route receives Stripe events (payment_intent.succeeded, payment_intent.payment_failed, charge.refunded) and updates your orders table accordingly. Uses stripe.webhooks.constructEvent() with the raw request body and your webhook signing secret.
Note: This is the most failure-prone component. Read the raw body as Uint8Array before any JSON parsing — parsing first mangles the signature and breaks verification.
Orders table
DataSupabase PostgreSQL stores order_id, user_id, stripe_payment_intent_id, stripe_customer_id, amount_cents, currency, status (pending / paid / failed / refunded), metadata jsonb, and created_at. Row-level security restricts each row to its owner.
Note: Add a UNIQUE index on stripe_payment_intent_id — prevents duplicate fulfillment if your webhook handler is invoked twice for the same event.
Confirmation and receipt
UIA post-payment success screen shows order details, then a Supabase Edge Function triggers a transactional receipt email via Resend or SendGrid. Email is triggered by the payment_intent.succeeded webhook, not the client redirect — so it fires even if the user closes the tab before the redirect completes.
Note: Trigger email from the webhook, not the success URL redirect. Client can disappear; webhook always fires.
The data model
Two tables cover the full payment flow. Run this in the Supabase SQL editor after connecting your project:
1create table public.orders (2 id uuid primary key default gen_random_uuid(),3 user_id uuid references auth.users(id) on delete set null,4 stripe_payment_intent_id text unique not null,5 stripe_customer_id text,6 amount_cents integer not null check (amount_cents >= 50),7 currency text not null default 'usd',8 status text not null default 'pending'9 check (status in ('pending', 'paid', 'failed', 'refunded')),10 metadata jsonb default '{}',11 created_at timestamptz not null default now(),12 updated_at timestamptz not null default now()13);1415alter table public.orders enable row level security;1617create policy "Users can view own orders"18 on public.orders for select19 using (auth.uid() = user_id);2021create table public.stripe_customers (22 id uuid primary key default gen_random_uuid(),23 user_id uuid references auth.users(id) on delete cascade not null unique,24 stripe_customer_id text not null unique,25 created_at timestamptz not null default now()26);2728alter table public.stripe_customers enable row level security;2930create policy "Users can view own Stripe customer record"31 on public.stripe_customers for select32 using (auth.uid() = user_id);3334create index orders_user_id_created_idx35 on public.orders (user_id, created_at desc);3637create index orders_payment_intent_idx38 on public.orders (stripe_payment_intent_id);Heads up: The UNIQUE constraint on stripe_payment_intent_id is what makes your webhook handler idempotent — a second delivery of the same event hits the constraint and does nothing instead of double-fulfilling.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Best all-round path — Lovable's native Stripe connector generates the Edge Functions, Supabase schema, and auth wiring in one project. Deploy once and the webhooks work.
Step by step
- 1Create a new Lovable project, open Cloud tab, enable Supabase — this provisions the database and auth
- 2Go to Cloud tab → Connectors → Stripe and connect your Stripe account (test mode first)
- 3Paste the prompt below in Agent Mode and let it generate the checkout page, Edge Functions, and orders table
- 4Click Publish — the Stripe connector only works on the published URL, not the in-editor preview iframe
- 5In the Stripe Dashboard, add your published URL as a webhook endpoint and copy the signing secret into Lovable Cloud → Secrets as STRIPE_WEBHOOK_SECRET
- 6Test with card 4242 4242 4242 4242, expiry 12/34, any CVC; confirm the order row appears in Supabase Table Editor
Build a complete payment checkout feature using Stripe. Use the Stripe native connector. Create a checkout page with Stripe Elements for the card field, Apple Pay, and Google Pay. On form submit, call an Edge Function that creates a Stripe Payment Intent server-side with amount in cents, currency 'usd', and metadata including the current user_id. Return the client_secret to the browser and confirm payment with stripe.confirmCardPayment(). After payment, show a confirmation screen with the order total and a 'View order history' link. Create a Stripe webhook Edge Function that handles payment_intent.succeeded (set order status to 'paid', trigger a confirmation email via Resend), payment_intent.payment_failed (set status to 'failed', show a toast with the decline reason and a retry button). Verify the webhook signature using constructEventAsync() with the raw Uint8Array body — never JSON.parse before verification. Check if the order already has status 'paid' before updating to prevent duplicate fulfillment. Use test card 4242 4242 4242 4242 for development. Handle 3DS cards (authenticate) by listening for the requires_action state. Scaffold two Supabase Secrets: STRIPE_SECRET_KEY for the server and STRIPE_WEBHOOK_SECRET for webhook verification. Include an order history page showing each order's amount, status, date, and a link to the Stripe-hosted receipt URL.
Where this path bites
- Stripe Checkout redirect and Stripe Elements do not function inside the Lovable in-editor preview iframe — always test on the published URL
- Webhook testing requires your published domain — Stripe cannot deliver events to localhost
- Switching from test to live mode requires a new webhook endpoint with a new signing secret; prompt to scaffold both as separate Supabase Secrets
- Stripe Customer saved payment methods require additional Edge Function logic beyond the default connector setup
Third-party services you'll need
Three services cover the full payment stack. Stripe is mandatory; Resend and Supabase scale gracefully on free tiers for early-stage products.
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| Stripe | Core payment processing: card acceptance, Apple/Google Pay, 3DS, fraud detection (Radar), webhooks | Free to sign up; test mode unlimited | 2.9% + $0.30 per charge; Radar fraud: $0.05/charge (approx); Stripe Billing adds 0.5–0.8% for subscriptions |
| Resend | Transactional receipt emails triggered by the payment_intent.succeeded webhook | 100 emails/day free | $20/mo (50K emails) (approx) |
| Supabase | PostgreSQL orders table + RLS + Edge Functions for Payment Intent creation and webhook handling | Free (2 projects) | Pro $25/mo |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
Stripe fees on purchase volume ($3–10 depending on average order value) + Supabase free tier + Resend free tier. Negligible fixed cost.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Webhook fires before the browser redirect completes — double fulfillment
Symptom: Stripe fires payment_intent.succeeded almost instantly after the card is charged — often before the browser has redirected to your success page. If your AI-generated handler marks an order fulfilled, and your success page also updates the order, you get a race condition or duplicate email. Worse: Stripe retries webhooks on any 5xx response, so a crashing handler can trigger multiple fulfillments.
Fix: Use Stripe's idempotency key on Payment Intent creation (set idempotency_key: order_id). In the webhook handler, check the order's current status before updating — only transition from 'pending' to 'paid', never overwrite 'paid'. Add a UNIQUE index on stripe_payment_intent_id so a second INSERT fails gracefully.
Preview iframe blocks Stripe Checkout redirect — payment appears to hang
Symptom: In the Lovable in-editor preview and the V0 sandbox, Stripe Checkout's redirect opens inside a sandboxed iframe. The iframe security policy blocks the redirect, so the checkout page appears to freeze. This is often mistaken for a code bug and triggers hours of debugging.
Fix: Always test payment flows on your published Lovable URL or Vercel deployment. Use the Stripe test card 4242 4242 4242 4242 on the live HTTPS URL — camera and payment APIs both require a real browser context outside iframes.
Webhook signature verification fails — 400 errors on every event
Symptom: Stripe signs webhook payloads with an HMAC using the raw request body bytes. If your handler calls JSON.parse() or response.json() before passing the body to stripe.webhooks.constructEvent(), the byte representation changes and the signature check always fails. This is the most common payment integration bug AI tools produce.
Fix: In your Edge Function, read the request body as raw bytes: const body = await request.arrayBuffer(); const rawBody = new Uint8Array(body). Pass rawBody directly to constructEventAsync(). Never await request.json() before this step. Add a log line that prints the first 20 bytes to confirm you have the raw body before verification.
Live vs test mode mismatch — test orders appear in production
Symptom: AI tools build with Stripe test keys (sk_test_...). When you switch to live mode, Stripe issues separate webhook signing secrets for each endpoint — the test webhook secret does not verify live events. Apps that use a single STRIPE_SECRET environment variable silently mix test and live charges.
Fix: Scaffold two Supabase Secret entries from the start: STRIPE_SECRET_KEY (initially test, swap to live at launch) and STRIPE_WEBHOOK_SECRET (one per webhook endpoint registration). Add a STRIPE_ENV flag ('test'|'live') your app reads to show an admin banner when running in test mode.
'Cannot make live charges' error after switching keys
Symptom: Stripe requires business details — legal name, address, bank account, and identity verification — before activating a live account. An app built with test keys that switches to live keys without completing Stripe Dashboard onboarding throws this error on every charge attempt. Users who encounter this see a generic payment failure.
Fix: Before launching, complete Stripe Dashboard onboarding fully. In your admin panel, call stripe.accounts.retrieve() and check capabilities.card_payments.status === 'active'. Show a prominent 'Complete Stripe setup' banner to the account owner until this check passes.
Best practices
Always create Payment Intents server-side — never expose your Stripe secret key to the browser or allow amounts to be set client-side
Use Stripe's idempotency keys on Payment Intent creation so retried requests return the same intent instead of charging twice
Make your webhook handler idempotent: check the order's current status before updating, and use INSERT ... ON CONFLICT DO NOTHING for receipt records
Test every decline scenario before launch: 4000000000000002 (generic decline), 4000000000009995 (insufficient funds), 4000000000009987 (lost card) — each produces a different decline_code your UI should handle specifically
Store the Stripe receipt URL (charge.receipt_url) in your orders table so users can access it from their account without you building a PDF generator
Trigger receipt emails from the webhook handler, not the success URL redirect — the client can close the tab before the redirect fires
Display proration previews before plan upgrades using stripe.invoices.retrieveUpcoming() — surprise charges at upgrade are the top cause of subscription cancellations
Scaffold two separate Stripe API key secrets (test and live) from the beginning; retrofitting this after launch is painful and error-prone
When You Need Custom Development
AI tools handle standard checkout well. A few payment architectures genuinely require custom development:
- Multi-vendor marketplace where each seller receives a separate payout via Stripe Connect — the split payment logic, platform fee calculation, and seller onboarding flow require bespoke Edge Function design
- Volume-based or seat-based pricing with proration — Stripe Billing's schedule phases and pending invoice items need careful custom implementation to behave predictably
- PCI DSS compliance audit required by an enterprise client — this mandates architecture review and documentation AI tools cannot produce
- Complex partial refund workflows with a role-based approval chain before the Stripe refund API is called
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
Does Stripe work in all countries?
Stripe supports businesses in 46 countries and accepts cards from customers worldwide. The main limitation is where you, the business owner, can register — not where your customers are located. Check stripe.com/global for the current country list. If your country is not supported, Stripe Atlas can help you incorporate in the US.
How do I switch from test to live mode safely?
Create a new webhook endpoint in the Stripe Dashboard pointing to your production URL and copy the new signing secret to your STRIPE_WEBHOOK_SECRET environment variable. Swap STRIPE_SECRET_KEY from sk_test_ to sk_live_. Test orders from test mode do not appear in live mode — you start with a clean slate. Complete Stripe Dashboard business verification before switching or all live charges will be declined.
What happens if a payment fails midway — is the user charged?
If the charge fails — declined card, failed 3DS, or network error — no money moves. A Payment Intent in the 'requires_payment_method' state has not captured any funds. If a payment succeeds and you later refund it, Stripe returns the principal but keeps the transaction fee (2.9% + $0.30). The user sees no charge on their statement for a failed payment.
Can I save cards for repeat purchases without PCI liability?
Yes. Create or retrieve a Stripe Customer object for each user and save their stripe_customer_id in your database. Stripe stores the card; your server only stores the customer ID. On subsequent purchases, pass the stripe_customer_id to payment_intent.create() and Stripe presents saved payment methods. You never touch raw card data, so PCI scope is minimal.
How do refunds work — automatic or manual?
Refunds are manual by default — you call stripe.refunds.create() with the payment_intent_id or charge_id, optionally with an amount for partial refunds. Stripe then pushes a charge.refunded webhook event. Funds return to the customer's card in 5–10 business days. Build an admin screen to trigger refunds; do not expose the refund action to end users directly.
What's the difference between Stripe Checkout and Stripe Elements?
Stripe Checkout is a Stripe-hosted page — fastest to implement, zero UI code, handles 3DS and Apple/Google Pay automatically, but limited visual customization. Stripe Elements embeds individual fields directly in your page, giving you full control over the layout and user experience. For most apps, Elements with the prebuilt Payment Element is the right balance: fully embedded, supports all payment methods, and handles 3DS without a redirect.
Can I accept Apple Pay and Google Pay?
Yes, with no extra configuration when using Stripe Elements or the Payment Element — Stripe detects the browser and shows the wallet button automatically. Apple Pay requires your domain to be registered in the Stripe Dashboard (Stripe generates a verification file you serve from your domain). Google Pay works on any HTTPS page in Chrome on Android. Both require your Stripe account to be in live mode for real transactions.
How do I handle 3D Secure for European cards?
Stripe handles 3DS automatically when you use stripe.confirmPayment() with the Payment Element. If the card requires authentication, Stripe triggers the 3DS modal without any extra code. For manual Payment Intent flows, listen for the requires_action status and call stripe.handleNextAction(clientSecret) — Stripe opens the bank's authentication iframe and resolves the promise when done.
Need this feature production-ready?
RapidDev builds a payment gateway into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.