Skip to main content
RapidDev - Software Development Agency

How to Build Marketplace with V0

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'll build

  • Listing feed with Card grid, category Tabs, search filtering, and featured seller profiles
  • Stripe Connect onboarding flow for sellers with automatic account creation and status tracking
  • Split payment processing with configurable platform fee using PaymentIntent with transfer_data
  • Seller dashboard for managing listings, viewing orders, and tracking earnings
  • Review and rating system with star ratings tied to completed orders
  • In-order messaging thread using shadcn/ui Sheet for buyer-seller communication
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced10 min read2-4 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

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

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
Stripe ConnectPayments

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

1

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.

prompt.txt
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.

2

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.

prompt.txt
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.

3

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.

app/api/stripe/connect/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import Stripe from 'stripe'
3import { createClient } from '@supabase/supabase-js'
4
5const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function POST(req: NextRequest) {
12 const { user_id, email } = await req.json()
13
14 const account = await stripe.accounts.create({
15 type: 'express',
16 email,
17 metadata: { user_id },
18 })
19
20 await supabase
21 .from('profiles')
22 .update({ stripe_account_id: account.id })
23 .eq('id', user_id)
24
25 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 })
31
32 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.

4

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.

app/api/payments/create-checkout/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import Stripe from 'stripe'
3import { createClient } from '@supabase/supabase-js'
4
5const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function POST(req: NextRequest) {
12 const { listing_id, buyer_id } = await req.json()
13
14 const { data: listing } = await supabase
15 .from('listings')
16 .select('*, seller:profiles!seller_id(stripe_account_id)')
17 .eq('id', listing_id)
18 .single()
19
20 if (!listing?.seller?.stripe_account_id) {
21 return NextResponse.json({ error: 'Seller not onboarded' }, { status: 400 })
22 }
23
24 const platformFee = Math.round(listing.price_cents * 0.1)
25
26 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 })
44
45 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.

5

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.

app/api/webhooks/stripe/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import Stripe from 'stripe'
3import { createClient } from '@supabase/supabase-js'
4
5const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function POST(req: NextRequest) {
12 const rawBody = await req.text()
13 const sig = req.headers.get('stripe-signature')!
14
15 let event: Stripe.Event
16 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 }
21
22 switch (event.type) {
23 case 'checkout.session.completed': {
24 const session = event.data.object as Stripe.Checkout.Session
25 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 break
37 }
38 case 'account.updated': {
39 const account = event.data.object as Stripe.Account
40 if (account.charges_enabled) {
41 await supabase.from('profiles')
42 .update({ onboarding_complete: true })
43 .eq('stripe_account_id', account.id)
44 }
45 break
46 }
47 }
48
49 return NextResponse.json({ received: true })
50}

Expected result: Successful checkouts create order records. Completed onboarding updates seller profiles. Both events are webhook-verified.

6

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.

prompt.txt
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 Rating
5// - Tabs: Listings, Orders, Earnings
6// - 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

app/api/webhooks/stripe/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import Stripe from 'stripe'
3import { createClient } from '@supabase/supabase-js'
4
5const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function POST(req: NextRequest) {
12 const rawBody = await req.text()
13 const sig = req.headers.get('stripe-signature')!
14
15 let event: Stripe.Event
16 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 }
23
24 if (event.type === 'checkout.session.completed') {
25 const session = event.data.object as Stripe.Checkout.Session
26 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 }
39
40 if (event.type === 'account.updated') {
41 const acct = event.data.object as Stripe.Account
42 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 }
48
49 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.

ChatGPT Prompt

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.

Build Prompt

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.

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.