- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
666 lines
19 KiB
Markdown
666 lines
19 KiB
Markdown
# Marketplace Platform Development
|
||
|
||
**Meta-Description:** Multi-Vendor Marketplace bauen. Seller Management, Product Listings, Escrow Payments und Commission Handling.
|
||
|
||
**Keywords:** Marketplace, Multi-Vendor, E-Commerce, Seller Management, Commission, Escrow, Platform Business
|
||
|
||
---
|
||
|
||
## Einführung
|
||
|
||
**Marketplace Platforms** verbinden Käufer und Verkäufer. Von **Seller Onboarding** bis **Commission Splitting** – die technischen Herausforderungen sind komplex. Dieser Guide zeigt die Architektur und Implementation eines Multi-Vendor Marketplaces.
|
||
|
||
---
|
||
|
||
## Marketplace Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ MARKETPLACE PLATFORM ARCHITECTURE │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Stakeholders: │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ Platform (You) │ │
|
||
│ │ ├── Commission on sales │ │
|
||
│ │ ├── Subscription fees │ │
|
||
│ │ └── Value-added services │ │
|
||
│ │ │ │
|
||
│ │ Sellers │ │
|
||
│ │ ├── List products │ │
|
||
│ │ ├── Receive payouts │ │
|
||
│ │ └── Manage inventory │ │
|
||
│ │ │ │
|
||
│ │ Buyers │ │
|
||
│ │ ├── Browse & purchase │ │
|
||
│ │ ├── Reviews & ratings │ │
|
||
│ │ └── Dispute resolution │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Payment Flow: │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ Buyer pays $100 │ │
|
||
│ │ ↓ │ │
|
||
│ │ Platform receives $100 (Stripe Connect) │ │
|
||
│ │ ↓ │ │
|
||
│ │ Platform takes 15% commission ($15) │ │
|
||
│ │ ↓ │ │
|
||
│ │ Seller receives $85 (after payout delay) │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Core Features: │
|
||
│ ├── Seller Dashboard │
|
||
│ ├── Product Catalog │
|
||
│ ├── Shopping Cart │
|
||
│ ├── Checkout (Split Payments) │
|
||
│ ├── Order Management │
|
||
│ ├── Reviews & Ratings │
|
||
│ ├── Dispute Resolution │
|
||
│ └── Analytics & Reporting │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Database Schema
|
||
|
||
```typescript
|
||
// prisma/schema.prisma
|
||
model Seller {
|
||
id String @id @default(cuid())
|
||
userId String @unique
|
||
user User @relation(fields: [userId], references: [id])
|
||
|
||
// Business Info
|
||
businessName String
|
||
businessType String // individual, company
|
||
taxId String?
|
||
description String?
|
||
logo String?
|
||
|
||
// Stripe Connect
|
||
stripeAccountId String? @unique
|
||
stripeOnboarded Boolean @default(false)
|
||
payoutEnabled Boolean @default(false)
|
||
|
||
// Settings
|
||
commissionRate Float @default(0.15) // 15%
|
||
payoutSchedule String @default("daily") // daily, weekly, monthly
|
||
|
||
// Status
|
||
status SellerStatus @default(PENDING)
|
||
verifiedAt DateTime?
|
||
|
||
// Relations
|
||
products Product[]
|
||
orders OrderItem[]
|
||
payouts Payout[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
enum SellerStatus {
|
||
PENDING
|
||
ACTIVE
|
||
SUSPENDED
|
||
BANNED
|
||
}
|
||
|
||
model Product {
|
||
id String @id @default(cuid())
|
||
sellerId String
|
||
seller Seller @relation(fields: [sellerId], references: [id])
|
||
|
||
// Product Info
|
||
name String
|
||
slug String
|
||
description String
|
||
price Int // in cents
|
||
compareAtPrice Int?
|
||
currency String @default("EUR")
|
||
|
||
// Inventory
|
||
sku String?
|
||
inventory Int @default(0)
|
||
trackInventory Boolean @default(true)
|
||
|
||
// Categorization
|
||
categoryId String?
|
||
category Category? @relation(fields: [categoryId], references: [id])
|
||
tags String[]
|
||
|
||
// Media
|
||
images ProductImage[]
|
||
|
||
// Status
|
||
status ProductStatus @default(DRAFT)
|
||
publishedAt DateTime?
|
||
|
||
// Stats
|
||
viewCount Int @default(0)
|
||
salesCount Int @default(0)
|
||
rating Float?
|
||
reviewCount Int @default(0)
|
||
|
||
// Relations
|
||
variants ProductVariant[]
|
||
orderItems OrderItem[]
|
||
reviews Review[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([sellerId, slug])
|
||
@@index([categoryId])
|
||
@@index([status])
|
||
}
|
||
|
||
model Order {
|
||
id String @id @default(cuid())
|
||
orderNumber String @unique
|
||
|
||
// Buyer
|
||
userId String
|
||
user User @relation(fields: [userId], references: [id])
|
||
|
||
// Totals
|
||
subtotal Int // Sum of items
|
||
platformFee Int // Platform commission
|
||
shippingTotal Int
|
||
taxTotal Int
|
||
total Int
|
||
|
||
// Payment
|
||
stripePaymentId String?
|
||
paymentStatus PaymentStatus @default(PENDING)
|
||
paidAt DateTime?
|
||
|
||
// Shipping
|
||
shippingAddress Json
|
||
shippingMethod String?
|
||
|
||
// Status
|
||
status OrderStatus @default(PENDING)
|
||
|
||
// Items (can be from multiple sellers)
|
||
items OrderItem[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
model OrderItem {
|
||
id String @id @default(cuid())
|
||
orderId String
|
||
order Order @relation(fields: [orderId], references: [id])
|
||
|
||
// Product
|
||
productId String
|
||
product Product @relation(fields: [productId], references: [id])
|
||
variantId String?
|
||
|
||
// Seller
|
||
sellerId String
|
||
seller Seller @relation(fields: [sellerId], references: [id])
|
||
|
||
// Pricing
|
||
price Int
|
||
quantity Int
|
||
subtotal Int // price * quantity
|
||
commission Int // Platform commission
|
||
sellerPayout Int // subtotal - commission
|
||
|
||
// Status (per-seller fulfillment)
|
||
status OrderItemStatus @default(PENDING)
|
||
shippedAt DateTime?
|
||
trackingNumber String?
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
model Payout {
|
||
id String @id @default(cuid())
|
||
sellerId String
|
||
seller Seller @relation(fields: [sellerId], references: [id])
|
||
|
||
amount Int
|
||
currency String @default("EUR")
|
||
status PayoutStatus @default(PENDING)
|
||
|
||
stripePayoutId String?
|
||
paidAt DateTime?
|
||
failedAt DateTime?
|
||
failureReason String?
|
||
|
||
// Payout period
|
||
periodStart DateTime
|
||
periodEnd DateTime
|
||
|
||
createdAt DateTime @default(now())
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Seller Onboarding
|
||
|
||
```typescript
|
||
// lib/marketplace/seller-onboarding.ts
|
||
import { stripe } from '@/lib/stripe';
|
||
import { db } from '@/lib/db';
|
||
|
||
export async function createSellerAccount(
|
||
userId: string,
|
||
data: {
|
||
businessName: string;
|
||
businessType: 'individual' | 'company';
|
||
country: string;
|
||
email: string;
|
||
}
|
||
): Promise<string> {
|
||
// Create Stripe Connect account
|
||
const account = await stripe.accounts.create({
|
||
type: 'express',
|
||
country: data.country,
|
||
email: data.email,
|
||
business_type: data.businessType,
|
||
business_profile: {
|
||
name: data.businessName
|
||
},
|
||
capabilities: {
|
||
card_payments: { requested: true },
|
||
transfers: { requested: true }
|
||
}
|
||
});
|
||
|
||
// Create seller record
|
||
await db.seller.create({
|
||
data: {
|
||
userId,
|
||
businessName: data.businessName,
|
||
businessType: data.businessType,
|
||
stripeAccountId: account.id,
|
||
status: 'PENDING'
|
||
}
|
||
});
|
||
|
||
// Generate onboarding link
|
||
const accountLink = 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/onboarding/complete`,
|
||
type: 'account_onboarding'
|
||
});
|
||
|
||
return accountLink.url;
|
||
}
|
||
|
||
export async function completeSellerOnboarding(
|
||
stripeAccountId: string
|
||
): Promise<void> {
|
||
// Check account status
|
||
const account = await stripe.accounts.retrieve(stripeAccountId);
|
||
|
||
if (account.charges_enabled && account.payouts_enabled) {
|
||
await db.seller.update({
|
||
where: { stripeAccountId },
|
||
data: {
|
||
stripeOnboarded: true,
|
||
payoutEnabled: true,
|
||
status: 'ACTIVE',
|
||
verifiedAt: new Date()
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Webhook handler for account updates
|
||
export async function handleAccountUpdated(
|
||
account: Stripe.Account
|
||
): Promise<void> {
|
||
await db.seller.update({
|
||
where: { stripeAccountId: account.id },
|
||
data: {
|
||
payoutEnabled: account.payouts_enabled || false,
|
||
stripeOnboarded: account.details_submitted || false
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Split Payments
|
||
|
||
```typescript
|
||
// lib/marketplace/checkout.ts
|
||
import { stripe } from '@/lib/stripe';
|
||
import { db } from '@/lib/db';
|
||
import { nanoid } from 'nanoid';
|
||
|
||
interface CartItem {
|
||
productId: string;
|
||
quantity: number;
|
||
variantId?: string;
|
||
}
|
||
|
||
export async function createMarketplaceCheckout(
|
||
userId: string,
|
||
items: CartItem[],
|
||
shippingAddress: any
|
||
): Promise<string> {
|
||
// Group items by seller
|
||
const itemsBySeller = await groupItemsBySeller(items);
|
||
|
||
// Calculate totals
|
||
const { subtotal, platformFee, lineItems, transfers } =
|
||
await calculateOrderTotals(itemsBySeller);
|
||
|
||
// Create order
|
||
const order = await db.order.create({
|
||
data: {
|
||
orderNumber: `ORD-${nanoid(10).toUpperCase()}`,
|
||
userId,
|
||
subtotal,
|
||
platformFee,
|
||
shippingTotal: 0, // Calculate based on sellers
|
||
taxTotal: 0,
|
||
total: subtotal,
|
||
shippingAddress,
|
||
status: 'PENDING',
|
||
items: {
|
||
create: lineItems.map(item => ({
|
||
productId: item.productId,
|
||
sellerId: item.sellerId,
|
||
variantId: item.variantId,
|
||
price: item.price,
|
||
quantity: item.quantity,
|
||
subtotal: item.subtotal,
|
||
commission: item.commission,
|
||
sellerPayout: item.sellerPayout,
|
||
status: 'PENDING'
|
||
}))
|
||
}
|
||
}
|
||
});
|
||
|
||
// Create Stripe Checkout with transfers
|
||
const session = await stripe.checkout.sessions.create({
|
||
mode: 'payment',
|
||
customer_email: (await db.user.findUnique({ where: { id: userId } }))?.email,
|
||
line_items: lineItems.map(item => ({
|
||
price_data: {
|
||
currency: 'eur',
|
||
unit_amount: item.price,
|
||
product_data: {
|
||
name: item.productName,
|
||
images: item.images
|
||
}
|
||
},
|
||
quantity: item.quantity
|
||
})),
|
||
payment_intent_data: {
|
||
// Split payment to sellers
|
||
transfer_group: order.id,
|
||
metadata: {
|
||
orderId: order.id
|
||
}
|
||
},
|
||
success_url: `${process.env.NEXT_PUBLIC_URL}/orders/${order.id}/success`,
|
||
cancel_url: `${process.env.NEXT_PUBLIC_URL}/cart`,
|
||
metadata: {
|
||
orderId: order.id
|
||
}
|
||
});
|
||
|
||
return session.url!;
|
||
}
|
||
|
||
async function calculateOrderTotals(itemsBySeller: Map<string, CartItem[]>) {
|
||
const lineItems: any[] = [];
|
||
let subtotal = 0;
|
||
let platformFee = 0;
|
||
const transfers: any[] = [];
|
||
|
||
for (const [sellerId, items] of itemsBySeller) {
|
||
const seller = await db.seller.findUnique({
|
||
where: { id: sellerId }
|
||
});
|
||
|
||
for (const item of items) {
|
||
const product = await db.product.findUnique({
|
||
where: { id: item.productId }
|
||
});
|
||
|
||
if (!product) continue;
|
||
|
||
const itemSubtotal = product.price * item.quantity;
|
||
const commission = Math.round(itemSubtotal * (seller?.commissionRate || 0.15));
|
||
const sellerPayout = itemSubtotal - commission;
|
||
|
||
subtotal += itemSubtotal;
|
||
platformFee += commission;
|
||
|
||
lineItems.push({
|
||
productId: product.id,
|
||
productName: product.name,
|
||
sellerId,
|
||
variantId: item.variantId,
|
||
price: product.price,
|
||
quantity: item.quantity,
|
||
subtotal: itemSubtotal,
|
||
commission,
|
||
sellerPayout,
|
||
images: [] // Add product images
|
||
});
|
||
|
||
// Prepare transfer for seller
|
||
transfers.push({
|
||
sellerId,
|
||
stripeAccountId: seller?.stripeAccountId,
|
||
amount: sellerPayout
|
||
});
|
||
}
|
||
}
|
||
|
||
return { subtotal, platformFee, lineItems, transfers };
|
||
}
|
||
|
||
// After successful payment, create transfers
|
||
export async function createSellerTransfers(orderId: string): Promise<void> {
|
||
const order = await db.order.findUnique({
|
||
where: { id: orderId },
|
||
include: {
|
||
items: {
|
||
include: { seller: true }
|
||
}
|
||
}
|
||
});
|
||
|
||
if (!order) return;
|
||
|
||
// Group payouts by seller
|
||
const payoutsBySeller = new Map<string, number>();
|
||
|
||
for (const item of order.items) {
|
||
const current = payoutsBySeller.get(item.sellerId) || 0;
|
||
payoutsBySeller.set(item.sellerId, current + item.sellerPayout);
|
||
}
|
||
|
||
// Create transfers
|
||
for (const [sellerId, amount] of payoutsBySeller) {
|
||
const seller = order.items.find(i => i.sellerId === sellerId)?.seller;
|
||
|
||
if (!seller?.stripeAccountId) continue;
|
||
|
||
await stripe.transfers.create({
|
||
amount,
|
||
currency: 'eur',
|
||
destination: seller.stripeAccountId,
|
||
transfer_group: orderId,
|
||
metadata: {
|
||
orderId,
|
||
sellerId
|
||
}
|
||
});
|
||
}
|
||
|
||
// Update order status
|
||
await db.order.update({
|
||
where: { id: orderId },
|
||
data: {
|
||
paymentStatus: 'PAID',
|
||
paidAt: new Date()
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Seller Dashboard
|
||
|
||
```tsx
|
||
// app/seller/dashboard/page.tsx
|
||
import { getServerSession } from 'next-auth';
|
||
import { db } from '@/lib/db';
|
||
|
||
export default async function SellerDashboard() {
|
||
const session = await getServerSession();
|
||
const seller = await db.seller.findUnique({
|
||
where: { userId: session!.user.id }
|
||
});
|
||
|
||
const [stats, recentOrders, topProducts] = await Promise.all([
|
||
getSellerStats(seller!.id),
|
||
getRecentOrders(seller!.id),
|
||
getTopProducts(seller!.id)
|
||
]);
|
||
|
||
return (
|
||
<div className="p-6 space-y-6">
|
||
<h1 className="text-2xl font-bold">Seller Dashboard</h1>
|
||
|
||
{/* Stats Cards */}
|
||
<div className="grid grid-cols-4 gap-4">
|
||
<StatCard
|
||
title="Revenue (30d)"
|
||
value={`€${(stats.revenue / 100).toFixed(2)}`}
|
||
change={stats.revenueChange}
|
||
/>
|
||
<StatCard
|
||
title="Orders"
|
||
value={stats.orderCount}
|
||
change={stats.orderChange}
|
||
/>
|
||
<StatCard
|
||
title="Products"
|
||
value={stats.productCount}
|
||
/>
|
||
<StatCard
|
||
title="Pending Payout"
|
||
value={`€${(stats.pendingPayout / 100).toFixed(2)}`}
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-6">
|
||
{/* Recent Orders */}
|
||
<div className="bg-white rounded-lg shadow p-6">
|
||
<h2 className="text-lg font-semibold mb-4">Recent Orders</h2>
|
||
<OrderList orders={recentOrders} />
|
||
</div>
|
||
|
||
{/* Top Products */}
|
||
<div className="bg-white rounded-lg shadow p-6">
|
||
<h2 className="text-lg font-semibold mb-4">Top Products</h2>
|
||
<ProductList products={topProducts} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
async function getSellerStats(sellerId: string) {
|
||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||
|
||
const [revenue, orders, products, pendingPayout] = await Promise.all([
|
||
db.orderItem.aggregate({
|
||
where: {
|
||
sellerId,
|
||
order: { paidAt: { gte: thirtyDaysAgo } }
|
||
},
|
||
_sum: { sellerPayout: true }
|
||
}),
|
||
db.orderItem.count({
|
||
where: {
|
||
sellerId,
|
||
createdAt: { gte: thirtyDaysAgo }
|
||
}
|
||
}),
|
||
db.product.count({ where: { sellerId } }),
|
||
db.orderItem.aggregate({
|
||
where: {
|
||
sellerId,
|
||
order: { paymentStatus: 'PAID' },
|
||
// Not yet paid out
|
||
},
|
||
_sum: { sellerPayout: true }
|
||
})
|
||
]);
|
||
|
||
return {
|
||
revenue: revenue._sum.sellerPayout || 0,
|
||
revenueChange: 0, // Calculate vs previous period
|
||
orderCount: orders,
|
||
orderChange: 0,
|
||
productCount: products,
|
||
pendingPayout: pendingPayout._sum.sellerPayout || 0
|
||
};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Platform Economics
|
||
|
||
| Revenue Stream | Example |
|
||
|---------------|---------|
|
||
| **Commission** | 10-20% per sale |
|
||
| **Listing Fee** | $0.20 per listing |
|
||
| **Subscription** | $29/mo for premium |
|
||
| **Promoted Listings** | $0.10 per click |
|
||
| **Transaction Fee** | 2.9% + $0.30 |
|
||
|
||
---
|
||
|
||
## Fazit
|
||
|
||
Marketplace Platform Development erfordert:
|
||
|
||
1. **Multi-Party Payments**: Stripe Connect für Split Payments
|
||
2. **Seller Management**: Onboarding und Verification
|
||
3. **Trust & Safety**: Reviews, Disputes, Moderation
|
||
4. **Scalability**: Handling vieler Verkäufer und Produkte
|
||
|
||
Marketplaces sind komplex, aber hochprofitabel.
|
||
|
||
---
|
||
|
||
## Bildprompts
|
||
|
||
1. "Marketplace platform architecture diagram, buyers sellers and platform"
|
||
2. "Seller dashboard interface, revenue charts and order list"
|
||
3. "Payment flow visualization, split payment to multiple sellers"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [Stripe Connect](https://stripe.com/docs/connect)
|
||
- [Marketplace Business Models](https://a16z.com/marketplace-100/)
|
||
- [Sharetribe Marketplace Guide](https://www.sharetribe.com/academy/)
|
||
- [Building Marketplaces](https://www.nfx.com/post/marketplace-handbook)
|