Build eBay Clone with Next.js and Stripe Payment Integration 2025

Build eBay Clone with Next.js and Stripe Payment Integration 2025

Recent market news about GameStop's acquisition interest in eBay has sparked renewed interest in understanding how modern marketplace platforms work. If you're a developer tasked with building a marketplace application similar to eBay's core functionality, this guide walks you through creating a production-ready clone using Next.js, Stripe for payments, and PostgreSQL for data persistence.

Why Build an eBay Clone in 2025?

Marketplace platforms remain one of the most complex yet valuable software products to build. Understanding how to architect a multi-vendor, payment-enabled marketplace teaches you:

  • User authentication and authorization
  • Payment processing at scale
  • Database design for transactional systems
  • Real-time inventory management
  • Secure seller/buyer interactions

This project is ideal for intermediate developers looking to move beyond CRUD applications into real-world commerce systems.

Architecture Overview

Here's the tech stack we'll use:

| Component | Technology | Purpose | |-----------|-----------|----------| | Frontend | Next.js 14+ (App Router) | UI, Server Actions, API routes | | Backend | Node.js + Next.js API | Business logic, payment processing | | Database | PostgreSQL | User accounts, listings, orders, transactions | | Payments | Stripe API | Payment processing, payout management | | Storage | AWS S3 or Vercel Blob | Product images | | Auth | NextAuth.js v5 | User authentication |

Step 1: Project Setup and Dependencies

Start by creating a new Next.js project with TypeScript:

npx create-next-app@latest ebay-clone --typescript --tailwind --eslint
cd ebay-clone
npm install stripe @stripe/react-stripe-js next-auth prisma @prisma/client bcryptjs
npm install -D @types/bcryptjs

Initialize Prisma for database management:

npx prisma init

Step 2: Database Schema Design

Create a comprehensive Prisma schema that supports marketplace operations:

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id            String    @id @default(cuid())
  email         String    @unique
  name          String?
  passwordHash  String
  role          UserRole  @default(BUYER)
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
  
  listings      Listing[]
  orders        Order[]
  reviews       Review[]
  stripeId      String?   @unique
}

enum UserRole {
  BUYER
  SELLER
  ADMIN
}

model Listing {
  id          String    @id @default(cuid())
  title       String
  description String
  price       Float
  category    String
  condition   String    // New, Used, Refurbished
  images      String[]  // URLs to S3/Blob
  sellerId    String
  seller      User      @relation(fields: [sellerId], references: [id], onDelete: Cascade)
  
  bids        Bid[]
  orders      OrderItem[]
  createdAt   DateTime  @default(now())
  expiresAt   DateTime
  
  @@index([sellerId])
  @@index([category])
}

model Bid {
  id          String    @id @default(cuid())
  listingId   String
  listing     Listing   @relation(fields: [listingId], references: [id], onDelete: Cascade)
  buyerId     String
  buyer       User      @relation(fields: [buyerId], references: [id], onDelete: Cascade)
  amount      Float
  createdAt   DateTime  @default(now())
  
  @@unique([listingId, buyerId])
  @@index([listingId])
}

model Order {
  id              String      @id @default(cuid())
  buyerId         String
  buyer           User        @relation(fields: [buyerId], references: [id])
  items           OrderItem[]
  status          OrderStatus @default(PENDING_PAYMENT)
  totalAmount     Float
  stripePaymentId String?     @unique
  createdAt       DateTime    @default(now())
  updatedAt       DateTime    @updatedAt
  
  @@index([buyerId])
  @@index([status])
}

enum OrderStatus {
  PENDING_PAYMENT
  PAID
  SHIPPED
  DELIVERED
  CANCELLED
  REFUNDED
}

model OrderItem {
  id        String  @id @default(cuid())
  orderId   String
  order     Order   @relation(fields: [orderId], references: [id], onDelete: Cascade)
  listingId String
  listing   Listing @relation(fields: [listingId], references: [id])
  quantity  Int
  price     Float
  
  @@index([orderId])
}

model Review {
  id       String  @id @default(cuid())
  rating   Int     // 1-5
  comment  String?
  authorId String
  author   User    @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
}

Step 3: Configure Stripe Webhook Handler

Create a Stripe webhook endpoint to handle payment confirmations:

// app/api/webhooks/stripe/route.ts

import { stripe } from '@/lib/stripe';
import { prisma } from '@/lib/prisma';
import { headers } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';

const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(req: NextRequest) {
  const body = await req.text();
  const headersList = await headers();
  const sig = headersList.get('stripe-signature')!;

  let event;

  try {
    event = stripe.webhooks.constructEvent(body, sig, endpointSecret);
  } catch (err) {
    return NextResponse.json(
      { error: `Webhook signature verification failed` },
      { status: 400 }
    );
  }

  if (event.type === 'payment_intent.succeeded') {
    const paymentIntent = event.data.object;
    
    // Update order status in database
    await prisma.order.update({
      where: { stripePaymentId: paymentIntent.id },
      data: { status: 'PAID' }
    });
  }

  return NextResponse.json({ received: true });
}

Step 4: Implement Checkout Flow

Create a server action to initiate Stripe payments:

// app/actions/checkout.ts

'use server';

import { stripe } from '@/lib/stripe';
import { prisma } from '@/lib/prisma';
import { redirect } from 'next/navigation';

export async function createCheckoutSession(
  userId: string,
  items: Array<{ listingId: string; quantity: number }>
) {
  // Calculate total and validate items
  let total = 0;
  const lineItems = [];

  for (const item of items) {
    const listing = await prisma.listing.findUnique({
      where: { id: item.listingId }
    });

    if (!listing) throw new Error('Listing not found');
    
    const amount = Math.round(listing.price * item.quantity * 100);
    total += amount;
    
    lineItems.push({
      price_data: {
        currency: 'usd',
        product_data: {
          name: listing.title,
          images: [listing.images[0]]
        },
        unit_amount: Math.round(listing.price * 100)
      },
      quantity: item.quantity
    });
  }

  // Create order first
  const order = await prisma.order.create({
    data: {
      buyerId: userId,
      totalAmount: total / 100,
      items: {
        create: items.map(item => ({
          listingId: item.listingId,
          quantity: item.quantity,
          price: 0 // Will be calculated from listing
        }))
      }
    }
  });

  // Create Stripe session
  const session = await stripe.checkout.sessions.create({
    line_items: lineItems,
    mode: 'payment',
    success_url: `${process.env.NEXT_PUBLIC_URL}/orders/${order.id}?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_URL}/cart`,
    metadata: {
      orderId: order.id
    }
  });

  if (session.url) {
    redirect(session.url);
  }
}

Step 5: Deploy with Vercel

For production deployment, use Vercel which integrates seamlessly with Next.js:

  1. Push your code to GitHub
  2. Connect your repository to Vercel
  3. Set environment variables (DATABASE_URL, STRIPE_SECRET_KEY, etc.)
  4. Deploy automatically on every push

Common Pitfalls to Avoid

  • Race conditions on bid updates: Use database transactions with Prisma's $transaction
  • Missing inventory validation: Always verify listing exists before order creation
  • Webhook timeout issues: Process Stripe events asynchronously with queues
  • PII exposure: Never log sensitive payment data; let Stripe handle it
  • Concurrent payment attempts: Implement idempotency keys in Stripe requests

Testing the Integration

Use Stripe's test mode with these credentials:

  • Test card: 4242 4242 4242 4242
  • Any future expiry date
  • Any 3-digit CVC

Run migrations and start development:

npx prisma migrate dev --name init
npm run dev

Conclusion

Building an eBay-like marketplace with Next.js and Stripe teaches you production-grade patterns for payment processing, user management, and inventory control. The architecture scales from MVP to millions of listings through proper database indexing and caching strategies.

Recommended Tools

  • VercelDeploy frontend apps instantly with zero config
  • SupabaseOpen source Firebase alternative with Postgres