Security

Production hardening with security headers, rate limiting, structured logging, and error boundaries.


Key files

  • next.config.ts - Security headers configuration
  • lib/rate-limit.ts - Rate limiting helpers
  • lib/logger.ts - Structured JSON logger
  • app/error.tsx - Error boundary
  • app/global-error.tsx - Global error boundary

Security headers

Headers are configured in next.config.ts and applied to all routes:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy: camera=(), microphone=(), geolocation=()
  • Strict-Transport-Security: max-age=31536000; includeSubDomains
  • Content-Security-Policy: ...

Verify in production:

curl -I https://yourdomain.com

Rate limiting

All public POST endpoints that can be abused should use the rate limiter:

import { rateLimit, hashForRateLimit } from "@/lib/rate-limit";

const result = await rateLimit({
  key: `feature:${hashForRateLimit(email)}`,
  limit: 10,
  windowSeconds: 3600,
});

if (!result.allowed) {
  return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}

Default limits:

  • Magic links: 5/hour per email, 5/hour per IP
  • Checkout sessions: 10/hour per user

Structured logging

Use the logger instead of console.*:

import { logger } from "@/lib/logger";

logger.info("Operation completed", { userId, action: "create" });
logger.error("Operation failed", { errorMessage: error.message });

The logger:

  • Outputs JSON for log aggregation
  • Automatically redacts sensitive fields (tokens, secrets, signatures)
  • Includes timestamps and log levels

Error handling

Catch errors in API routes, log them, and return generic messages:

try {
  await riskyOperation();
} catch (error) {
  logger.error("Operation failed", {
    errorMessage: error instanceof Error ? error.message : "Unknown",
  });
  return NextResponse.json({ error: "Something went wrong" }, { status: 500 });
}

Never leak internal error details to clients.


CSP for third-party scripts

To add analytics or payment scripts, update ContentSecurityPolicy in next.config.ts:

const ContentSecurityPolicy = `
  script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com;
  frame-src https://js.stripe.com https://hooks.stripe.com;
  connect-src 'self' https: https://api.stripe.com;
`;

Production checklist

Before deploying:

  • Set NEXT_PUBLIC_SITE_URL to production domain
  • Set AUTH_SECRET (new value for production)
  • Run pnpm db:migrate
  • Configure webhook URLs in Stripe/Polar dashboard
  • Verify sending domain in Resend
  • Test security headers with curl -I