Skip to main content

Best Boilerplates for Building Marketplace Platforms 2026

·StarterPick Team
marketplacestripe-connecttwo-sidedplatformboilerplate2026

TL;DR

Building a marketplace is 10x harder than a regular SaaS because you have two customer types (buyers + sellers) with different flows, and money movement between them. The core complexity is always Stripe Connect for split payments and payouts. No boilerplate handles this fully — you combine a SaaS starter + custom Stripe Connect implementation. The most important decision: standard (seller can't touch funds) vs express accounts (seller has a Stripe dashboard).

Key Takeaways

  • Stripe Connect: Non-negotiable for any real marketplace with money movement
  • Connect account type: Express (seller has Stripe dashboard) vs Standard vs Custom
  • Application fee: Stripe's built-in mechanism for platform fee on each transaction
  • Transfers: Separate payment from payout — hold funds, release after service delivery
  • Seller onboarding: OAuth flow for Standard; hosted onboarding for Express
  • Tax reporting: Stripe handles 1099 generation for US marketplaces with Connect

Marketplace Architecture

Buyer pays → Stripe (platform account)
                   ↓
           Split: 85% → Seller account
                   +  15% → Platform fee (your revenue)
                   
Timeline:
  1. Buyer pays → Funds held in platform Stripe account
  2. Service delivered / product shipped
  3. Platform releases funds → Seller's Stripe account
  4. Seller transfers to bank account (2-7 day payout)

// lib/stripe-connect.ts:
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// Step 1: Create Express account onboarding link for seller:
export async function createSellerOnboardingLink(sellerId: string) {
  // First, create (or get existing) Connect account:
  const account = await stripe.accounts.create({
    type: 'express',
    metadata: { sellerId },
    capabilities: {
      transfers: { requested: true },
      card_payments: { requested: true },
    },
  });
  
  // Save account ID to your DB:
  await db.seller.update({
    where: { id: sellerId },
    data: { stripeAccountId: account.id },
  });
  
  // Generate onboarding link:
  const link = await stripe.accountLinks.create({
    account: account.id,
    refresh_url: `${process.env.NEXT_PUBLIC_URL}/seller/onboarding/refresh`,
    return_url: `${process.env.NEXT_PUBLIC_URL}/seller/dashboard`,
    type: 'account_onboarding',
  });
  
  return link.url;  // Redirect seller to this URL
}

// Check if seller completed onboarding:
export async function checkSellerOnboarded(stripeAccountId: string) {
  const account = await stripe.accounts.retrieve(stripeAccountId);
  return account.details_submitted && !account.requirements?.currently_due?.length;
}
// Step 2: Create payment with application fee (the platform fee):
export async function createMarketplacePayment(params: {
  amount: number;           // Total amount in cents ($100 = 10000)
  platformFeePercent: number;  // e.g., 0.15 for 15%
  sellerStripeAccountId: string;
  description: string;
  metadata: Record<string, string>;
}) {
  const { amount, platformFeePercent, sellerStripeAccountId } = params;
  const applicationFeeAmount = Math.round(amount * platformFeePercent);
  
  const paymentIntent = await stripe.paymentIntents.create({
    amount,
    currency: 'usd',
    application_fee_amount: applicationFeeAmount,  // Platform gets this
    transfer_data: {
      destination: sellerStripeAccountId,          // Seller gets the rest
    },
    metadata: params.metadata,
  });
  
  return paymentIntent;
}

// Or with Checkout Session:
export async function createCheckoutSession(params: {
  lineItems: Stripe.Checkout.SessionCreateParams.LineItem[];
  sellerAccountId: string;
  platformFeePercent: number;
}) {
  const session = await stripe.checkout.sessions.create({
    mode: 'payment',
    line_items: params.lineItems,
    payment_intent_data: {
      application_fee_amount: calculateFee(params.lineItems, params.platformFeePercent),
      transfer_data: { destination: params.sellerAccountId },
    },
    success_url: `${process.env.NEXT_PUBLIC_URL}/orders/{CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/cancel`,
  });
  
  return session.url;
}

Service Marketplace: Escrow Pattern

// For service marketplaces (Fiverr-like) — hold funds until delivery:

// 1. Capture payment but don't transfer yet:
const paymentIntent = await stripe.paymentIntents.create({
  amount: 10000,  // $100
  currency: 'usd',
  capture_method: 'manual',  // Don't capture yet (auth only)
  transfer_data: { destination: sellerAccountId },
  application_fee_amount: 1500,  // $15 platform fee
});

// 2. When buyer confirms delivery — capture and release:
async function releasePayment(paymentIntentId: string) {
  await stripe.paymentIntents.capture(paymentIntentId);
  
  // The transfer to seller happens automatically
  // Update order status:
  await db.order.update({
    where: { stripePaymentIntentId: paymentIntentId },
    data: { status: 'completed', completedAt: new Date() },
  });
}

// 3. If dispute — refund buyer:
async function refundBuyer(paymentIntentId: string) {
  await stripe.paymentIntents.cancel(paymentIntentId);
  // or for already captured: stripe.refunds.create({ payment_intent: id })
}

Seller Dashboard: Payout Status

// Seller can see their Stripe Express dashboard:
async function getSellerDashboardLink(sellerStripeAccountId: string) {
  const link = await stripe.accounts.createLoginLink(sellerStripeAccountId);
  return link.url;  // Direct link to Stripe Express dashboard
}

// Or build your own payout display:
async function getSellerBalance(sellerStripeAccountId: string) {
  const balance = await stripe.balance.retrieve({
    stripeAccount: sellerStripeAccountId,
  });
  
  return {
    available: balance.available[0]?.amount ?? 0,  // Ready to payout
    pending: balance.pending[0]?.amount ?? 0,       // Not yet settled
  };
}

Listing Management

// Marketplace listing schema:
model Listing {
  id          String   @id @default(cuid())
  sellerId    String
  seller      Seller   @relation(fields: [sellerId], references: [id])
  title       String
  description String
  price       Int      // Cents
  category    String
  images      String[]
  status      ListingStatus  // DRAFT, ACTIVE, PAUSED, SOLD
  featured    Boolean  @default(false)
  
  // For physical products:
  stock       Int?
  shipping    Json?    // { methods: [{name, price, days}] }
  
  // For services:
  deliveryDays Int?
  
  createdAt   DateTime @default(now())
}

// Buyer search/filter:
async function searchListings(params: {
  query?: string;
  category?: string;
  minPrice?: number;
  maxPrice?: number;
  sortBy: 'newest' | 'price_asc' | 'price_desc' | 'popular';
}) {
  const listings = await db.listing.findMany({
    where: {
      status: 'ACTIVE',
      ...(params.query && {
        OR: [
          { title: { contains: params.query, mode: 'insensitive' } },
          { description: { contains: params.query, mode: 'insensitive' } },
        ],
      }),
      ...(params.category && { category: params.category }),
      price: {
        gte: (params.minPrice ?? 0) * 100,
        lte: (params.maxPrice ?? 99999) * 100,
      },
    },
    orderBy: {
      newest: { createdAt: 'desc' },
      price_asc: { price: 'asc' },
      price_desc: { price: 'desc' },
      popular: { orders: { _count: 'desc' } },
    }[params.sortBy],
    include: { seller: { select: { name: true, rating: true, avatarUrl: true } } },
  });
  
  return listings;
}

Decision Guide

Use Express Connect if:
  → Sellers are individuals (gig economy, freelancers)
  → You want Stripe to handle seller compliance
  → Sellers need their own payout dashboard
  → 95% of marketplace use cases

Use Standard Connect if:
  → Sellers are businesses with existing Stripe accounts
  → Sellers want their own Stripe account management
  → B2B marketplace (SaaS vendors selling through your platform)

Use Custom Connect if:
  → Complete control over onboarding UI/UX
  → White-label the payment experience
  → Complex compliance requirements
  → Significant engineering investment required

Best boilerplate starting points for marketplaces:
  → ShipFast + custom Stripe Connect (fastest MVP)
  → T3 Stack + custom Stripe Connect (TypeScript-first)
  → Medusa.js (ecommerce-focused, has marketplace plugin)
  → No pre-built marketplace boilerplate handles Connect well — build on top

Find marketplace boilerplates at StarterPick.

Comments