A two-sided marketplace is the hardest SaaS to build because you're solving two products at once. The buyer side needs frictionless browsing, instant booking, and confidence they'll get refunded if something goes wrong. The seller side needs clear earnings potential, quick payment, and protection from bad actors. Both sides live in the same database but see completely different interfaces. One schema. Two user roles. Stripe Connect for automated payouts. Ratings + dispute resolution to keep the platform trustworthy. Here's what a production marketplace architecture looks like: the schema, matching flows, dispute handling, and why bootstrapping sellers is harder than attracting buyers.
The Core Problem
Marketplaces fail because they chase buyers before sellers exist. You build a beautiful "find a task" interface, launch, and get 50 sign-ups in week one with nothing to buy. Sellers don't show up because there's no buyer traffic. Buyers leave because there's no inventory. This is the chicken-and-egg trap that kills 90% of marketplace startups. Successful ones (Stripe, Shopify, Lyft) solved this backwards: they found a supply side first, built a seller dashboard, proved demand on that seller base existed, then scaled buyer acquisition. You need to understand that a marketplace isn't one product—it's two dependent products that have to launch together, or at least sellers first.
The other hard part: trust and payment. A buyer gives you $200. A seller does work. Both are strangers. If the seller disappears or the work is terrible, you either refund the buyer (and eat the loss) or keep their money (and lose the platform). This requires ratings (public trust signals), escrow (Stripe holding the money until work is approved), and dispute resolution (you actually read both sides and make a call). Airbnb's trust and safety team is 1000+ people. You're probably one person. So your system has to be smart: automated matching to reduce bad pairings, automated rating submission, graduated escalation for disputes, and one clear rule the whole platform follows.
The Schema
One Postgres table for users. Two boolean role flags: is_buyer and is_seller. Most users are both (buyers who occasionally sell, sellers who book other services). This is simpler than role-based access control and closer to how real marketplaces work.
-- Users (one role per person, many roles per account)
create table users (
id uuid primary key,
email text unique not null,
display_name text,
avatar_url text,
bio text,
is_buyer boolean default true,
is_seller boolean default false,
seller_category text, -- "cleaning", "photography", "tutoring", etc
seller_onboarded_at timestamp,
seller_stripe_account_id text, -- Stripe Connected account
hourly_rate_cents integer, -- for services; null for tasks
created_at timestamp default now()
);
-- Listings (seller offerings)
create table listings (
id uuid primary key,
seller_id uuid references users(id) on delete cascade,
title text not null,
description text,
category text, -- "home-repair", "tutoring", "design"
price_cents integer,
availability_json jsonb, -- { "monday": [9,17], "tuesday": [9,17], ... }
is_active boolean default true,
rating_avg numeric(3,2),
review_count integer default 0,
created_at timestamp default now()
);
-- Bookings (transactions)
create table bookings (
id uuid primary key,
buyer_id uuid references users(id) on delete cascade,
seller_id uuid references users(id) on delete cascade,
listing_id uuid references listings(id) on delete cascade,
amount_cents integer not null,
status text default 'pending', -- pending → paid → completed → disputed
scheduled_for timestamp,
completed_at timestamp,
payment_intent_id text, -- Stripe
stripe_transfer_id text, -- Seller payout
created_at timestamp default now(),
unique(buyer_id, seller_id, scheduled_for) -- Prevent double-booking
);
-- Reviews (post-completion ratings)
create table reviews (
id uuid primary key,
booking_id uuid references bookings(id) on delete cascade,
reviewer_id uuid references users(id), -- buyer or seller
reviewee_id uuid references users(id),
rating integer check (rating >= 1 and rating <= 5),
comment text,
tags jsonb, -- { "reliability": true, "quality": true, "communication": false }
created_at timestamp default now()
);
-- Disputes
create table disputes (
id uuid primary key,
booking_id uuid references bookings(id) on delete cascade,
initiator_id uuid references users(id), -- who opened the dispute
reason text, -- "work not completed", "quality issue", "no-show"
status text default 'open', -- open → resolved_buyer_wins → resolved_seller_wins → closed
resolution_notes text,
resolved_at timestamp,
created_at timestamp default now()
);
Key choices: no separate order table (bookings is the transaction record). status is the single source of truth—never derive it from related tables. seller_stripe_account_id ties each seller to their payout account. availability_json lets sellers set hours per day. unique(buyer_id, seller_id, scheduled_for) prevents overbooking—you can't book the same seller twice in the same slot.
Matching and Booking Flow
A buyer browses listings (simple: filter by category, sort by rating). They click "Book now" for a seller's listing. You show available slots (parsed from the seller's availability_json). Buyer picks a slot and pays. Stripe charge goes to your account, you deduct your platform fee, and schedule a transfer to the seller for after the booking is marked complete. Here's the code:
// Create booking → charge buyer → hold seller payout
export const createBooking = async (buyerId, sellerId, listingId, scheduledFor) => {
const listing = await db.listings.findOne(listingId);
const seller = await db.users.findOne(sellerId);
// Create payment intent (amount is locked until booking is complete)
const paymentIntent = await stripe.paymentIntents.create({
amount: listing.price_cents,
currency: 'aud',
customer: buyerId, // buyer's Stripe customer ID
application_fee_amount: Math.round(listing.price_cents * 0.15), // 15% platform fee
transfer_data: {
destination: seller.seller_stripe_account_id // Seller's payout account
},
metadata: { bookingId: crypto.randomUUID() }
});
// Create booking record in "pending" state
const booking = await db.bookings.create({
buyer_id: buyerId,
seller_id: sellerId,
listing_id: listingId,
amount_cents: listing.price_cents,
scheduled_for: scheduledFor,
status: 'pending',
payment_intent_id: paymentIntent.id
});
return { booking, clientSecret: paymentIntent.client_secret };
};
// Webhook: buyer confirms payment (from frontend)
const handlePaymentSuccess = async (paymentIntentId) => {
const booking = await db.bookings.findOne({ payment_intent_id: paymentIntentId });
await db.bookings.update(booking.id, { status: 'paid' });
// Notify seller: new booking
await sendNotification(booking.seller_id, {
type: 'booking_confirmed',
message: `New booking on ${booking.scheduled_for.toLocaleDateString()}`,
bookingId: booking.id
});
};
The key: you never hold the money. Stripe charges the buyer's card, takes processing fees (2.9% + 30¢), you take your fee (e.g., 15%), and the remainder is automatically transferred to the seller's bank account after the booking is marked complete. If the buyer disputes within 30 days, Stripe reverses the charge and reverses the payout — the seller eats the loss, not you.
Ratings and Dispute Escalation
After the scheduled time passes, both buyer and seller get a prompt to rate each other (1-5 stars + tags). Ratings are public and immediate—they feed into the seller's listing.rating_avg (used for ranking) and impact seller trust. If either party files a dispute, the system escalates: first, automated checks (booking exists, payment settled, time passed). If both sides have conflicting stories, you manually review evidence: screenshots, messages in your platform, external proof. You make a call and issue a refund or close the dispute. Document everything—this is your liability defense if things go legal.
Critical rule: disputes must be resolvable fast (48 hours max). Any longer and both sides assume you're unfair. Set a clear policy in your terms: "We refund if the seller didn't show up (no confirmation from buyer), or if the work quality is objectively deficient (with photo evidence)." Yes-your-word-against-theirs disputes go 70/30 to the buyer (they risk less, so they should trust the platform more). Seller must complete work or refund is automatic on the scheduled time + 24 hours.
Supply-Side Bootstrap
You can't launch with zero sellers. You have three levers: first, recruit 5–10 local sellers manually (email, calls, whatever) and give them free premium listings for 90 days. Get 20–30 bookings flowing through to prove the system works. Second, build a seller application flow that's frictionless (10 minutes of form-filling + Stripe verification). Third, offer sellers a higher take-rate while you bootstrap: give them 90% of revenue for the first 3 months, then drop to 80% (your take increases from 10% to 20%). This works because sellers see money moving fast and tell their friends. By month 4, you've got momentum.
Don't launch with a buyer side until you have 20+ active sellers. Buyers will come once there's inventory—marketplaces are supply-constrained at launch, not demand-constrained. Build the supply-side dashboard first: seller can list, set availability, see earnings, export payouts. Let them live in that dashboard for 2–3 months before you market to buyers. The first 100 bookings come from word-of-mouth and your seller recruitment, not ads.
Six FAQs
Can I automate disputes?
Partially. If buyer marks booking complete and seller hasn't disputed within 24 hours, payout is automatic. If both rate each other 4+ stars, dispute risk is near-zero. Use a rule engine: same-person bookings (buyer books, immediately markets "work complete") get refunded 100% because it's obviously fraud. Everything else requires manual review. AI can help summarize messages, but you make the final call.
What if a seller gets too many negative reviews?
Automated suspension. If rating drops below 3.0 or dispute rate exceeds 5%, automatic email to seller: "Your listing is suspended pending review." Give them 7 days to respond or improve. If they don't, delist them. Public rating floors protect the platform; private review process protects the seller.
How do I prevent scam sellers?
Seller verification on signup: government ID + bank verification (Stripe handles this). Manual review of first 5 listings (you read them). Watch for common scam patterns: too-cheap pricing, vague descriptions, new account + high volume = red flag. Flag and investigate. For repeat offenders, permanent ban.
Can sellers charge cancellation fees?
Yes. Add a cancellation_policy field to listings. You enforce it: buyer cancels within 48 hours = seller keeps 50%. Within 24 hours = seller keeps 100%. This incentivizes sellers to accept bookings (they're partially protected) and buyers to commit (time cost). Rules are set once per listing, not per booking.
What if payment processing fails?
Booking stays in pending state. After 24 hours, you auto-cancel and notify buyer: "Payment failed, please retry or rebook." Don't nag endlessly—too many retry prompts destroy UX. One retry loop, then archive.
How do I calculate seller earnings accurately?
Single source of truth: Stripe. Query stripe.charges.list() and filter by your platform account. Sum the application_fee_amount for your revenue. Sum the transfer_data.destination transfers for seller earnings. Never calculate it client-side or in your DB—it will drift. Stripe is the ledger.
The Bottom Line
Two-sided marketplaces are complex because you're solving two separate product problems with one database and one trust system. The wins: one schema (users, listings, bookings, reviews, disputes) handles buyer + seller flows. Stripe Connect automates payouts so you don't hold money or manage escrow. Automated rating submission and dispute escalation keep platform health high without hiring a 1000-person trust team. Start with supply-side recruitment, launch when you've got 20+ active sellers, then scale buyer acquisition. The first marketplace that nails this pattern—frictionless seller onboarding, transparent earnings, fast dispute resolution—wins the market. Ready to build? See Velocity X marketplace implementations for full architecture and go-live support. Compare with Stripe Connect payment flows for deeper payout mechanics.