Skip to main content
RapidDev - Software Development Agency

How to Build a Marketplace with Lovable

Build a full two-sided marketplace in Lovable where sellers list products and buyers place orders. You get seller onboarding with Stripe Connect, split payment disbursements, Supabase Storage for listing images, a Command palette search, and a complete order lifecycle — all without writing backend code from scratch.

What you'll build

  • Seller onboarding flow with Stripe Connect OAuth and Supabase seller profile creation
  • Listing creation form with multi-image upload to Supabase Storage, category, price, and stock
  • Buyer-facing storefront with Command palette search and faceted category filtering
  • Shopping cart and checkout that creates a Stripe PaymentIntent splitting funds to the seller
  • Order management dashboard for both buyers (purchase history) and sellers (incoming orders)
  • Review and rating system locked to verified purchasers with aggregate star display
  • Admin moderation panel to approve listings and suspend seller accounts
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced17 min read5–7 hoursLovable Pro or higherApril 2026RapidDev Engineering Team
TL;DR

Build a full two-sided marketplace in Lovable where sellers list products and buyers place orders. You get seller onboarding with Stripe Connect, split payment disbursements, Supabase Storage for listing images, a Command palette search, and a complete order lifecycle — all without writing backend code from scratch.

What you're building

A two-sided marketplace has three actor types: buyers, sellers, and admins. The database schema centers on a sellers table that extends auth.users with Stripe Connect account IDs and verification status. Each seller owns listings — products with images stored in Supabase Storage, price, stock, and a category.

Buyers browse listings using a shadcn/ui Command palette for instant search, or drill into categories. Adding to cart and checking out triggers a Supabase Edge Function that creates a Stripe PaymentIntent with an application_fee_amount so your platform takes its cut while the remainder routes to the seller's connected account.

Orders flow through status states (pending → paid → shipped → delivered → disputed). Sellers see their pending orders in a dashboard and mark them shipped. Buyers confirm delivery, which releases any held funds. Reviews can only be written after an order reaches the delivered state, preventing fake reviews.

Final result

A production-ready marketplace with real seller payouts, image uploads, search, order lifecycle management, and verified buyer reviews.

Tech stack

LovableFrontend
SupabaseDatabase, Auth, Storage, Edge Functions
Stripe ConnectSplit payments and seller payouts
shadcn/uiUI Components
react-hook-form + zodForms
Tailwind CSSStyling

Prerequisites

  • Lovable Pro account (Stripe Connect Edge Functions are credit-intensive to generate)
  • Supabase project created at supabase.com — free tier works for development
  • Stripe account with Connect enabled and your platform's client_id from the Stripe Dashboard
  • Stripe secret key and webhook signing secret added to Cloud tab → Secrets
  • A rough list of product categories your marketplace will support

Build steps

1

Define the full database schema

Start with a single comprehensive schema prompt. Getting the schema right at this stage saves significant rework. The core tables are sellers, listings, listing_images, orders, order_items, and reviews. RLS must be defined at this stage.

prompt.txt
1Create a two-sided marketplace app with Supabase. Set up these tables:
2
3- sellers: id (uuid, references auth.users), stripe_account_id (text), stripe_onboarding_complete (bool default false), display_name, bio, avatar_url, commission_rate (numeric default 0.10), status (pending|approved|suspended), created_at
4- listings: id, seller_id (references sellers), title, description, price (numeric), stock_quantity (int), category (text), status (draft|active|paused|sold_out), created_at, updated_at
5- listing_images: id, listing_id (references listings), storage_path (text), position (int), is_primary (bool default false)
6- orders: id, buyer_id (references auth.users), seller_id (references sellers), status (pending|paid|shipped|delivered|disputed|refunded), stripe_payment_intent_id (text), subtotal (numeric), platform_fee (numeric), seller_payout (numeric), shipping_address (jsonb), created_at, updated_at
7- order_items: id, order_id, listing_id, quantity, unit_price
8- reviews: id, listing_id, order_id (unique one review per order), reviewer_id (references auth.users), rating (int 1-5), body (text), created_at
9
10RLS:
11- sellers: users can read any approved seller, can insert/update their own seller row
12- listings: anyone can read active listings, sellers can CRUD their own listings
13- listing_images: same as listings
14- orders: buyers see their own orders (buyer_id = auth.uid()), sellers see orders where seller_id matches their seller row
15- reviews: anyone can read, only verified buyers can insert (enforce via Edge Function check)
16
17Create a Supabase Storage bucket called 'listings' (public reads, authenticated uploads).

Pro tip: Ask Lovable to add a listings_search_vector tsvector column computed from title and description, and create a GIN index on it. This enables fast full-text search without an external search service.

Expected result: All seven tables are created with proper foreign keys and RLS. The listings bucket appears in Supabase Storage. TypeScript types are generated for all tables.

2

Build seller onboarding with Stripe Connect

Sellers must connect their Stripe account before they can receive payouts. This requires an Edge Function that generates the Stripe Connect OAuth URL and another that handles the OAuth callback to store the connected account ID.

prompt.txt
1Create two Supabase Edge Functions:
2
31. supabase/functions/stripe-connect-onboard/index.ts
4Generates a Stripe Connect OAuth URL.
5Receives: { return_url: string } in body.
6Logic: call Stripe OAuth authorize URL with client_id from Deno.env.get('STRIPE_CONNECT_CLIENT_ID'), scope=read_write, state=user JWT sub claim, redirect_uri pointing to the callback function URL.
7Returns: { url: string }
8
92. supabase/functions/stripe-connect-callback/index.ts
10Handles the OAuth redirect from Stripe.
11Receives: code and state query params.
12Logic:
13- Exchange code for access_token and stripe_user_id via Stripe's token endpoint
14- Decode state to get user_id
15- Upsert sellers row: { id: user_id, stripe_account_id: stripe_user_id, stripe_onboarding_complete: true }
16- Redirect browser to /seller/dashboard?connected=true
17
18Store STRIPE_SECRET_KEY and STRIPE_CONNECT_CLIENT_ID in Cloud tab Secrets.

Pro tip: After the seller connects their Stripe account, also create their seller record with status='pending' and send an admin notification. This gives you a manual review step before listings go live, preventing fraud from day one.

Expected result: Clicking 'Become a Seller' redirects to Stripe, connects the account, and returns the user to /seller/dashboard with a success Banner. The sellers table row is created with the Stripe account ID.

3

Build the listing creation form with image upload

Sellers create listings with a multi-field form and drag-and-drop image upload. Images go to Supabase Storage. The primary image path is stored in listing_images with is_primary = true.

src/components/seller/CreateListingForm.tsx
1import { useCallback, useState } from 'react'
2import { useForm } from 'react-hook-form'
3import { zodResolver } from '@hookform/resolvers/zod'
4import { z } from 'zod'
5import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
6import { Input } from '@/components/ui/input'
7import { Textarea } from '@/components/ui/textarea'
8import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
9import { Button } from '@/components/ui/button'
10import { supabase } from '@/integrations/supabase/client'
11
12const schema = z.object({
13 title: z.string().min(5).max(120),
14 description: z.string().min(20).max(2000),
15 price: z.coerce.number().positive(),
16 stock_quantity: z.coerce.number().int().min(0),
17 category: z.string().min(1),
18})
19
20type ListingFormValues = z.infer<typeof schema>
21
22export function CreateListingForm({ sellerId }: { sellerId: string }) {
23 const [images, setImages] = useState<File[]>([])
24 const [uploading, setUploading] = useState(false)
25 const form = useForm<ListingFormValues>({ resolver: zodResolver(schema) })
26
27 const onDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
28 e.preventDefault()
29 const files = Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith('image/'))
30 setImages((prev) => [...prev, ...files].slice(0, 8))
31 }, [])
32
33 const onSubmit = async (values: ListingFormValues) => {
34 setUploading(true)
35 const { data: listing } = await supabase
36 .from('listings')
37 .insert({ ...values, seller_id: sellerId, status: 'active' })
38 .select('id')
39 .single()
40 if (!listing) { setUploading(false); return }
41 for (let i = 0; i < images.length; i++) {
42 const path = `${sellerId}/${listing.id}/${Date.now()}-${images[i].name}`
43 await supabase.storage.from('listings').upload(path, images[i])
44 await supabase.from('listing_images').insert({
45 listing_id: listing.id,
46 storage_path: path,
47 position: i,
48 is_primary: i === 0,
49 })
50 }
51 setUploading(false)
52 }
53
54 return (
55 <Form {...form}>
56 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
57 <div
58 onDrop={onDrop}
59 onDragOver={(e) => e.preventDefault()}
60 className="border-2 border-dashed rounded-lg p-8 text-center text-muted-foreground cursor-pointer hover:border-primary transition-colors"
61 >
62 {images.length === 0 ? 'Drag images here or click to select' : `${images.length} image(s) selected`}
63 </div>
64 <FormField control={form.control} name="title" render={({ field }) => (
65 <FormItem><FormLabel>Title</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
66 )} />
67 <FormField control={form.control} name="description" render={({ field }) => (
68 <FormItem><FormLabel>Description</FormLabel><FormControl><Textarea rows={5} {...field} /></FormControl><FormMessage /></FormItem>
69 )} />
70 <div className="grid grid-cols-2 gap-4">
71 <FormField control={form.control} name="price" render={({ field }) => (
72 <FormItem><FormLabel>Price ($)</FormLabel><FormControl><Input type="number" step="0.01" {...field} /></FormControl><FormMessage /></FormItem>
73 )} />
74 <FormField control={form.control} name="stock_quantity" render={({ field }) => (
75 <FormItem><FormLabel>Stock</FormLabel><FormControl><Input type="number" {...field} /></FormControl><FormMessage /></FormItem>
76 )} />
77 </div>
78 <FormField control={form.control} name="category" render={({ field }) => (
79 <FormItem><FormLabel>Category</FormLabel>
80 <Select onValueChange={field.onChange}>
81 <FormControl><SelectTrigger><SelectValue placeholder="Select category" /></SelectTrigger></FormControl>
82 <SelectContent>
83 <SelectItem value="electronics">Electronics</SelectItem>
84 <SelectItem value="clothing">Clothing</SelectItem>
85 <SelectItem value="home">Home & Garden</SelectItem>
86 <SelectItem value="art">Art & Crafts</SelectItem>
87 </SelectContent>
88 </Select>
89 <FormMessage /></FormItem>
90 )} />
91 <Button type="submit" disabled={uploading} className="w-full">
92 {uploading ? 'Publishing...' : 'Publish Listing'}
93 </Button>
94 </form>
95 </Form>
96 )
97}

Expected result: Sellers can drag images, fill in details, and submit. The listing appears in the storefront immediately. Images load from Supabase Storage public URLs.

4

Build the storefront with Command search

The buyer-facing storefront needs a responsive grid of listing cards and a Command palette for instant search. The Command component searches the listings_search_vector column using Supabase full-text search.

prompt.txt
1Build a marketplace storefront page at src/pages/Marketplace.tsx.
2
3Requirements:
4- Header with a shadcn/ui Command component (open with Cmd+K or clicking a search bar).
5 On input change, query Supabase: .textSearch('listings_search_vector', query, { type: 'websearch' })
6 Show results as CommandItem rows with listing title, price, and seller name.
7 Clicking a result navigates to /listing/[id].
8- Category filter tabs below the header using shadcn/ui Tabs. 'All' plus one tab per category.
9 Switching tabs re-fetches listings with .eq('category', selected) filter.
10- Listing grid: 2 columns mobile, 3 tablet, 4 desktop. Each Card shows:
11 - Primary image from listing_images where is_primary = true (Supabase Storage public URL)
12 - Title (2-line clamp), price, seller display_name, average rating (from reviews aggregate), stock Badge
13 - 'Add to Cart' button
14- Infinite scroll: fetch 20 listings at a time using .range(offset, offset+19). Increment offset when the user scrolls near the bottom using an IntersectionObserver.
15- Sort dropdown: Newest, Price Low to High, Price High to Low, Top Rated

Pro tip: Ask Lovable to add a shadcn/ui Drawer component on mobile that contains the category filters and sort options, triggered by a filter icon in the header. This keeps the mobile layout clean.

Expected result: The storefront loads with paginated listing cards. Opening the Command palette and typing returns matching results instantly. Switching category tabs filters the grid.

5

Build split payment checkout with Stripe Connect

The checkout Edge Function creates a Stripe PaymentIntent that charges the buyer and automatically routes the seller's share to their connected account, withholding the platform fee.

prompt.txt
1Create a Supabase Edge Function at supabase/functions/create-payment-intent/index.ts.
2
3The function receives: { order_id: string } in the request body.
4Requires authentication (validate JWT).
5
6Logic:
71. Fetch the order with its order_items and seller from Supabase using service role key.
82. Calculate: subtotal, platform_fee = subtotal * seller.commission_rate, seller_payout = subtotal - platform_fee.
93. Create a Stripe PaymentIntent:
10 - amount: subtotal in cents (multiply by 100)
11 - currency: 'usd'
12 - application_fee_amount: platform_fee in cents
13 - transfer_data: { destination: seller.stripe_account_id }
14 - metadata: { order_id, seller_id }
154. Update the orders row: stripe_payment_intent_id = intent.id, platform_fee, seller_payout.
165. Return: { client_secret: intent.client_secret }
17
18In the frontend checkout page:
19- Load Stripe.js with your publishable key.
20- Call the Edge Function to get client_secret.
21- Mount a Stripe PaymentElement inside a shadcn/ui Card.
22- On payment confirmation, update order status to 'paid' and navigate to /order-confirmation/[orderId].

Pro tip: Also create a Supabase Edge Function as a Stripe webhook handler that listens for payment_intent.succeeded and payment_intent.payment_failed events. This is more reliable than relying on the frontend redirect to update order status — the redirect can fail if the user closes the tab.

Expected result: Completing checkout charges the buyer. The Stripe Dashboard shows the payment with an automatic transfer to the seller's connected account minus the platform fee. Order status updates to 'paid'.

6

Build the reviews system with purchase verification

Only buyers who have a delivered order for a listing can leave a review. An Edge Function validates this before inserting the review, preventing fake reviews.

prompt.txt
1Create a Supabase Edge Function at supabase/functions/submit-review/index.ts.
2
3Receives: { listing_id: string, order_id: string, rating: number, body: string }
4Requires authentication.
5
6Logic:
71. Get user_id from JWT.
82. Verify the order exists where id = order_id AND buyer_id = user_id AND status = 'delivered'.
93. Verify no existing review where order_id = order_id (one review per order).
104. Verify the order contains an order_item with listing_id = listing_id.
115. If all checks pass, insert review: { listing_id, order_id, reviewer_id: user_id, rating, body }.
126. Return { success: true }.
13
14In the frontend, after a review is submitted, re-fetch the listing's review aggregate.
15On the listing page, show:
16- Average rating as filled/half/empty Star icons
17- Review count in parentheses
18- List of reviews as Cards with reviewer avatar (from auth.users), rating, body, and relative date
19- 'Leave a Review' Button only shown if the current user has a delivered order for this listing and has not yet reviewed it

Expected result: The listing page shows reviews with star ratings and verified buyer labels. The review form only appears for eligible buyers. Attempting to submit a review for an undelivered order returns an error.

Complete code

src/hooks/useListings.ts
1import { useState, useEffect, useCallback } from 'react'
2import { supabase } from '@/integrations/supabase/client'
3
4type Listing = {
5 id: string
6 title: string
7 price: number
8 stock_quantity: number
9 category: string
10 status: string
11 seller: { display_name: string; avatar_url: string | null }
12 primary_image_url: string | null
13 avg_rating: number | null
14 review_count: number
15}
16
17type Filters = {
18 category?: string
19 sort?: 'newest' | 'price_asc' | 'price_desc' | 'top_rated'
20 search?: string
21}
22
23const PAGE_SIZE = 20
24
25export function useListings(filters: Filters = {}) {
26 const [listings, setListings] = useState<Listing[]>([])
27 const [loading, setLoading] = useState(false)
28 const [hasMore, setHasMore] = useState(true)
29 const [offset, setOffset] = useState(0)
30
31 const fetchListings = useCallback(async (reset = false) => {
32 setLoading(true)
33 const currentOffset = reset ? 0 : offset
34
35 let query = supabase
36 .from('listings')
37 .select(`
38 id, title, price, stock_quantity, category, status,
39 sellers!inner(display_name, avatar_url),
40 listing_images(storage_path, is_primary),
41 reviews(rating)
42 `)
43 .eq('status', 'active')
44 .range(currentOffset, currentOffset + PAGE_SIZE - 1)
45
46 if (filters.category) query = query.eq('category', filters.category)
47 if (filters.search) query = query.textSearch('listings_search_vector', filters.search, { type: 'websearch' })
48
49 switch (filters.sort) {
50 case 'price_asc': query = query.order('price', { ascending: true }); break
51 case 'price_desc': query = query.order('price', { ascending: false }); break
52 case 'newest': query = query.order('created_at', { ascending: false }); break
53 default: query = query.order('created_at', { ascending: false })
54 }
55
56 const { data } = await query
57
58 const mapped: Listing[] = (data ?? []).map((row: any) => {
59 const primaryImg = row.listing_images?.find((img: any) => img.is_primary)
60 const ratings = row.reviews?.map((r: any) => r.rating) ?? []
61 const avg = ratings.length > 0 ? ratings.reduce((a: number, b: number) => a + b, 0) / ratings.length : null
62 const { data: urlData } = primaryImg
63 ? supabase.storage.from('listings').getPublicUrl(primaryImg.storage_path)
64 : { data: null }
65 return {
66 id: row.id,
67 title: row.title,
68 price: row.price,
69 stock_quantity: row.stock_quantity,
70 category: row.category,
71 status: row.status,
72 seller: row.sellers,
73 primary_image_url: urlData?.publicUrl ?? null,
74 avg_rating: avg,
75 review_count: ratings.length,
76 }
77 })
78
79 setListings(reset ? mapped : (prev) => [...prev, ...mapped])
80 setHasMore((data ?? []).length === PAGE_SIZE)
81 setOffset(currentOffset + PAGE_SIZE)
82 setLoading(false)
83 }, [filters, offset])
84
85 useEffect(() => { fetchListings(true) }, [filters.category, filters.search, filters.sort])
86
87 return { listings, loading, hasMore, loadMore: () => fetchListings(false) }
88}

Customization ideas

Offer management (counteroffers and best-offer listings)

Add an offers table (listing_id, buyer_id, seller_id, amount, status: pending|accepted|declined|expired, expires_at). Listings can enable 'Accept Offers'. Buyers send offers, sellers accept or counter. Accepting an offer auto-creates an order at the offer price. Implement expiry with a Supabase scheduled Edge Function.

Escrow-style held payouts

Instead of immediate transfer to the seller, hold funds in your Stripe platform account by removing transfer_data from the PaymentIntent. When the buyer marks an order delivered, a separate Edge Function calls the Stripe Transfers API to release the seller payout. This gives you a dispute window.

Featured listings and promoted slots

Add a featured boolean and featured_until timestamp to listings. Create a Stripe Checkout flow where sellers pay a flat fee to feature their listing for 7 or 30 days. A Supabase Edge Function webhook handler activates the feature on payment success. Featured listings appear first in the storefront grid with a Badge.

Seller analytics dashboard

Create a /seller/analytics page that queries orders, order_items, and reviews for the logged-in seller. Show a Recharts line chart of daily revenue for the past 30 days, top 5 listings by sales volume, average review rating over time, and a conversion rate estimate based on listing views tracked in a listing_views table.

Dispute resolution workflow

Add a disputes table (order_id, opened_by, reason, status: open|under_review|resolved, resolution). Buyers can open a dispute within 14 days of delivery. Both parties upload evidence to Supabase Storage. An admin dashboard lists open disputes with a resolution form that can trigger a Stripe refund Edge Function.

Common pitfalls

Pitfall: Exposing the Stripe secret key on the frontend

How to avoid: Always create PaymentIntents in a Supabase Edge Function. Store STRIPE_SECRET_KEY only in Cloud tab → Secrets where it is encrypted and accessible only to Edge Functions via Deno.env.get().

Pitfall: Not enforcing seller status before allowing listing creation

How to avoid: Add a check in the listings INSERT policy: EXISTS (SELECT 1 FROM sellers WHERE id = auth.uid() AND status = 'approved'). This blocks suspended sellers at the database level, not just the frontend.

Pitfall: Storing listing images directly in the database as base64

How to avoid: Upload images to Supabase Storage and store only the storage_path string in listing_images. Use supabase.storage.from('listings').getPublicUrl(path) in the frontend to generate URLs. Supabase Storage serves files via CDN automatically.

Pitfall: Allowing self-reviews from sellers

How to avoid: The submit-review Edge Function should verify that the reviewer_id is not the seller_id of the listing, and that there is a real delivered order linking the buyer to the listing.

Pitfall: Not setting up Stripe Connect webhook event handling

How to avoid: Register a Supabase Edge Function URL as a Stripe webhook endpoint listening for payment_intent.succeeded and account.updated. Update order status and seller onboarding status from these webhook events, which are reliable even when the browser is closed.

Best practices

  • Always validate that a seller's stripe_onboarding_complete = true before allowing listing creation. An incomplete Connect onboarding means payouts will fail.
  • Use Supabase Storage transformation URLs for thumbnail generation. Append ?width=400&height=400&resize=cover to any Storage URL to get a resized version without storing multiple copies.
  • Structure listing image storage paths as {seller_id}/{listing_id}/{timestamp}-{filename}. This makes it easy to delete all images for a listing with a prefix delete operation.
  • Keep commission_rate on the sellers table rather than hardcoding it in Edge Functions. This lets you negotiate different rates with high-volume sellers without redeploying code.
  • Index the listings table on (category, status, created_at) to keep category-filtered queries fast as the table grows.
  • Use Supabase Realtime on the orders table so sellers see new orders appear in their dashboard instantly without polling.
  • Implement idempotency keys on all Stripe API calls in Edge Functions. Pass the order_id as the idempotency key so retried requests don't create duplicate charges.
  • Paginate all listing queries with .range() and never use .limit() alone without an offset — it makes infinite scroll impossible to implement correctly.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a two-sided marketplace in Lovable (React + Supabase + Stripe Connect). I have tables: sellers (id, stripe_account_id, commission_rate), listings (seller_id, price, status), orders (buyer_id, seller_id, status, stripe_payment_intent_id). Write a TypeScript Supabase Edge Function (Deno) called create-payment-intent that takes { order_id } from the request body, fetches the order and seller, calculates the platform fee using commission_rate, and creates a Stripe PaymentIntent with application_fee_amount and transfer_data.destination set to the seller's stripe_account_id. Return the client_secret.

Lovable Prompt

Add a 'Save for Later' feature to the marketplace. Add a saved_listings table (user_id, listing_id, created_at) with RLS allowing users to manage their own saved listings. On each listing card, add a heart icon Button that toggles the saved state. Create a /saved page showing all saved listings in the same grid layout as the main storefront. Show the count of saves on each listing card as a muted text under the price.

Build Prompt

In Supabase, write a PostgreSQL function called get_seller_dashboard_stats(p_seller_id uuid) that returns a JSON object with: total_revenue (sum of seller_payout from paid/shipped/delivered orders), pending_orders (count where status=paid), avg_rating (average from reviews joined through listings), total_listings (count of active listings), revenue_last_30_days (sum of seller_payout from orders in the past 30 days). Run this as a single RPC call from the seller dashboard.

Frequently asked questions

How does Stripe Connect handle taxes?

Stripe Connect itself does not calculate taxes — that is your platform's responsibility. For simple cases, add a tax_rate field to your listings and calculate tax in the Edge Function before adding it to the PaymentIntent amount. For production marketplaces, Stripe Tax can be enabled on the PaymentIntent by adding automatic_payment_methods and tax configuration. Store the tax amount on the orders row for accounting.

Can sellers withdraw their earnings at any time?

With Stripe Connect, you can configure payouts to happen automatically on a schedule (daily, weekly, or monthly) or manually on demand. Standard Connect accounts handle their own payout schedule. Express or Custom accounts let your platform trigger payouts via the Stripe Transfers API. Choose the Connect account type in your Stripe Dashboard based on how much control you want.

How do I handle international sellers and currencies?

Stripe Connect supports multi-currency payouts. The buyer pays in your platform's currency, and Stripe handles the conversion to the seller's bank currency. In the PaymentIntent, set currency to your base currency. Sellers in different countries receive payouts in their local currency automatically. No extra Edge Function code is required.

What happens if a buyer disputes a charge?

Stripe notifies your platform via a charge.dispute.created webhook event. Create an Edge Function to handle this event, update the disputed order's status to 'disputed' in Supabase, and notify the seller via email. Stripe automatically debits the disputed amount from your platform account. Your dispute workflow should collect evidence from both parties and submit it to Stripe within the response deadline.

How do I add search filters like price range?

Supabase supports range filters natively. In your listings query, add .gte('price', minPrice).lte('price', maxPrice). For the UI, ask Lovable to add a shadcn/ui Slider component with two thumbs for min and max price. Debounce the slider's onChange handler by 300ms before re-fetching to avoid hammering Supabase on every slider move.

Can listings have variants (size, color)?

Yes. Add a listing_variants table (id, listing_id, name, options jsonb, price_adjustment numeric, stock_quantity). A 'T-Shirt' listing might have a 'Size' variant with options ['S','M','L','XL'] and individual stock per variant. In the listing detail page, show variant selectors that update the add-to-cart target to a specific variant_id. Store variant_id in order_items for fulfillment.

How do I moderate listings before they go live?

Set new listing status to 'draft' by default and add an admin dashboard page that fetches listings where status = 'draft'. Admins can preview the listing and approve it (update status to 'active') or reject it (update to 'rejected' with a rejection_reason). Add an RLS policy so only users with an admin role can update any listing's status field.

Is there help available if the Stripe Connect setup gets complex?

RapidDev specializes in production marketplace builds with Stripe Connect, including escrow workflows, dispute handling, and multi-currency configurations. Reach out if you need expert help with your Lovable marketplace.

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.