Build a two-sided marketplace with V0 using Next.js, Supabase, Stripe Connect, and shadcn/ui. Features buyer and seller flows, split payments with platform fees, listing management, order tracking, reviews, and in-order messaging — all with webhook-verified payment processing. Takes about 2-4 hours.
What you're building
Two-sided marketplaces like Etsy, Fiverr, and Airbnb connect buyers with sellers and take a platform fee on each transaction. Building one requires buyer/seller role management, Stripe Connect for split payments, listing management, and order tracking.
V0 generates the listing UI, seller dashboard, and payment flow from prompts. Stripe Connect via the Vercel Marketplace handles split payments automatically — you set the platform fee percentage and Stripe routes funds to sellers. Supabase provides the database, auth, and real-time messaging.
The architecture uses Stripe Connect with destination charges for payment splitting, Supabase RLS for role-based access (buyer vs seller), Server Components for the listing feed, and webhook handlers for payment and seller onboarding events.
Final result
A two-sided marketplace with listing management, Stripe Connect split payments, seller onboarding, order tracking, reviews, and buyer-seller messaging.
Tech stack
Prerequisites
- A V0 account (Premium or higher — this is a complex build)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Stripe account with Connect enabled (test mode works — connect via Vercel Marketplace)
- Your marketplace concept (what buyers and sellers will trade)
Build steps
Set up the database schema for profiles, listings, and orders
Create the Supabase schema with role-based profiles, listings, orders with payment tracking, reviews, and messages. RLS policies ensure buyers and sellers only access their own data.
1// Paste this prompt into V0's AI chat:2// Build a two-sided marketplace. Create a Supabase schema:3// 1. profiles: id (uuid PK FK to auth.users), role (text CHECK IN 'buyer','seller','both'), display_name (text), avatar_url (text), stripe_account_id (text), onboarding_complete (boolean DEFAULT false)4// 2. listings: id (uuid PK), seller_id (uuid FK to profiles), title (text), description (text), price_cents (int), category (text), images (text[]), status (text DEFAULT 'active'), created_at (timestamptz)5// 3. orders: id (uuid PK), listing_id (uuid FK to listings), buyer_id (uuid FK to profiles), seller_id (uuid FK to profiles), amount_cents (int), platform_fee_cents (int), status (text DEFAULT 'pending'), stripe_payment_intent (text), created_at (timestamptz)6// 4. reviews: id (uuid PK), order_id (uuid FK to orders), reviewer_id (uuid FK to profiles), rating (int CHECK 1-5), comment (text), created_at (timestamptz)7// 5. messages: id (uuid PK), order_id (uuid FK to orders), sender_id (uuid FK to profiles), body (text), created_at (timestamptz)8// Add RLS policies: buyers see their orders, sellers see their listings and orders.Pro tip: Use V0's Connect panel for Supabase setup and the Vercel Marketplace for Stripe Connect — both auto-provision keys into the Vars tab.
Expected result: Supabase is connected with all tables, RLS policies for role-based access, and Stripe Connect keys in the Vars tab.
Build the listing feed and detail pages
Create the public marketplace pages with listing cards, category filters, and individual listing pages with seller info, images, and purchase flow.
1// Paste this prompt into V0's AI chat:2// Create marketplace pages:3// 1. app/page.tsx — listing feed with shadcn/ui Card grid: listing image, title, price, seller Avatar + name, category Badge. Add Tabs for categories, search Input, and sort Select (newest, price low-high, price high-low).4// 2. app/listings/[id]/page.tsx — listing detail: image gallery, full description, price in large text, seller Card with Avatar + rating stars + listing count. Add 'Buy Now' Button that creates a Stripe Checkout session. Add 'Message Seller' Button. Show reviews from completed orders below.5// Use Server Components for both pages. The 'Buy Now' button calls a Server Action.Expected result: The listing feed shows a filterable grid of items. Detail pages show full listing info with buy and message buttons.
Create the Stripe Connect onboarding flow
Build the seller onboarding that creates a Stripe Connect account and redirects them to Stripe's hosted onboarding. After completing onboarding, the webhook updates the seller's profile.
1import { NextRequest, NextResponse } from 'next/server'2import Stripe from 'stripe'3import { createClient } from '@supabase/supabase-js'45const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)6const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export async function POST(req: NextRequest) {12 const { user_id, email } = await req.json()1314 const account = await stripe.accounts.create({15 type: 'express',16 email,17 metadata: { user_id },18 })1920 await supabase21 .from('profiles')22 .update({ stripe_account_id: account.id })23 .eq('id', user_id)2425 const accountLink = await stripe.accountLinks.create({26 account: account.id,27 refresh_url: `${process.env.NEXT_PUBLIC_APP_URL}/seller/onboarding?refresh=true`,28 return_url: `${process.env.NEXT_PUBLIC_APP_URL}/seller/dashboard`,29 type: 'account_onboarding',30 })3132 return NextResponse.json({ url: accountLink.url })33}Expected result: Sellers click 'Start Selling', get a Stripe Connect account created, and are redirected to complete onboarding on Stripe's hosted page.
Set up the payment flow with platform fees
Create the payment processing that charges the buyer, sends funds to the seller's connected account, and keeps the platform fee. Use destination charges for simplicity.
1import { NextRequest, NextResponse } from 'next/server'2import Stripe from 'stripe'3import { createClient } from '@supabase/supabase-js'45const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)6const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export async function POST(req: NextRequest) {12 const { listing_id, buyer_id } = await req.json()1314 const { data: listing } = await supabase15 .from('listings')16 .select('*, seller:profiles!seller_id(stripe_account_id)')17 .eq('id', listing_id)18 .single()1920 if (!listing?.seller?.stripe_account_id) {21 return NextResponse.json({ error: 'Seller not onboarded' }, { status: 400 })22 }2324 const platformFee = Math.round(listing.price_cents * 0.1)2526 const session = await stripe.checkout.sessions.create({27 mode: 'payment',28 line_items: [{29 price_data: {30 currency: 'usd',31 product_data: { name: listing.title },32 unit_amount: listing.price_cents,33 },34 quantity: 1,35 }],36 payment_intent_data: {37 application_fee_amount: platformFee,38 transfer_data: { destination: listing.seller.stripe_account_id },39 },40 metadata: { listing_id, buyer_id, seller_id: listing.seller_id },41 success_url: `${process.env.NEXT_PUBLIC_APP_URL}/orders?success=true`,42 cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/listings/${listing_id}`,43 })4445 return NextResponse.json({ url: session.url })46}Pro tip: Use application_fee_amount with transfer_data.destination for destination charges. Stripe automatically splits the payment: the platform fee goes to your account, the rest to the seller.
Expected result: Buyers are redirected to Stripe Checkout. Payment is split: 10% platform fee, 90% to the seller's connected account.
Build the webhook handler for orders and onboarding
Create the webhook handler that processes checkout completions (creating orders) and account updates (tracking seller onboarding). Use request.text() for raw body verification.
1import { NextRequest, NextResponse } from 'next/server'2import Stripe from 'stripe'3import { createClient } from '@supabase/supabase-js'45const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)6const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export async function POST(req: NextRequest) {12 const rawBody = await req.text()13 const sig = req.headers.get('stripe-signature')!1415 let event: Stripe.Event16 try {17 event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!)18 } catch {19 return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })20 }2122 switch (event.type) {23 case 'checkout.session.completed': {24 const session = event.data.object as Stripe.Checkout.Session25 const { listing_id, buyer_id, seller_id } = session.metadata || {}26 if (listing_id && buyer_id) {27 const fee = Math.round((session.amount_total || 0) * 0.1)28 await supabase.from('orders').insert({29 listing_id, buyer_id, seller_id,30 amount_cents: session.amount_total,31 platform_fee_cents: fee,32 status: 'paid',33 stripe_payment_intent: session.payment_intent,34 })35 }36 break37 }38 case 'account.updated': {39 const account = event.data.object as Stripe.Account40 if (account.charges_enabled) {41 await supabase.from('profiles')42 .update({ onboarding_complete: true })43 .eq('stripe_account_id', account.id)44 }45 break46 }47 }4849 return NextResponse.json({ received: true })50}Expected result: Successful checkouts create order records. Completed onboarding updates seller profiles. Both events are webhook-verified.
Build the seller dashboard and deploy
Create the seller-facing dashboard for managing listings, viewing orders, and tracking earnings. Then deploy and register Stripe webhooks.
1// Paste this prompt into V0's AI chat:2// Create a seller dashboard at app/seller/dashboard/page.tsx.3// Requirements:4// - Summary Cards: Total Earnings, Active Listings, Pending Orders, Average Rating5// - Tabs: Listings, Orders, Earnings6// - Listings tab: Table with title, price, status Badge, views count, edit/delete actions. Add 'New Listing' Button with Dialog form.7// - Orders tab: Table with order ID, buyer name, listing title, amount, status Badge (pending/paid/shipped/completed), date. Status Select to update order status.8// - Earnings tab: summary of total paid, platform fees, net earnings. Table of completed orders.9// - If seller not onboarded with Stripe Connect, show an onboarding Card with 'Complete Setup' Button.10// Use Server Components with Supabase queries filtered by seller_id = current user.Pro tip: After deploying, register your webhook URL in Stripe Dashboard. Select both checkout.session.completed and account.updated events.
Expected result: The seller dashboard shows listings, orders, and earnings. Sellers without Stripe Connect see an onboarding prompt.
Complete code
1import { NextRequest, NextResponse } from 'next/server'2import Stripe from 'stripe'3import { createClient } from '@supabase/supabase-js'45const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)6const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export async function POST(req: NextRequest) {12 const rawBody = await req.text()13 const sig = req.headers.get('stripe-signature')!1415 let event: Stripe.Event16 try {17 event = stripe.webhooks.constructEvent(18 rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!19 )20 } catch {21 return NextResponse.json({ error: 'Signature failed' }, { status: 400 })22 }2324 if (event.type === 'checkout.session.completed') {25 const session = event.data.object as Stripe.Checkout.Session26 const meta = session.metadata || {}27 if (meta.listing_id) {28 await supabase.from('orders').insert({29 listing_id: meta.listing_id,30 buyer_id: meta.buyer_id,31 seller_id: meta.seller_id,32 amount_cents: session.amount_total,33 platform_fee_cents: Math.round((session.amount_total || 0) * 0.1),34 status: 'paid',35 stripe_payment_intent: session.payment_intent as string,36 })37 }38 }3940 if (event.type === 'account.updated') {41 const acct = event.data.object as Stripe.Account42 if (acct.charges_enabled && acct.details_submitted) {43 await supabase.from('profiles')44 .update({ onboarding_complete: true })45 .eq('stripe_account_id', acct.id)46 }47 }4849 return NextResponse.json({ received: true })50}Customization ideas
Escrow-style delayed payouts
Hold funds for a configurable period (e.g., 7 days) using Stripe's payout schedule settings, giving buyers time to confirm receipt before funds release.
Featured listings with paid promotion
Let sellers pay to feature their listings at the top of search results using a separate Stripe Checkout flow with listing_id metadata.
Real-time messaging with Supabase Realtime
Upgrade the messaging system to use Supabase Realtime subscriptions for instant message delivery without page refreshes.
Dispute resolution system
Add a dispute workflow where buyers can flag orders, admin reviews evidence, and refunds are processed through Stripe's Refund API.
Common pitfalls
Pitfall: Using request.json() instead of request.text() for Stripe webhook verification
How to avoid: Always use request.text() to get the raw body, then pass it to stripe.webhooks.constructEvent().
Pitfall: Not checking if the seller has completed Stripe Connect onboarding before allowing purchases
How to avoid: Check the seller's onboarding_complete flag before showing the Buy button. If not onboarded, show a message that the seller is setting up payments.
Pitfall: Storing platform fee as a fixed amount instead of calculating from the order total
How to avoid: Calculate platform_fee_cents as a percentage of the listing price (e.g., Math.round(price_cents * 0.1) for 10%) in the checkout creation endpoint.
Best practices
- Use Stripe Connect destination charges with application_fee_amount for simple payment splitting between platform and sellers
- Always verify Stripe webhook signatures with request.text() before processing any events
- Check seller onboarding_complete before enabling purchases to prevent failed payment attempts
- Use RLS policies to ensure buyers only see their orders and sellers only see their listings and incoming orders
- Register both checkout.session.completed and account.updated webhook events in Stripe Dashboard
- Use V0's Design Mode (Option+D) to adjust listing Card layouts and seller profile styling without spending credits
- Store all amounts in cents (integers) to avoid floating-point math errors in currency calculations
- Use Server Components for the listing feed for SEO — search engines see fully rendered listing titles and descriptions
AI prompts to try
Copy these prompts to build this project faster.
I'm building a two-sided marketplace with Next.js App Router, Supabase, and Stripe Connect. I need help with the payment flow. When a buyer purchases a listing, I need to create a Stripe Checkout session that charges the buyer, sends 90% to the seller's connected account, and keeps 10% as a platform fee. Use destination charges with application_fee_amount. The seller's stripe_account_id is in the profiles table. Please write the API route.
Create a Stripe Connect seller onboarding flow. Build an API route at app/api/stripe/connect/route.ts that creates an Express Connect account, stores the account ID in the profiles table, and returns an Account Link URL for hosted onboarding. The return URL should be the seller dashboard. After onboarding, handle the account.updated webhook to set onboarding_complete = true when charges_enabled is true.
Frequently asked questions
How does the payment split work between platform and sellers?
Stripe Connect destination charges handle the split automatically. You set application_fee_amount (e.g., 10% of the listing price) and transfer_data.destination (seller's connected account). Stripe charges the buyer, deducts your platform fee, and sends the rest to the seller.
What is Stripe Connect Express?
Express is the simplest Connect account type. Sellers complete onboarding on Stripe's hosted page (not your site), and Stripe handles identity verification, tax forms, and payouts. You just create the account and redirect them.
Do I need a paid V0 plan?
Yes, Premium ($20/month) at minimum. The marketplace has many complex pages (listing feed, detail, seller dashboard, onboarding, webhook handler) that require numerous prompts to build.
How do I handle refunds and disputes?
Use the Stripe Refund API in a Server Action. For Connect payments, you can refund from your platform account or reverse the transfer to the seller. Handle the charge.dispute.created webhook to track disputes.
Can sellers manage their own payouts?
Yes. Stripe Express accounts include a seller dashboard hosted by Stripe where sellers can view their balance, upcoming payouts, and transaction history. Generate a login link with stripe.accounts.createLoginLink().
How do I deploy the marketplace?
Click Share in V0, then Publish to Production. Set STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY in the Vars tab. Register webhook events (checkout.session.completed, account.updated) with your production URL in Stripe Dashboard.
Can RapidDev help build a custom marketplace?
Yes. RapidDev has built over 600 apps including two-sided marketplaces with Stripe Connect, escrow payments, and real-time messaging. Book a free consultation to discuss your marketplace concept and get a production-ready platform.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation