Back to Resources
Build & Launch
Advanced
55 min
Chris MaskChris Mask
Oct 6, 2025

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:

Early Traction

Acquiring first users, generating initial revenue, and proving product-market fit.

Best For Role:

Developers

Technical implementation guides and code examples for developers.

Expected Impact:

Strategic

Medium-term initiatives that build competitive advantages.

Platform: Platform Agnostic
Reading Level: Advanced

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)

  1. Payment security (PCI compliance)
  2. Authentication (secure login)
  3. Authorization (who can do what)
  4. SQL injection prevention
  5. XSS protection

Tier 2: Important (Implement Before Launch)

  1. HTTPS everywhere
  2. CSRF protection
  3. Rate limiting
  4. Input validation
  5. Secure session management

Tier 3: Required for Scale (Implement Post-Launch)

  1. Two-factor authentication
  2. Advanced fraud detection
  3. Database encryption at rest
  4. GDPR/CCPA compliance
  5. Security monitoring and alerts

Tier 4: Enterprise (Implement for B2B/Enterprise)

  1. SOC 2 compliance
  2. Penetration testing
  3. Bug bounty program
  4. Advanced DDoS protection
  5. 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 Calculator
#security
#authentication
#authorization
#encryption
#gdpr
#pci-compliance
#owasp
#vulnerability-prevention
Found this helpful? Share it
Share:

About the Author

Chris Mask

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.