Marketplace Security: Complete Implementation Guide from Authentication to Compliance
Implement comprehensive security for your marketplace. Learn authentication strategies, authorization patterns, payment security, data encryption, GDPR compliance, and how to prevent common vulnerabilities.
Who Is This For?
This guide is specifically designed for:
Startup Stage:
Acquiring first users, generating initial revenue, and proving product-market fit.
Best For Role:
Technical implementation guides and code examples for developers.
Expected Impact:
Medium-term initiatives that build competitive advantages.
What You'll Learn
- Implement secure authentication with JWT and refresh tokens
- Build role-based access control (RBAC) systems
- Prevent SQL injection, XSS, and CSRF attacks
- Configure data encryption at rest and in transit
- Achieve GDPR and PCI DSS compliance
- Implement fraud detection and two-factor authentication
Prerequisites
- •Understanding of web security fundamentals
- •Experience with JWT and session management
- •Familiarity with TypeScript/Node.js
- •Knowledge of SQL injection and XSS concepts
A security breach doesn't just cost money—it destroys trust.
In marketplaces, trust is everything. Buyers trust you to protect their payment information. Sellers trust you to safeguard their earnings. Both trust you to keep their personal data private.
One breach can permanently damage your marketplace. This guide provides production-ready security implementations covering authentication, authorization, payment security, encryption, and compliance.
The Security Hierarchy (Priority-Based Implementation)
Most founders approach security wrong—they try to secure everything equally. That's expensive and ineffective.
Implement security in this priority order:
Tier 1: Critical (Implement Day 1)
- •Payment security (PCI compliance)
- •Authentication (secure login)
- •Authorization (who can do what)
- •SQL injection prevention
- •XSS protection
Tier 2: Important (Implement Before Launch)
- •HTTPS everywhere
- •CSRF protection
- •Rate limiting
- •Input validation
- •Secure session management
Tier 3: Required for Scale (Implement Post-Launch)
- •Two-factor authentication
- •Advanced fraud detection
- •Database encryption at rest
- •GDPR/CCPA compliance
- •Security monitoring and alerts
Tier 4: Enterprise (Implement for B2B/Enterprise)
- •SOC 2 compliance
- •Penetration testing
- •Bug bounty program
- •Advanced DDoS protection
- •Data residency controls
Tier 1: Critical Security (Day 1 Implementation)
1. Payment Security: Never Touch Card Data
Rule #1: Never store credit card numbers. Ever.
Use Stripe (or similar) to handle all payment data:
// ❌ NEVER DO THIS
const cardNumber = request.body.cardNumber;
await db.payment.create({
data: { cardNumber }, // ILLEGAL - PCI violation
});
// ✅ CORRECT: Use Stripe tokens
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { paymentMethodId, amount } = await request.json();
// Stripe handles the card data
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: "usd",
payment_method: paymentMethodId,
confirm: true,
});
// You only store Stripe IDs
await db.transaction.create({
data: {
paymentIntentId: paymentIntent.id, // Safe to store
amountCents: amount,
},
});
}
PCI DSS Compliance Checklist:
- • Never store full card numbers
- • Never store CVV/CVC codes
- • Use Stripe.js to collect card data (keeps it off your servers)
- • Use HTTPS everywhere
- • Keep Stripe API keys secret (never in client code)
- • Monitor for suspicious transactions
Client-Side Implementation:
// client/checkout.tsx
import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'
export function CheckoutForm() {
const stripe = useStripe()
const elements = useElements()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!stripe || !elements) return
// Create payment method (card data never touches your server)
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: elements.getElement(CardElement)!,
})
if (error) {
console.error(error)
return
}
// Send only the payment method ID to your server
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
paymentMethodId: paymentMethod.id,
amount: 10000, // $100
}),
})
// Handle response...
}
return (
<form onSubmit={handleSubmit}>
<CardElement />
<button type="submit">Pay</button>
</form>
)
}
2. Authentication: Secure User Login
Use bcrypt for password hashing (minimum 10 rounds):
// lib/auth/password.ts
import bcrypt from "bcryptjs";
const SALT_ROUNDS = 12; // Higher = more secure but slower
export async function hashPassword(password: string): Promise<string> {
return await bcrypt.hash(password, SALT_ROUNDS);
}
export async function verifyPassword(
password: string,
hash: string,
): Promise<boolean> {
return await bcrypt.compare(password, hash);
}
Password Requirements:
import { z } from "zod";
export const passwordSchema = z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
.regex(/[0-9]/, "Password must contain at least one number")
.regex(
/[^A-Za-z0-9]/,
"Password must contain at least one special character",
);
export const signupSchema = z.object({
email: z.string().email("Invalid email address"),
password: passwordSchema,
firstName: z.string().min(2).max(50),
lastName: z.string().min(2).max(50),
});
JWT Implementation with Refresh Tokens:
// lib/auth/jwt.ts
import { SignJWT, jwtVerify } from "jose";
import { hashPassword, verifyPassword } from "./password";
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
export async function createTokens(userId: string, role: string) {
// Short-lived access token (15 minutes)
const accessToken = await new SignJWT({ userId, role })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("15m")
.sign(secret);
// Long-lived refresh token (7 days)
const refreshToken = await new SignJWT({ userId })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("7d")
.sign(secret);
// Store refresh token hash in database
const refreshTokenHash = await hashPassword(refreshToken);
await db.refreshToken.create({
data: {
userId,
tokenHash: refreshTokenHash,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
return { accessToken, refreshToken };
}
export async function verifyAccessToken(token: string) {
try {
const { payload } = await jwtVerify(token, secret);
return payload as { userId: string; role: string };
} catch {
return null;
}
}
Refresh Token Flow:
// app/api/auth/refresh/route.ts
export async function POST(request: Request) {
const { refreshToken } = await request.json();
if (!refreshToken) {
return Response.json({ error: "Missing refresh token" }, { status: 401 });
}
// Verify refresh token
const payload = await verifyAccessToken(refreshToken);
if (!payload) {
return Response.json({ error: "Invalid refresh token" }, { status: 401 });
}
// Check if refresh token exists in database (not revoked)
const storedToken = await db.refreshToken.findFirst({
where: {
userId: payload.userId,
expiresAt: { gt: new Date() },
},
});
if (!storedToken) {
return Response.json(
{ error: "Token revoked or expired" },
{ status: 401 },
);
}
// Verify hash matches
const isValid = await verifyPassword(refreshToken, storedToken.tokenHash);
if (!isValid) {
return Response.json({ error: "Invalid token" }, { status: 401 });
}
// Generate new access token
const user = await db.user.findUnique({ where: { id: payload.userId } });
const { accessToken } = await createTokens(user!.id, user!.role);
return Response.json({ accessToken });
}
3. Authorization: Role-Based Access Control
Middleware for Role Checks:
// lib/auth/authorize.ts
export async function requireRole(
request: Request,
allowedRoles: string[],
): Promise<{ userId: string; role: string } | Response> {
const token = request.headers.get("authorization")?.split(" ")[1];
if (!token) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const payload = await verifyAccessToken(token);
if (!payload) {
return Response.json({ error: "Invalid token" }, { status: 401 });
}
if (!allowedRoles.includes(payload.role)) {
return Response.json({ error: "Forbidden" }, { status: 403 });
}
return payload;
}
// Usage in API routes
export async function DELETE(request: Request, { params }: any) {
const user = await requireRole(request, ["admin"]);
if (user instanceof Response) return user;
// Admin-only logic
await db.user.delete({ where: { id: params.id } });
return Response.json({ success: true });
}
Resource Ownership Checks:
// app/api/listings/[id]/route.ts
export async function PATCH(request: Request, { params }: any) {
const user = await requireRole(request, ["seller", "admin"]);
if (user instanceof Response) return user;
const listing = await db.listing.findUnique({
where: { id: params.id },
});
if (!listing) {
return Response.json({ error: "Not found" }, { status: 404 });
}
// Check ownership (unless admin)
if (user.role !== "admin" && listing.sellerId !== user.userId) {
return Response.json({ error: "Forbidden" }, { status: 403 });
}
// Update listing
const updated = await db.listing.update({
where: { id: params.id },
data: await request.json(),
});
return Response.json({ success: true, data: updated });
}
4. SQL Injection Prevention
Use Prisma (or similar ORM) with parameterized queries:
// ❌ NEVER DO THIS - Vulnerable to SQL injection
const search = request.url.searchParams.get("q");
const results = await db.$queryRaw`
SELECT * FROM listings WHERE title LIKE '%${search}%'
`;
// ✅ CORRECT: Parameterized query
const search = request.url.searchParams.get("q");
const results = await db.listing.findMany({
where: {
title: {
contains: search || "",
mode: "insensitive",
},
},
});
// ✅ ALSO CORRECT: If you must use raw SQL, use parameters
const results = await db.$queryRaw`
SELECT * FROM listings WHERE title ILIKE ${"%" + search + "%"}
`;
Input Sanitization:
import { z } from "zod";
const searchSchema = z.object({
q: z.string().max(100).trim(),
category: z.string().uuid().optional(),
minPrice: z.number().int().min(0).optional(),
maxPrice: z.number().int().max(1000000).optional(),
});
export async function GET(request: Request) {
const url = new URL(request.url);
const params = {
q: url.searchParams.get("q") || "",
category: url.searchParams.get("category") || undefined,
minPrice: parseInt(url.searchParams.get("minPrice") || "0"),
maxPrice: parseInt(url.searchParams.get("maxPrice") || "1000000"),
};
try {
const validated = searchSchema.parse(params);
// Use validated parameters
const results = await db.listing.findMany({
where: {
title: { contains: validated.q, mode: "insensitive" },
categoryId: validated.category,
priceCents: {
gte: validated.minPrice,
lte: validated.maxPrice,
},
},
});
return Response.json({ results });
} catch (error) {
if (error instanceof z.ZodError) {
return Response.json({ error: "Invalid parameters" }, { status: 422 });
}
throw error;
}
}
5. XSS (Cross-Site Scripting) Prevention
React escapes by default, but be careful with dangerouslySetInnerHTML:
// ❌ DANGEROUS
function ListingDescription({ description }: { description: string }) {
return <div dangerouslySetInnerHTML={{ __html: description }} />
}
// ✅ SAFE: Sanitize first
import DOMPurify from 'isomorphic-dompurify'
function ListingDescription({ description }: { description: string }) {
const clean = DOMPurify.sanitize(description, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
})
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
// ✅ BETTER: Use Markdown instead
import ReactMarkdown from 'react-markdown'
function ListingDescription({ description }: { description: string }) {
return (
<ReactMarkdown
allowedElements={['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a']}
>
{description}
</ReactMarkdown>
)
}
Server-Side Sanitization:
import DOMPurify from "isomorphic-dompurify";
const createListingSchema = z.object({
title: z.string().min(10).max(100).trim(),
description: z.string().min(50).max(5000),
});
export async function POST(request: Request) {
const body = await request.json();
const validated = createListingSchema.parse(body);
// Sanitize HTML content on server
const cleanDescription = DOMPurify.sanitize(validated.description, {
ALLOWED_TAGS: ["p", "br", "strong", "em", "ul", "ol", "li"],
ALLOWED_ATTR: [],
});
const listing = await db.listing.create({
data: {
title: validated.title,
description: cleanDescription,
},
});
return Response.json({ success: true, listing });
}
Tier 2: Important Security (Before Launch)
6. HTTPS Everywhere
// next.config.js
module.exports = {
async headers() {
return [
{
source: "/:path*",
headers: [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
],
},
];
},
};
Nginx Configuration (if self-hosting):
# nginx.conf
server {
listen 80;
server_name yourmarketplace.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name yourmarketplace.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# Strong SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
}
7. CSRF Protection
Use SameSite Cookies:
// lib/auth/session.ts
import { cookies } from "next/headers";
export function setSessionCookie(token: string) {
cookies().set("session", token, {
httpOnly: true, // Not accessible via JavaScript
secure: process.env.NODE_ENV === "production", // HTTPS only in production
sameSite: "lax", // CSRF protection
maxAge: 60 * 60 * 24 * 7, // 7 days
path: "/",
});
}
CSRF Tokens for API Routes:
// lib/csrf.ts
import crypto from "crypto";
export function generateCSRFToken(): string {
return crypto.randomBytes(32).toString("hex");
}
export function verifyCSRFToken(token: string, sessionToken: string): boolean {
return crypto.timingSafeEqual(Buffer.from(token), Buffer.from(sessionToken));
}
// Middleware
export async function POST(request: Request) {
const csrfToken = request.headers.get("x-csrf-token");
const sessionToken = cookies().get("csrf-token")?.value;
if (
!csrfToken ||
!sessionToken ||
!verifyCSRFToken(csrfToken, sessionToken)
) {
return Response.json({ error: "Invalid CSRF token" }, { status: 403 });
}
// Process request...
}
8. Rate Limiting
Prevent Brute Force Attacks:
// lib/rate-limit.ts
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
interface RateLimitConfig {
identifier: string; // Email, IP, user ID, etc.
limit: number; // Max requests
window: number; // Time window in seconds
}
export async function rateLimit({
identifier,
limit,
window,
}: RateLimitConfig): Promise<{
success: boolean;
remaining: number;
reset: number;
}> {
const key = `rate_limit:${identifier}`;
const now = Math.floor(Date.now() / 1000);
// Get current count
const count = (await redis.get<number>(key)) || 0;
if (count >= limit) {
const ttl = await redis.ttl(key);
return {
success: false,
remaining: 0,
reset: now + (ttl > 0 ? ttl : window),
};
}
// Increment count
await redis.incr(key);
await redis.expire(key, window);
return {
success: true,
remaining: limit - count - 1,
reset: now + window,
};
}
// Usage: Prevent brute force attacks on login
export async function POST(request: Request) {
const { email, password } = await request.json();
// Rate limit by email (5 attempts per 15 minutes)
const result = await rateLimit({
identifier: `login:${email}`,
limit: 5,
window: 900, // 15 minutes
});
if (!result.success) {
return Response.json(
{
error: "Too many login attempts. Try again in 15 minutes.",
retryAfter: result.reset,
},
{
status: 429,
headers: {
"Retry-After": String(result.reset - Math.floor(Date.now() / 1000)),
},
},
);
}
// Process login...
}
9. Input Validation
Validate Everything with Zod:
import { z } from "zod";
const createListingSchema = z.object({
title: z.string().min(10).max(100).trim(),
description: z.string().min(50).max(5000).trim(),
priceCents: z.number().int().positive().max(1000000000), // Max $10M
categoryId: z.string().uuid(),
images: z
.array(z.string().url())
.min(1, "At least one image required")
.max(10, "Maximum 10 images"),
// Sanitize attributes (prevent XSS in JSONB)
attributes: z
.record(z.union([z.string(), z.number(), z.boolean()]))
.optional(),
});
export async function POST(request: Request) {
const body = await request.json();
try {
const validated = createListingSchema.parse(body);
// Use validated data
const listing = await db.listing.create({
data: {
...validated,
sellerId: request.user.id,
status: "pending",
},
});
return Response.json({ success: true, listing });
} catch (error) {
if (error instanceof z.ZodError) {
return Response.json(
{ error: "Validation failed", details: error.errors },
{ status: 422 },
);
}
throw error;
}
}
10. Secure Session Management
Session Fixation Prevention:
// Regenerate session ID after login
export async function POST(request: Request) {
const { email, password } = await request.json();
const user = await db.user.findUnique({ where: { email } });
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return Response.json({ error: "Invalid credentials" }, { status: 401 });
}
// Delete old sessions (prevent session fixation)
await db.session.deleteMany({ where: { userId: user.id } });
// Create new session
const { accessToken, refreshToken } = await createTokens(user.id, user.role);
setSessionCookie(accessToken);
return Response.json({ success: true, user: sanitizeUser(user) });
}
function sanitizeUser(user: any) {
const { passwordHash, ...safe } = user;
return safe;
}
Tier 3: Required for Scale
11. Two-Factor Authentication
// lib/auth/totp.ts
import { authenticator } from "otplib";
export function generateTOTPSecret(email: string) {
const secret = authenticator.generateSecret();
const otpauth = authenticator.keyuri(email, "YourMarketplace", secret);
return { secret, otpauth };
}
export function verifyTOTP(token: string, secret: string): boolean {
return authenticator.verify({ token, secret });
}
// Enable 2FA flow
export async function POST(request: Request) {
const user = await requireAuth(request);
if (user instanceof Response) return user;
// Generate secret
const { secret, otpauth } = generateTOTPSecret(user.email);
// Store secret (encrypted)
await db.user.update({
where: { id: user.userId },
data: {
totpSecret: await encrypt(secret),
totpEnabled: false, // Not enabled until verified
},
});
// Return QR code URL
return Response.json({
otpauth, // Client generates QR code from this
});
}
// Verify and enable 2FA
export async function POST(request: Request) {
const user = await requireAuth(request);
if (user instanceof Response) return user;
const { token } = await request.json();
const userRecord = await db.user.findUnique({ where: { id: user.userId } });
const secret = await decrypt(userRecord!.totpSecret!);
if (!verifyTOTP(token, secret)) {
return Response.json({ error: "Invalid code" }, { status: 401 });
}
// Enable 2FA
await db.user.update({
where: { id: user.userId },
data: { totpEnabled: true },
});
return Response.json({ success: true });
}
12. Fraud Detection
Basic Fraud Signals:
// lib/fraud/detector.ts
export async function detectFraudulentTransaction(transaction: {
userId: string;
amountCents: number;
ipAddress: string;
}) {
const signals: string[] = [];
let riskScore = 0;
// Check 1: Unusual purchase amount
const userAvg = await db.transaction.aggregate({
where: { buyerId: transaction.userId },
_avg: { amountCents: true },
});
if (transaction.amountCents > (userAvg._avg.amountCents || 0) * 5) {
signals.push("unusual_amount");
riskScore += 25;
}
// Check 2: Multiple transactions in short time
const recentCount = await db.transaction.count({
where: {
buyerId: transaction.userId,
createdAt: { gte: new Date(Date.now() - 60 * 60 * 1000) }, // Last hour
},
});
if (recentCount > 5) {
signals.push("rapid_transactions");
riskScore += 25;
}
// Check 3: IP geolocation check
const geoData = await fetch(
`https://ipapi.co/${transaction.ipAddress}/json/`,
);
const { country_code } = await geoData.json();
const highRiskCountries = ["CN", "RU", "NG"]; // Example
if (highRiskCountries.includes(country_code)) {
signals.push("high_risk_country");
riskScore += 25;
}
// Check 4: New user, high value
const user = await db.user.findUnique({ where: { id: transaction.userId } });
const accountAge = Date.now() - user!.createdAt.getTime();
if (accountAge < 24 * 60 * 60 * 1000 && transaction.amountCents > 50000) {
signals.push("new_user_high_value");
riskScore += 25;
}
return {
riskScore,
signals,
shouldBlock: riskScore >= 75,
shouldReview: riskScore >= 50,
};
}
// Apply fraud detection
export async function POST(request: Request) {
const { amount, paymentMethodId } = await request.json();
const user = await requireAuth(request);
if (user instanceof Response) return user;
const ipAddress = request.headers.get("x-forwarded-for") || "0.0.0.0";
const fraudCheck = await detectFraudulentTransaction({
userId: user.userId,
amountCents: amount,
ipAddress,
});
if (fraudCheck.shouldBlock) {
return Response.json(
{ error: "Transaction blocked for security review" },
{ status: 403 },
);
}
if (fraudCheck.shouldReview) {
// Process payment but flag for review
await db.flaggedTransactions.create({
data: {
userId: user.userId,
amountCents: amount,
riskScore: fraudCheck.riskScore,
signals: fraudCheck.signals,
},
});
}
// Process payment...
}
13. Database Encryption at Rest
PostgreSQL Field-Level Encryption:
-- Enable pgcrypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Encrypt sensitive columns
CREATE TABLE users (
id UUID PRIMARY KEY,
email VARCHAR(255),
phone_number TEXT, -- Store encrypted
ssn TEXT -- Store encrypted (if needed)
);
-- Encrypt on insert
INSERT INTO users (email, phone_number)
VALUES (
'user@example.com',
pgp_sym_encrypt('555-1234', 'encryption-key')
);
-- Decrypt on select
SELECT
email,
pgp_sym_decrypt(phone_number::bytea, 'encryption-key') as phone_number
FROM users;
Application-Level Encryption (TypeScript):
// lib/encryption.ts
import crypto from "crypto";
const algorithm = "aes-256-gcm";
const key = Buffer.from(process.env.ENCRYPTION_KEY!, "hex"); // 32 bytes
export function encrypt(text: string): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag();
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
}
export function decrypt(encrypted: string): string {
const [ivHex, authTagHex, encryptedText] = encrypted.split(":");
const iv = Buffer.from(ivHex, "hex");
const authTag = Buffer.from(authTagHex, "hex");
const decipher = crypto.createDecipheriv(algorithm, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedText, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
// Usage
const user = await db.user.findUnique({ where: { id: userId } });
const phoneNumber = decrypt(user.encryptedPhoneNumber);
14. GDPR Compliance
Data Access Request (Right to Access):
// app/api/gdpr/data-export/route.ts
export async function GET(request: Request) {
const user = await requireAuth(request);
if (user instanceof Response) return user;
// Collect all user data
const userData = await db.user.findUnique({
where: { id: user.userId },
include: {
listings: true,
transactions: true,
messages: true,
reviews: true,
},
});
// Return as JSON
return Response.json({
personalData: {
email: userData?.email,
name: `${userData?.firstName} ${userData?.lastName}`,
phoneNumber: userData?.phoneNumber,
createdAt: userData?.createdAt,
},
listings: userData?.listings,
transactions: userData?.transactions.map((t) => ({
id: t.id,
amount: t.totalCents,
createdAt: t.createdAt,
})),
messages: userData?.messages,
reviews: userData?.reviews,
});
}
Data Deletion (Right to be Forgotten):
// app/api/gdpr/delete-account/route.ts
export async function DELETE(request: Request) {
const user = await requireAuth(request);
if (user instanceof Response) return user;
// Can't delete if active transactions
const activeTransactions = await db.transaction.count({
where: {
OR: [{ buyerId: user.userId }, { sellerId: user.userId }],
status: { in: ["pending", "processing"] },
},
});
if (activeTransactions > 0) {
return Response.json(
{ error: "Cannot delete account with active transactions" },
{ status: 400 },
);
}
// Soft delete (anonymize)
await db.user.update({
where: { id: user.userId },
data: {
email: `deleted-${user.userId}@example.com`,
firstName: "Deleted",
lastName: "User",
phoneNumber: null,
avatarUrl: null,
bio: null,
accountStatus: "deleted",
deletedAt: new Date(),
},
});
return Response.json({ success: true });
}
Security Implementation Checklist
Week 1 (MVP)
- • Use Stripe for payments (never touch card data)
- • Hash passwords with bcrypt (12 rounds minimum)
- • Implement JWT authentication with refresh tokens
- • Use parameterized SQL queries (Prisma ORM)
- • Enable HTTPS everywhere
- • Set security headers (HSTS, X-Frame-Options, etc.)
Week 4 (Before Launch)
- • Implement CSRF protection
- • Add rate limiting on auth endpoints
- • Validate all inputs with Zod
- • Implement secure session management
- • Add XSS prevention (DOMPurify)
- • Configure CORS properly
- • Set up error monitoring (Sentry)
Month 3 (Scaling)
- • Implement two-factor authentication
- • Add basic fraud detection
- • Encrypt sensitive database fields
- • Implement GDPR compliance (if EU users)
- • Set up security monitoring and alerts
- • Add IP geolocation and blocking
Month 6+ (Enterprise)
- • Achieve SOC 2 compliance
- • Conduct penetration testing
- • Launch bug bounty program
- • Implement advanced DDoS protection
- • Add data residency controls
- • Regular security audits
Common Security Issues and Solutions
Issue: Token theft via XSS
Solution: Store tokens in httpOnly cookies, not localStorage:
// ❌ Vulnerable
localStorage.setItem("token", accessToken);
// ✅ Secure
cookies().set("session", accessToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
});
Issue: Brute force password attacks
Solution: Implement progressive delays and account lockout:
export async function checkLoginAttempts(email: string): Promise<boolean> {
const attempts = await db.loginAttempts.count({
where: {
email,
createdAt: { gte: new Date(Date.now() - 30 * 60 * 1000) },
},
});
if (attempts >= 5) {
// Lock account for 30 minutes
await db.user.update({
where: { email },
data: { lockedUntil: new Date(Date.now() + 30 * 60 * 1000) },
});
return false;
}
return true;
}
Issue: Session fixation attacks
Solution: Regenerate session ID on login:
export async function login(email: string, password: string) {
// Verify credentials
const user = await verifyCredentials(email, password);
// Delete all old sessions
await db.session.deleteMany({ where: { userId: user.id } });
// Create new session with new ID
const { accessToken, refreshToken } = await createTokens(user.id, user.role);
return { accessToken, refreshToken };
}
Conclusion
Security is not optional, but it's also not all-or-nothing. Implement security in tiers based on your stage:
MVP (Week 1): Focus on Tier 1 critical security Launch (Week 4): Complete Tier 2 important security Scale (Month 3): Add Tier 3 advanced security Enterprise (Month 6+): Implement Tier 4 compliance
The key is to build security in from day one, then progressively enhance it as you grow.
Additional Resources
How much should your build actually cost?
Get a personalized investment estimate based on your platform type, scope, and timeline.
Open the Investment CalculatorAbout the Author

Chris Mask
Founder & CEO
Serial entrepreneur, marketplace architect, and AI-assisted development pioneer with 7+ years building two-sided platforms. Founded Directorism after launching and exiting two successful marketplace businesses. Has personally architected and consulted on 200+ marketplace and directory projects. Recognized authority on cold-start problems, platform economics, marketplace SEO, and leveraging AI tools for rapid development. Early adopter of AI-powered coding workflows, integrating Claude, Cursor, and agentic development patterns into production systems.
Related Resources
Implementing Marketplace Payment Processing: Complete Technical Guide
Implement split payments, escrow, and compliance in your marketplace. Learn Stripe Connect setup, webhook handling, dispute resolution, and security best practices with production-ready code examples.
Real-Time Messaging System Architecture for Marketplaces
Learn how to architect pre-booking and post-booking messaging systems with automated sequences and engagement optimization.
Search Technology Implementation Guide for Marketplaces
Learn how to select and implement the right search technology with code examples for PostgreSQL, Typesense, Elasticsearch, and Algolia.