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:
- Push your code to GitHub
- Connect your repository to Vercel
- Set environment variables (DATABASE_URL, STRIPE_SECRET_KEY, etc.)
- 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.