Security
Production hardening with security headers, rate limiting, structured logging, and error boundaries.
Key files
next.config.ts- Security headers configurationlib/rate-limit.ts- Rate limiting helperslib/logger.ts- Structured JSON loggerapp/error.tsx- Error boundaryapp/global-error.tsx- Global error boundary
Security headers
Headers are configured in next.config.ts and applied to all routes:
X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: strict-origin-when-cross-originPermissions-Policy: camera=(), microphone=(), geolocation=()Strict-Transport-Security: max-age=31536000; includeSubDomainsContent-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_URLto 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