Back to Blog

How to Build a Marketplace Platform Like Uber or Airbnb

How to Build a Marketplace Platform Like Uber or Airbnb

Every marketplace looks simple from the outside. Sellers list things. Buyers buy things. The platform takes a cut. How hard can it be?

Very hard, as it turns out. We know because we built Vincelio, a creator-brand marketplace for the Latin American influencer marketing industry. Brands post campaigns. Creators apply. Content gets produced, reviewed, approved, and paid for. It sounds straightforward until you start building it and realize that a marketplace is not one product. It is two products that have to work together, plus a payment system, a trust system, and a matching system that somehow balances supply and demand.

Here is what we learned about marketplace architecture, drawn from our experience building Vincelio and studying the patterns behind platforms like Uber, Airbnb, and Fiverr.

The Two-Sided Data Model

A marketplace has at least two distinct user types with fundamentally different needs. In Vincelio, those are creators and brands. In Airbnb, hosts and guests. In Uber, drivers and riders.

The data model has to reflect this from day one. You cannot build a marketplace on a single user table with a type flag and hope for the best. The profiles, the workflows, the dashboards, and the incentives are completely different for each side.

Here is how we structure the data model:

-- Shared auth identity
create table users (
  id uuid primary key default gen_random_uuid(),
  email text unique not null,
  created_at timestamptz default now()
);

-- Supply side (creators, hosts, drivers)
create table creator_profiles (
  id uuid primary key default gen_random_uuid(),
  user_id uuid unique not null references users(id),
  display_name text not null,
  bio text,
  category text[],
  portfolio_urls text[],
  location text,
  audience_size int,
  rating numeric(3,2) default 0,
  review_count int default 0,
  verified boolean default false,
  created_at timestamptz default now()
);

-- Demand side (brands, guests, riders)
create table brand_profiles (
  id uuid primary key default gen_random_uuid(),
  user_id uuid unique not null references users(id),
  company_name text not null,
  industry text,
  website text,
  logo_url text,
  created_at timestamptz default now()
);

-- The marketplace transaction object
create table campaigns (
  id uuid primary key default gen_random_uuid(),
  brand_id uuid not null references brand_profiles(id),
  title text not null,
  description text not null,
  budget numeric(10,2) not null,
  category text not null,
  requirements jsonb,
  status text not null default 'draft',
  created_at timestamptz default now()
);

-- The matching/application layer
create table applications (
  id uuid primary key default gen_random_uuid(),
  campaign_id uuid not null references campaigns(id),
  creator_id uuid not null references creator_profiles(id),
  pitch text,
  proposed_rate numeric(10,2),
  status text not null default 'pending',
  created_at timestamptz default now(),
  unique(campaign_id, creator_id)
);

Notice the applications table. This is the matching layer — the core of any marketplace. The exact shape varies. In Uber, matching is algorithmic and instant. In Airbnb, it is a request/approval flow. In Vincelio, creators apply to campaigns and brands select who they want to work with. But in every case, there is a table (or service) that connects supply to demand.

Two-sided marketplace connecting buyers and sellers on a digital platform

Matching: The Core of the Marketplace

Matching is where marketplaces differentiate. The matching mechanism determines the user experience more than any other feature.

There are three common patterns:

1. Manual matching (marketplace curates). The platform reviews both sides and makes introductions. This is high-touch, does not scale, but works well for early-stage marketplaces building trust. Some enterprise marketplaces stay here permanently.

2. Search and apply. Supply or demand posts listings. The other side searches, filters, and applies or books. This is what we built for Vincelio — brands post campaigns, creators search and apply. Airbnb and Fiverr also use this model.

3. Algorithmic matching. The platform automatically matches supply to demand based on criteria. Uber does this for rides. DoorDash does this for deliveries. This requires significant data and iteration to get right.

For most marketplace startups, pattern 2 is the right starting point. It is the simplest to build, gives users control, and generates data you can later use for algorithmic matching.

The search and filter system is where you invest engineering effort:

// api/campaigns/search.ts
interface CampaignSearchParams {
  category?: string;
  minBudget?: number;
  maxBudget?: number;
  location?: string;
  sortBy?: 'newest' | 'budget_high' | 'budget_low';
  page?: number;
  limit?: number;
}

async function searchCampaigns(params: CampaignSearchParams) {
  let query = supabase
    .from('campaigns')
    .select(`
      *,
      brand:brand_profiles(company_name, logo_url, industry),
      application_count:applications(count)
    `)
    .eq('status', 'active');

  if (params.category) {
    query = query.eq('category', params.category);
  }
  if (params.minBudget) {
    query = query.gte('budget', params.minBudget);
  }
  if (params.maxBudget) {
    query = query.lte('budget', params.maxBudget);
  }
  if (params.location) {
    query = query.ilike('location', `%${params.location}%`);
  }

  const sortMap = {
    newest: { column: 'created_at', ascending: false },
    budget_high: { column: 'budget', ascending: false },
    budget_low: { column: 'budget', ascending: true },
  };

  const sort = sortMap[params.sortBy || 'newest'];
  query = query.order(sort.column, { ascending: sort.ascending });

  const page = params.page || 1;
  const limit = params.limit || 20;
  query = query.range((page - 1) * limit, page * limit - 1);

  return query;
}

Over time, you layer in relevance signals. Show creators campaigns that match their category. Boost campaigns from brands that have good completion rates. Surface creators who have high ratings in the brand’s category. This is how you evolve from simple search to smart matching without building an ML pipeline on day one.

Payments: Stripe Connect

Marketplace payments are fundamentally different from SaaS payments. In SaaS, the customer pays you. In a marketplace, the customer pays the seller through you. You are a payment facilitator, and that comes with legal and technical complexity.

We use Stripe Connect for every marketplace we build. It handles the hard parts: onboarding sellers with identity verification, splitting payments between seller and platform, handling tax reporting, and managing payouts.

The flow works like this:

// 1. Onboard a creator with Stripe Connect
async function createConnectedAccount(creatorId: string, email: string) {
  const account = await stripe.accounts.create({
    type: 'express',
    email,
    capabilities: {
      transfers: { requested: true },
    },
    metadata: { creatorId },
  });

  // Store the Stripe account ID
  await db
    .update(creatorProfiles)
    .set({ stripeAccountId: account.id })
    .where(eq(creatorProfiles.id, creatorId));

  // Generate onboarding link
  const accountLink = await stripe.accountLinks.create({
    account: account.id,
    refresh_url: `${APP_URL}/settings/payments`,
    return_url: `${APP_URL}/settings/payments?setup=complete`,
    type: 'account_onboarding',
  });

  return accountLink.url;
}

// 2. Process a payment with platform fee
async function processPayment(
  campaignId: string,
  creatorId: string,
  amount: number
) {
  const creator = await db.query.creatorProfiles.findFirst({
    where: eq(creatorProfiles.id, creatorId),
  });

  const platformFee = Math.round(amount * 0.10); // 10% platform fee

  const paymentIntent = await stripe.paymentIntents.create({
    amount: Math.round(amount * 100), // cents
    currency: 'usd',
    application_fee_amount: platformFee * 100,
    transfer_data: {
      destination: creator!.stripeAccountId!,
    },
    metadata: {
      campaignId,
      creatorId,
    },
  });

  return paymentIntent;
}

There are two Stripe Connect account types to consider:

  • Express accounts: Stripe handles the onboarding UI and identity verification. The seller interacts with Stripe’s hosted dashboard for payouts. Less customizable, much less work.
  • Custom accounts: You build the entire onboarding and payout UI. Full control, significant compliance burden.

We always start with Express accounts. For Vincelio, Express accounts meant creators could be onboarded and receiving payments within minutes, with Stripe handling all the KYC and tax complexity for multiple Latin American countries. Building that ourselves would have added months to the project.

E-commerce platform interface with product listings and design elements

Trust and Reviews

Marketplaces live and die by trust. Neither side will transact if they do not trust the other party and the platform.

The trust system has several components:

Verification. Confirm that users are who they claim to be. For creators on Vincelio, this means verifying their social media accounts and audience metrics. For brands, it means verifying the company identity. Stripe Connect Express handles financial verification automatically.

Reviews and ratings. After a transaction completes, both sides rate each other. This creates a feedback loop that rewards good behavior and surfaces bad actors.

create table reviews (
  id uuid primary key default gen_random_uuid(),
  campaign_id uuid not null references campaigns(id),
  reviewer_id uuid not null references users(id),
  reviewee_id uuid not null references users(id),
  rating int not null check (rating between 1 and 5),
  comment text,
  created_at timestamptz default now(),
  unique(campaign_id, reviewer_id)
);

-- Trigger to update aggregate rating
create or replace function update_creator_rating()
returns trigger as $$
begin
  update creator_profiles
  set
    rating = (
      select avg(rating)::numeric(3,2)
      from reviews
      where reviewee_id = (
        select user_id from creator_profiles where id = NEW.reviewee_id
      )
    ),
    review_count = (
      select count(*)
      from reviews
      where reviewee_id = (
        select user_id from creator_profiles where id = NEW.reviewee_id
      )
    )
  where user_id = NEW.reviewee_id;
  return NEW;
end;
$$ language plpgsql;

create trigger trg_update_rating
after insert on reviews
for each row execute function update_creator_rating();

Escrow and milestones. Money should not transfer until both sides are satisfied. For Vincelio, the brand pays when they approve the delivered content, not when the creator submits it. The funds are held by the platform (via Stripe) until approval.

Dispute resolution. Things go wrong. Content does not meet the brief. A seller does not deliver. The platform needs a process for handling disputes, even if that process is manual at first.

The Chicken-and-Egg Problem

The hardest problem in building a marketplace is not technical. It is the cold start problem. You need supply to attract demand, and demand to attract supply. Neither side wants to be first.

There are several strategies we have seen work:

Seed the supply side manually. When we launched Vincelio, we onboarded creators directly before opening the platform to brands. The creators were already active on social media — the platform gave them a new channel to monetize their audience. By the time brands arrived, there was a catalog of creators to choose from.

Start as a single-player tool. Build something useful for one side of the marketplace even without the other side. Trackelio works as a feedback collection tool even if only one product team uses it. The marketplace dynamics (sharing feedback across teams, benchmarking) come later.

Constrain the market. Do not try to be Uber for everything on day one. Start with one city, one category, one niche. Vincelio started with Mexican creators in specific content categories. A narrow market is easier to fill on both sides.

Online marketplace business with transactions and customer engagement

Notifications and Activity Feeds

Marketplace users need to know when things happen. A new application came in. Your application was accepted. Payment was released. Content was approved.

We build a notification system early because marketplace engagement depends on timely communication:

// lib/notifications.ts
type NotificationType =
  | 'application_received'
  | 'application_accepted'
  | 'application_rejected'
  | 'content_submitted'
  | 'content_approved'
  | 'payment_released';

interface Notification {
  userId: string;
  type: NotificationType;
  title: string;
  body: string;
  actionUrl: string;
  read: boolean;
}

async function notify(userId: string, type: NotificationType, data: Record<string, string>) {
  const templates: Record<NotificationType, { title: string; body: string; actionUrl: string }> = {
    application_received: {
      title: 'New application',
      body: `${data.creatorName} applied to ${data.campaignTitle}`,
      actionUrl: `/campaigns/${data.campaignId}/applications`,
    },
    application_accepted: {
      title: 'Application accepted',
      body: `Your application for ${data.campaignTitle} was accepted`,
      actionUrl: `/campaigns/${data.campaignId}`,
    },
    // ... other templates
  };

  const template = templates[type];

  // Save to database for in-app notifications
  await db.insert(notifications).values({
    userId,
    type,
    ...template,
    read: false,
  });

  // Send email for important notifications
  if (['application_accepted', 'payment_released'].includes(type)) {
    await sendNotificationEmail(userId, template);
  }
}

The decision of what gets an email versus what stays in-app is important. Too many emails and users unsubscribe from everything. Too few and they miss critical updates. We default to in-app only and add email for transaction-critical events: money movement, approvals, and deadlines.

Admin and Moderation

Every marketplace needs an admin layer that the end users never see. Content moderation, dispute resolution, user verification, platform metrics.

We build a simple admin dashboard from week one. It does not need to be fancy. It needs to show:

  • New users awaiting verification
  • Active disputes
  • Transaction volume and platform fee revenue
  • Flagged content or users
  • Key marketplace health metrics (supply/demand ratio, time to first transaction, repeat transaction rate)

The marketplace health metrics matter more than vanity metrics. If supply is growing but demand is flat, you have a problem. If time to first transaction is increasing, your matching is getting worse. These numbers tell you where to invest next.

What We Would Build Differently

Looking back at Vincelio and the lessons learned:

Invest in onboarding earlier. The biggest drop-off in any marketplace is between signup and first transaction. We should have built a guided onboarding flow (complete your profile, set your rates, browse your first campaigns) from the start instead of adding it later.

Build messaging into the platform. We initially relied on external communication between creators and brands. That was a mistake. Platform messaging gives you data about transaction quality, helps with dispute resolution, and prevents disintermediation (users taking transactions off-platform to avoid fees).

Start with manual matching. Our first version had search only. We should have manually matched the first 50 creator-brand pairs to learn what makes a good match before building automated recommendations.

These are not technical failures. They are product decisions that we now bake into every marketplace project from the start.

If you are building a marketplace and want a team that has done it before, reach out at hello@threshline.com. We will help you navigate the technical and product decisions that make or break two-sided platforms.