The boilerplate implements comprehensive security measures following OWASP guidelines, including rate limiting, input sanitization, CSRF protection, and security headers.

CSRF Protection

Double Submit Cookie pattern with Origin/Referer validation.

Rate Limiting

Upstash Redis or in-memory with pre-configured limiters.

Input Sanitization

XSS prevention with DOMPurify, SQL injection protection, path traversal blocking.

Security Headers

CSP, HSTS, X-Frame-Options, and more via next.config.ts.

Env Validation

Zod-based environment variable validation with type safety.

AI Credit Accounting

1 credit = 1 LLM token. Pre-flight check, post-stream decrement of actual tokens.

File Structure

lib/security/
├── api-security.ts    # API route security wrapper
├── rate-limiter.ts    # Rate limiting (Upstash Redis / in-memory)
├── sanitize.ts        # Input sanitization utilities
├── csrf.ts            # CSRF protection
├── verify-admin.ts    # Shared admin verification utility
└── index.ts           # Re-exports

lib/env.ts             # Zod-based environment validation
next.config.ts         # Security headers configuration

API Security Wrapper

The withSecurity wrapper provides comprehensive protection for API routes with a single function call.

Rate limiting is preconfigured with different tiers: strict (5 requests per minute for sensitive operations), standard (30 per minute for general API calls), relaxed (100 per minute for public endpoints), and specialized limiters for AI and contact form endpoints. The system uses Upstash Redis in production and falls back to in-memory limiting in development.

Shorthand Helpers

Wrap your API route handlers with the appropriate security level from apiSecurity: use public() for unauthenticated endpoints with standard rate limiting, authenticated() for endpoints requiring a logged-in user, admin() for platform admin endpoints, ai() for AI endpoints with stricter limits, or webhook() for external webhook receivers.

Rate Limiting

Rate limiting uses Upstash Redis when configured, with automatic fallback to in-memory limiting for development or single-instance deployments.

Limiter Limit Window Use Case
strict 5 requests 1 minute Auth, password reset, sensitive ops
standard 30 requests 1 minute General API endpoints
relaxed 100 requests 1 minute Read-heavy endpoints, public data
ai 10 requests 1 minute AI streaming, expensive operations
contact 3 requests 1 minute Contact forms, newsletter signup
webhook 100 requests 1 minute Stripe webhooks, external callbacks

Client IP identity is deliberately conservative. The rate limiter trusts Cloudflare's CF-Connecting-IP header only when TRUST_CLOUDFLARE_IP=true; enable that only when direct origin access is blocked or your proxy strips spoofed client headers. Otherwise the app ignores spoofable Cloudflare-looking headers and falls back to the last X-Forwarded-For entry, then X-Real-IP.

Input Sanitization

Always sanitize user input using the built-in sanitization utilities. The library provides methods for HTML content (strips tags and scripts), email validation, URL validation, slug generation (alphanumeric and hyphens only), and filename sanitization. These are located in lib/security/sanitize.ts.

DOMPurify HTML Sanitization

For rendering CMS content or user-generated HTML, the boilerplate uses isomorphic-dompurify with a strict allowlist of safe tags and attributes.

Why DOMPurify?

DOMPurify removes all malicious code including <script> tags, event handlers (onclick, onerror), and javascript: URLs. The isomorphic-dompurify package works in both server-side and client-side rendering contexts.

Form Validation

All form inputs are validated on the server side using Zod schemas before processing. Define a schema for each form's expected data, then use safeParse() to validate the request body. Invalid inputs return a 400 response with descriptive error messages. Client-side validation is also applied for immediate user feedback, but never relied upon as the sole validation layer.

CSRF Protection

CSRF protection uses the Double Submit Cookie pattern combined with Origin/Referer header validation.

CSRF is enforced automatically inside every apiSecurity.* / withSecurity() wrapper for POST/PUT/PATCH/DELETE — there is no separate wrapper to add. The standalone withCsrfProtection helper was removed (it was unwired). Client code uses csrfFetch() to attach the Double-Submit token header on client-initiated state changes; the server validates the token + Origin/Referer (against appConfig.url, never the request Host) before the route handler runs.

Security Headers

Security headers are configured in next.config.ts and applied to all routes.

Header Value Purpose
X-Frame-Options DENY Prevent clickjacking
X-Content-Type-Options nosniff Prevent MIME sniffing
X-XSS-Protection 1; mode=block Enable XSS filter (legacy)
Referrer-Policy strict-origin-when-cross-origin Control referrer info
Permissions-Policy camera=(), microphone=(), ... Restrict browser features
Strict-Transport-Security max-age=31536000; includeSubDomains; preload Force HTTPS (production only)
Content-Security-Policy (see below) Control resource loading

Content Security Policy

The CSP is built per-request in lib/security/csp.ts using a cryptographically random nonce + 'strict-dynamic' in production. The static next.config.ts headers no longer carry the Content-Security-Policy entry — the middleware (proxy.ts) injects the dynamic policy. The policy allows resources from trusted domains (Supabase, Stripe, Google Analytics, Google Ads, GTM, Meta Pixel, your active email provider, Crisp, Cloudflare Turnstile, Twemoji flag SVGs from cdnjs) while blocking everything else. If you add a new third-party service, update the appropriate allowlist constant in lib/security/csp.ts rather than the legacy next.config.ts.

The CSP is assembled from six allowlist constants in lib/security/csp.ts — one per directive. The table below summarizes each allowlist; treat lib/security/csp.ts as the authoritative source and edit there when adding a new third party.

Allowlist (directive) Origins
SCRIPT_CDN_ALLOWLIST (script-src) Stripe (js.stripe.com), Google (GTM, GA, googleadservices, doubleclick, googlesyndication), Meta/Facebook (connect.facebook.net), X/Twitter (static.ads-twitter.com), Crisp (client.crisp.chat), Cloudflare Turnstile (challenges.cloudflare.com), Twemoji CDN (cdnjs.cloudflare.com)
STYLE_CDN_ALLOWLIST (style-src) Crisp, Google Fonts CSS (fonts.googleapis.com), cdnjs.cloudflare.com. Inline styles also allowed (Next.js RSC / CSS-in-JS compromise)
IMG_CDN_ALLOWLIST (img-src) Supabase Storage (*.supabase.co), Google (GTM, GA, googleadservices, doubleclick, googlesyndication, google.com, *.googleusercontent.com), Meta (facebook.com), X/Twitter (t.co, analytics.twitter.com), Crisp (client/image/storage.crisp.chat), Gravatar (www.gravatar.com), Twemoji flag SVGs (cdnjs.cloudflare.com)
FONT_CDN_ALLOWLIST (font-src) Crisp, Google Fonts files (fonts.gstatic.com), Facebook CDN (static.xx.fbcdn.net)
CONNECT_CDN_ALLOWLIST (connect-src) Supabase REST + Realtime (https://*.supabase.co, wss://*.supabase.co), Stripe API (api.stripe.com), Google (GTM, GA, googleadservices, doubleclick, googlesyndication, analytics.google.com, stats.g.doubleclick.net), Meta (facebook.com, graph.facebook.com), X/Twitter (static.ads-twitter.com, analytics.twitter.com, t.co), Crisp HTTPS + WebSockets (client.crisp.chat, wss://client.relay.crisp.chat, wss://stream.relay.crisp.chat), Upstash Redis (*.upstash.io), Cloudflare Turnstile
FRAME_CDN_ALLOWLIST (frame-src) Stripe (js.stripe.com, hooks.stripe.com), Google Ads conversion frames (td.doubleclick.net), Crisp game frame (game.crisp.chat), Cloudflare Turnstile, YouTube (youtube.com, www.youtube-nocookie.com), Vimeo (player.vimeo.com)

The middleware adds NEXT_PUBLIC_SUPABASE_URL to connect-src at runtime via the extraConnectOrigins argument so per-project URLs don't need to be hardcoded.

Bot Protection (Cloudflare Turnstile)

Cloudflare Turnstile protects forms from automated abuse. Set NEXT_PUBLIC_TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY from your Cloudflare dashboard. The Turnstile component renders the invisible challenge widget on protected forms (contact, newsletter, login). Server-side validation in the API routes verifies the token with Cloudflare before processing the form submission. In development mode, Turnstile validation is bypassed automatically.

Environment Variable Validation

All environment variables are validated at runtime using Zod schemas in lib/env.ts. This ensures type safety and clear error messages when required variables are missing.

Validation Timing

Environment variables are validated when the module is imported. If validation fails, the application will throw an error with a clear message indicating which variables are missing or invalid.

AI Credit Accounting

1 credit = 1 LLM token. The credit balance literally represents the number of LLM tokens the account can consume. Both chat (/api/ai/stream) and RAG (embeddings + similarity search) deduct the exact usage.total_tokens reported by the provider — no abstract per-chunk or per-search flat rates.

Credit operations must always use the atomic SQL RPC functions to prevent race conditions. Use decrement_credits to deduct credits (with a reason for the audit trail) and add_credits to add credits (with a source identifier). Never update the credits balance column directly.

Pre-flight check, no upfront reserve

Before each LLM call the route checks credits_balance >= aiConfig.minCreditsRequired (fast 402 if too low) and only debits after the response, with the exact token count from the provider. The previous reserve-refund pattern was removed in favor of pure 1:1 token accounting.

Concurrent requests are not blocked at the application layer; the database CHECK (credits_balance >= 0) constraint is the safety net of last resort. If a race causes a post-stream decrement to fail, it is logged via logError (category ai, event decrement_after_stream_failed) and surfaced in /admin-dashboard/logs.

Admin Verification Utility

A shared utility for verifying admin access in API routes, with auto-fix logic for the admin email setting.

Additional Security Hardening

The boilerplate includes several layers of defense-in-depth beyond the core security middleware:

Protection Details
Privilege Escalation Prevention The is_admin flag cannot be set client-side. Admin status is determined server-side by matching the user email against app_settings.admin_email.
Open Redirect Protection Auth callbacks and gtagReportConversion validate redirect URLs against the application origin, blocking external redirect attacks.
Locale Validation on Magic Links The magic link flow validates the locale parameter against the configured locale list before constructing callback URLs, preventing injection via crafted locale values.
OTP Verify (no enumeration leak) POST /api/auth/verify-otp Zod-validates the code against a dynamic regex built from appConfig.auth.otpLength (e.g. ^\d{6}$), strict-rate-limits per IP, and returns a single generic "Invalid or expired code" message for every failure (wrong digits, expired token, unknown email) — no enumeration vector. The endpoint also re-applies the prelaunch allowlist after verifyOtp succeeds (belt-and-suspenders, mirrors the magic-link callback path).
XSS Prevention (DOMPurify) All CMS-rendered content is sanitized with isomorphic-dompurify using a strict tag/attribute allowlist. Covers both server-side and client-side rendering.
Timing-Safe Secret Comparison Job authentication uses crypto.timingSafeEqual for secret comparison, preventing timing-based attacks on the JOBS_SECRET_KEY.
SSRF Protection (Webhooks) Webhook handler URLs are validated before fetch: URL scheme (http/https) and string-pattern checks for common private-IP literals are enforced. For new fetch destinations from user-supplied URLs, prefer DNS-resolution + IP pinning (resolve the hostname, reject if it lands in a private range, then pin the connection to the resolved IP) per anti-pattern D1 in .claude/rules/anti-patterns.md — a hostname-only string compare can be bypassed by attacker-controlled DNS that resolves to 127.0.0.1.
IP Spoofing Resistance Rate limiter trusts CF-Connecting-IP only when TRUST_CLOUDFLARE_IP=true. Keep it disabled unless the origin is actually protected by Cloudflare or a trusted proxy strips spoofed headers.
Rate Limiting on Sensitive Routes Organization deletion (/api/org/schedule-deletion) and invitation routes use strict rate limiting (5/min) to prevent abuse.
Debug Endpoint Gating Debug and diagnostic endpoints are gated to NODE_ENV === 'development' only, never exposed in production builds.