Building a Multi-Seller Marketplace with Next.js + Whop (No Stripe Connect)
Decoupling Marketplace Payments: A Platform-First Architecture for Multi-Vendor Commerce
Current Situation Analysis
Multi-vendor commerce platforms face a structural bottleneck: payment routing and regulatory compliance consume disproportionate engineering resources. Traditional architectures rely on connected-account systems like Stripe Connect to manage vendor onboarding, KYC verification, and split payments. While financially robust, these systems require developers to manually orchestrate payout schedules, handle tax documentation, and maintain complex webhook state machines. The cognitive overhead often delays product launches by four to six weeks, pushing teams to deprioritize core marketplace features like discovery, reviews, and digital asset delivery.
This friction is frequently misunderstood as an unavoidable cost of doing business. Many engineering teams assume that handling financial routing, identity verification, and fee distribution must be built in-house or tightly coupled to a single payment processor. In reality, the compliance and routing layers can be abstracted into a platform-first SDK without sacrificing control over the checkout experience or revenue split logic.
Industry benchmarks indicate that marketplace payment stacks typically account for 30-40% of initial development time. By delegating KYC, connected account provisioning, and fee routing to a dedicated platform API, teams can reduce payment infrastructure overhead by approximately 70%. The remaining engineering effort focuses on domain-specific logic: digital asset delivery, access control, and user experience. This shift transforms payment routing from a custom engineering project into a declarative configuration step.
WOW Moment: Key Findings
The architectural divergence between traditional connected-account systems and platform-first SDKs becomes evident when measuring operational overhead against feature velocity. The following comparison isolates the engineering impact of each approach:
| Approach | Setup Time | Compliance Burden | Payout Routing Complexity | Webhook Management | Developer Overhead |
|---|---|---|---|---|---|
| Traditional Stripe Connect | 4β6 weeks | High (manual tax/KYC routing) | Custom payout scheduling | Multi-event state machine | High |
| Platform-First SDK (Whop for Platforms) | 3β5 days | Delegated (hosted KYC & verification) | Declarative fee routing | Single-event purchase confirmation | Low |
This finding matters because it decouples financial infrastructure from product development. Teams no longer need to maintain custom payout logic or build KYC verification flows. The platform SDK handles identity verification, fee calculation, and balance distribution automatically. Developers retain full control over the storefront, pricing models, and digital delivery mechanisms while offloading regulatory and financial routing complexity. The result is a faster path to market with reduced operational risk and fewer moving parts in the payment pipeline.
Core Solution
Building a multi-vendor marketplace without traditional connected-account plumbing requires a declarative payment architecture. The system relies on a platform SDK to manage seller onboarding, fee routing, and payout distribution, while the application layer handles product cataloging, digital delivery, and user sessions.
Architecture Decisions
- Framework: Next.js App Router provides server-side rendering for product pages and API routes for webhook handling. Turbopack accelerates local iteration.
- Database: Neon Postgres via Vercel integration ensures environment-specific connection strings without manual configuration. Prisma 7 with
@prisma/adapter-pgprovides type-safe queries and migration management. - Session Management:
iron-sessionhandles encrypted cookie-based sessions. This eliminates Redis dependencies while maintaining secure, stateless authentication for buyer and seller flows. - File Storage: UploadThing manages asset uploads. Public preview images are served via CDN, while downloadable files are gated behind signed URLs generated only after purchase verification.
- Validation: Zod enforces strict input validation for checkout payloads, webhook signatures, and environment variables.
Step-by-Step Implementation
- Seller Onboarding & KYC Routing When a user elects to become a vendor, the application provisions a connected entity through Whop for Platforms. The SDK redirects the user to a hosted verification flow. Upon completion, the vendor receives a unique identifier used for product creation and fee routing.
// services/vendor-onboarding.ts
import { whopClient } from '@/lib/whop-sdk';
import { db } from '@/lib/db';
export async function initiateVendorOnboarding(userId: string) {
const existingVendor = await db.vendor.findUnique({ where: { userId } });
if (existingVendor) return existingVendor.platformId;
const platformCompany = await whopClient.companies.create({
name: `Vendor-${userId}`,
parent_platform_id: process.env.PLATFORM_COMPANY_ID,
});
const vendorRecord = await db.vendor.create({
data: {
userId,
platformId: platformCompany.id,
kycStatus: 'pending',
},
});
const kycRedirect = await whopClient.companies.generateKycUrl(vendorRecord.platformId);
return kycRedirect.redirect_url;
}
- Product Creation & Fee Configuration
Vendors publish digital assets. Each product is registered on the platform with a declarative fee structure. The platform SDK calculates the vendor payout and platform commission automatically during checkout. The 5% platform cut is applied via
application_fee_amount.
// services/product-registry.ts
import { whopClient } from '@/lib/whop-sdk';
import { db } from '@/lib/db';
export async function registerProduct(vendorId: string, productData: {
title: string;
priceInCents: number;
assetKey: string;
}) {
const platformProduct = await whopClient.products.create({
company_id: vendorId,
name: productData.title,
price: productData.priceInCents,
type: 'digital',
});
const checkoutConfig = await whopClient.checkouts.create({
product_id: platformProduct.id,
application_fee_amount: Math.round(productData.priceInCents * 0.05),
currency: 'usd',
});
await db.product.create({
data: {
vendorId,
platformProductId: platformProduct.id,
platformCheckoutId: checkoutConfig.id,
assetKey: productData.assetKey,
priceInCents: productData.priceInCents,
},
});
return checkoutConfig.embed_url;
}
- Webhook Processing & Purchase Verification
The platform SDK emits a
payment.succeededevent upon successful transaction. The application validates the signature, ensures idempotency, and creates a purchase record. Free products bypass checkout entirely and trigger direct purchase creation via a server API route.
// app/api/webhooks/platform/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyWebhookSignature } from '@/lib/crypto';
import { db } from '@/lib/db';
export async function POST(req: NextRequest) {
const payload = await req.text();
const signature = req.headers.get('x-platform-signature') ?? '';
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const event = JSON.parse(payload);
if (event.type !== 'payment.succeeded') {
return NextResponse.json({ status: 'ignored' }, { status: 200 });
}
const existingPurchase = await db.purchase.findUnique({
where: { platformTransactionId: event.data.transaction_id },
});
if (existingPurchase) {
return NextResponse.json({ status: 'duplicate' }, { status: 200 });
}
await db.purchase.create({
data: {
platformTransactionId: event.data.transaction_id,
buyerId: event.data.buyer_id,
productId: event.data.product_id,
amount: event.data.amount,
status: 'completed',
purchasedAt: new Date(event.data.created_at),
},
});
return NextResponse.json({ status: 'processed' }, { status: 200 });
}
- Digital Delivery & Access Control Purchased assets are never exposed directly. The application generates time-limited signed URLs only after verifying purchase ownership. Preview images remain publicly accessible for marketing purposes.
// services/asset-delivery.ts
import { getSignedUrl } from '@uploadthing/sdk';
import { db } from '@/lib/db';
export async function generateDownloadLink(buyerId: string, productId: string) {
const purchase = await db.purchase.findFirst({
where: { buyerId, productId, status: 'completed' },
include: { product: true },
});
if (!purchase) throw new Error('Unauthorized access');
const signedUrl = await getSignedUrl({
fileKey: purchase.product.assetKey,
expiresIn: '1h',
download: true,
});
return signedUrl;
}
Rationale
The architecture separates financial routing from business logic. Whop for Platforms handles KYC, fee calculation, and payout distribution, while the application manages product metadata, purchase verification, and asset delivery. This separation reduces coupling, simplifies testing, and allows independent scaling of payment and delivery systems. Prisma 7βs explicit generate requirement ensures type safety aligns with schema changes, preventing runtime mismatches in production. The deploy-first workflow guarantees that OAuth callback URLs and Neon connection strings are resolved before local development begins, eliminating environment drift.
Pitfall Guide
Ignoring Webhook Idempotency Explanation: Platform SDKs may retry webhook deliveries due to network instability. Processing the same event twice creates duplicate purchase records and triggers multiple asset deliveries. Fix: Always check for existing transaction IDs before creating records. Return a 200 response immediately for duplicates to prevent retry loops.
Hardcoding Platform Fees Explanation: Embedding fee percentages directly in checkout creation logic makes it impossible to adjust commission rates without code deployments. Fix: Store fee configurations in a database table or environment variable. Calculate fees dynamically during checkout registration to support tiered or promotional pricing.
Skipping KYC Verification Status Checks Explanation: Allowing vendors to publish products before KYC completion results in failed payouts and platform policy violations. Fix: Implement a middleware or route guard that checks
kycStatus === 'verified'before enabling product creation or checkout generation.Exposing Downloadable Assets Without Signed URLs Explanation: Serving files directly from public storage buckets allows unauthorized sharing and bypasses purchase verification. Fix: Use time-limited signed URLs generated server-side. Restrict direct bucket access and validate purchase ownership before URL generation.
Mishandling Free vs. Paid Checkout Flows Explanation: Free products still require purchase records for access control. Skipping this step breaks download gating and analytics. Fix: Create a unified purchase creation service that handles both webhook-triggered paid transactions and direct server-side free product claims.
Neglecting Webhook Signature Validation Explanation: Accepting unverified webhook payloads exposes the application to spoofed events, leading to fraudulent purchase records or unauthorized access grants. Fix: Always validate cryptographic signatures using the platform SDKβs verification utility. Reject requests with missing or invalid signatures immediately.
Assuming Connected Accounts Are Instantly Active Explanation: Platform SDKs may take several minutes to propagate KYC completion and account activation across routing systems. Fix: Implement a polling mechanism or webhook listener for
account.activatedevents. Delay product registration until the platform confirms account readiness.
Production Bundle
Action Checklist
- Configure platform SDK credentials and webhook secrets in environment variables
- Set up Prisma 7 schema with explicit
generatestep in deployment pipeline - Implement webhook signature verification and idempotency checks
- Create KYC status guard middleware for vendor product creation routes
- Configure UploadThing with signed URL generation and public preview separation
- Add dynamic fee calculation service instead of hardcoded percentages
- Test free product flow with direct purchase record creation
- Verify payout portal integration for vendor balance management
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Digital templates & code assets | Platform SDK + UploadThing | Handles KYC, fee routing, and signed delivery natively | Low infrastructure cost, platform transaction fees apply |
| Physical goods with shipping | Traditional Stripe Connect + 3PL API | Requires complex shipping logic and inventory tracking | Higher setup cost, manual payout scheduling needed |
| Subscription-based SaaS | Platform SDK with recurring billing module | Simplifies tier management and automated invoicing | Moderate cost, reduced billing engineering overhead |
| High-volume B2B marketplace | Custom payment orchestration + dedicated compliance team | Enterprise contracts require custom settlement terms | High operational cost, extended compliance timeline |
Configuration Template
# .env.local
DATABASE_URL="postgresql://user:pass@host:5432/marketplace"
NEXT_PUBLIC_PLATFORM_CLIENT_ID="your_platform_client_id"
PLATFORM_API_SECRET="your_platform_api_secret"
WEBHOOK_SECRET="whsec_your_webhook_signing_key"
UPLOADTHING_SECRET="your_uploadthing_secret"
UPLOADTHING_APP_ID="your_uploadthing_app_id"
IRON_SESSION_PASSWORD="32-character-random-string-minimum"
// lib/whop-sdk.ts
import { WhopClient } from '@whop/sdk';
export const whopClient = new WhopClient({
clientId: process.env.NEXT_PUBLIC_PLATFORM_CLIENT_ID!,
apiSecret: process.env.PLATFORM_API_SECRET!,
environment: process.env.NODE_ENV === 'production' ? 'live' : 'sandbox',
});
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Vendor {
id String @id @default(cuid())
userId String @unique
platformId String @unique
kycStatus String @default("pending")
products Product[]
createdAt DateTime @default(now())
}
model Product {
id String @id @default(cuid())
vendorId String
platformProductId String @unique
platformCheckoutId String @unique
assetKey String
priceInCents Int
purchases Purchase[]
createdAt DateTime @default(now())
}
model Purchase {
id String @id @default(cuid())
platformTransactionId String @unique
buyerId String
productId String
amount Int
status String @default("completed")
purchasedAt DateTime @default(now())
}
Quick Start Guide
- Initialize a Next.js project with App Router and install dependencies:
npx create-next-app@latest marketplace --typescript --tailwind --eslint - Configure environment variables with platform SDK credentials, database URL, and webhook secrets.
- Run database migrations:
npx prisma generate && npx prisma db push - Deploy to Vercel to establish production callback URLs and Neon environment routing.
- Test the sandbox flow: create a test vendor, complete hosted KYC, publish a product, and trigger a test checkout.
Mid-Year Sale β Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all tutorials.
Sign In / Register β Start Free Trial7-day free trial Β· Cancel anytime Β· 30-day money-back
