Back to Blog
·13 min

How to Build a Multi-Vendor Marketplace with Stripe Connect

Multi-vendor marketplaces — platforms like Etsy, Airbnb, Uber, and Fiverr — are among the most valuable business models on the internet. They connect buyers with sellers, take a commission on each transaction, and scale by onboarding more vendors.

Building a marketplace platform from scratch is significantly more complex than building a single-vendor e-commerce site. You need vendor onboarding, commission management, split payments, dispute resolution, and a review system. Stripe Connect handles the payment complexity, and a marketplace starter kit provides the application infrastructure.

What Makes Marketplace Development Complex

A multi-vendor marketplace requires features that single-vendor platforms do not:

  • Vendor registration and onboarding with KYC verification
  • Product or service listing management per vendor
  • Commission calculation and automatic splitting
  • Delayed payouts with escrow-like protection
  • Stripe Connect for marketplace payments
  • Vendor dashboard with earnings analytics
  • Review and rating system for trust

The Multi-Vendor Marketplace Starter template includes all of this pre-built.

Stripe Connect Architecture

Stripe Connect provides two main integration patterns:

  • Destination charges: You charge the customer, take your commission, and send the remainder to the vendor
  • Separate charges and transfers: Each party is charged separately, and you transfer funds between accounts

For most marketplaces, destination charges with platform fees work best:

// lib/stripe/marketplace.ts
import { stripe } from './server'

export async function createMarketplacePayment(
  amount: number,
  vendorStripeAccountId: string,
  platformFee: number,
  description: string
) {
  const payment = await stripe.paymentIntents.create({
    amount: Math.round(amount * 100),
    currency: 'usd',
    description,
    application_fee_amount: Math.round(platformFee * 100),
    transfer_data: {
      destination: vendorStripeAccountId,
    },
  })

  return payment
}

Vendor Onboarding with Stripe Connect

// app/api/vendor/onboard/route.ts
import { NextResponse } from 'next/server'
import { stripe } from '@/lib/stripe/server'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'

export async function GET() {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const user = await prisma.user.findUnique({
    where: { id: session.user.id },
    select: { stripeConnectId: true, email: true },
  })

  let accountId = user?.stripeConnectId

  if (!accountId) {
    // Create a Stripe Connect Express account
    const account = await stripe.accounts.create({
      type: 'express',
      email: user!.email,
      capabilities: {
        transfers: { requested: true },
      },
      metadata: { userId: session.user.id },
    })

    accountId = account.id
    await prisma.user.update({
      where: { id: session.user.id },
      data: { stripeConnectId: account.id },
    })
  }

  // Generate onboarding link
  const accountLink = await stripe.accountLinks.create({
    account: accountId,
    refresh_url: `${process.env.NEXT_PUBLIC_APP_URL}/vendor/onboard`,
    return_url: `${process.env.NEXT_PUBLIC_APP_URL}/vendor/dashboard`,
    type: 'account_onboarding',
  })

  return NextResponse.json({ url: accountLink.url })
}

Product Listing with Vendor Association

// app/actions/create-listing.ts
'use server'

import { supabase } from '@/lib/supabase'
import { auth } from '@/lib/auth'
import { revalidatePath } from 'next/cache'

export async function createListing(formData: FormData) {
  const session = await auth()
  if (!session?.user) throw new Error('Unauthorized')

  const user = await prisma.user.findUnique({
    where: { id: session.user.id },
    select: { isVendor: true },
  })

  if (!user?.isVendor) throw new Error('Not a vendor')

  const name = formData.get('name') as string
  const description = formData.get('description') as string
  const price = parseFloat(formData.get('price') as string)
  const category = formData.get('category') as string

  const { error } = await supabase.from('products').insert({
    vendor_id: session.user.id,
    name,
    description,
    price,
    category,
    status: 'active',
  })

  if (error) throw new Error('Failed to create listing')

  revalidatePath('/vendor/products')
}

Commission Management

// lib/marketplace/commissions.ts
export interface CommissionConfig {
  platformFeePercent: number
  platformFeeFixed: number
  vendorPayoutSchedule: 'daily' | 'weekly' | 'monthly'
  minimumPayoutAmount: number
}

export const defaultCommission: CommissionConfig = {
  platformFeePercent: 10, // 10% platform fee
  platformFeeFixed: 0.50, // $0.50 fixed fee
  vendorPayoutSchedule: 'weekly',
  minimumPayoutAmount: 25, // $25 minimum payout
}

export function calculateFees(
  itemPrice: number,
  config: CommissionConfig = defaultCommission
) {
  const percentFee = itemPrice * (config.platformFeePercent / 100)
  const totalFee = percentFee + config.platformFeeFixed
  const vendorEarnings = itemPrice - totalFee

  return {
    platformFee: Math.round(totalFee * 100) / 100,
    vendorEarnings: Math.round(vendorEarnings * 100) / 100,
    itemPrice,
  }
}

Vendor Dashboard with Earnings

// app/vendor/dashboard/page.tsx
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { redirect } from 'next/navigation'
import { EarningsSummary } from './EarningsSummary'
import { RecentOrders } from './RecentOrders'

export default async function VendorDashboard() {
  const session = await auth()
  if (!session?.user) redirect('/login')

  const earnings = await prisma.order_item.aggregate({
    _sum: { price: true },
    where: {
      product: { vendor_id: session.user.id },
      order: { status: 'completed' },
    },
  })

  const totalSales = await prisma.order_item.count({
    where: {
      product: { vendor_id: session.user.id },
      order: { status: 'completed' },
    },
  })

  return (
    <div className="mx-auto max-w-6xl px-4 py-8">
      <h1 className="text-2xl font-bold">Vendor Dashboard</h1>
      <div className="mt-6 grid gap-6 sm:grid-cols-3">
        <div className="rounded-xl border bg-white p-6">
          <p className="text-sm text-gray-500">Total Earnings</p>
          <p className="mt-2 text-3xl font-bold text-green-600">
            ${(earnings._sum.price ?? 0).toFixed(2)}
          </p>
        </div>
        <div className="rounded-xl border bg-white p-6">
          <p className="text-sm text-gray-500">Total Sales</p>
          <p className="mt-2 text-3xl font-bold">{totalSales}</p>
        </div>
        <div className="rounded-xl border bg-white p-6">
          <p className="text-sm text-gray-500">Available for Payout</p>
          <p className="mt-2 text-3xl font-bold text-brand-600">
            ${(earnings._sum.price ?? 0 * 0.9).toFixed(2)}
          </p>
        </div>
      </div>
      <RecentOrders vendorId={session.user.id} />
    </div>
  )
}

The Bottom Line

Multi-vendor marketplaces are powerful but complex. Stripe Connect handles the payment infrastructure, and a marketplace starter kit provides vendor onboarding, commission management, and dashboards. The Multi-Vendor Marketplace Starter template gives you everything you need to launch your platform.

Ready to Build?

Get started with our production-ready starter kits and ship your project faster.

Browse Starter Kits