Next Starter Logo
Security

Security Headers & CSP

How Next Starter configures HTTP security headers and a Content Security Policy in next.config.ts, and how to add your own external domains.

HTTP security headers are small instructions your server sends with every response. They tell the browser to enforce extra safety rules: block injected scripts, force HTTPS, stop other sites from framing your pages. This page explains how the starter sets them and how to add your own trusted domains.

How it works

All headers live in one place: the securityHeaders array in next.config.ts. Next.js applies them to every route through the headers() async function. The same file sets poweredByHeader: false, so responses never advertise that the app runs on Next.js.

headers() returns three rules:

  • /((?!sitemap).*): applies every security header to all routes except the sitemap. The sitemap is excluded so the strict policy below doesn't get in the way of search crawlers reading it.
  • noIndexRoutes: these routes get the security headers plus X-Robots-Tag: noindex, nofollow, which tells search engines not to index them. The list is APP_CONFIG.noIndexRoutes in lib/config.ts: auth, API, dashboard, onboarding, the legal pages (/privacy, /terms), and error pages (/500, /error, /auth/error).
  • /manifest.webmanifest: a Cache-Control rule only, no security headers.

Header changes apply only after a server restart. headers() runs once at build/boot, not on every request.

Headers reference

HeaderValueWhat it does
X-DNS-Prefetch-ControlonLets the browser look up the IP address of external hosts (fonts, CDN) early, so they load faster.
X-Content-Type-OptionsnosniffStops the browser from guessing a file's type. It must trust the declared type, which blocks a class of attacks.
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preloadHSTS: forces HTTPS for 2 years (in seconds), including all subdomains. preload marks it eligible for browsers' built-in HTTPS list.
Referrer-Policystrict-origin-when-cross-originControls how much of the current URL is sent when a user follows a link out. Full URL within your site, only the domain to other sites, nothing on an HTTPS-to-HTTP downgrade.
X-Frame-OptionsSAMEORIGINStops other sites from loading your pages inside an iframe (clickjacking). Older mechanism; the CSP frame-ancestors below does the same job for modern browsers.
Content-Security-Policysee belowThe main defense. Lists which sources scripts, styles, images, and so on may load from. See the table below.
Cross-Origin-Opener-Policysame-originCuts the link between your tab and any window that opened it from another site, so they can't poke at each other.
Cross-Origin-Resource-Policycross-originLets other sites embed your assets (such as R2/CDN avatars). Set to cross-origin because the CDN serves images to your pages on a different host.
Permissions-Policycamera=(), microphone=(), …Turns off browser features the app doesn't use, so a stray script can't reach them (see below).

preload in HSTS signals intent to join the hstspreload.org list. Don't submit your domain there unless you're sure. Removal is slow. Dropping preload from the value is harmless if you're not submitting.

Content Security Policy

A Content Security Policy (CSP) tells the browser exactly which domains are allowed to supply each kind of content: scripts, styles, images, fonts, and so on. Anything not on the list is blocked. This is the strongest protection against cross-site scripting (XSS), where an attacker tries to run their own JavaScript on your page.

In code the CSP is an array of directives (one rule per resource type) joined with "; ". The defaults already allow the services the starter ships with: Cloudflare Turnstile (the bot check; see Turnstile), Cloudflare R2 storage (file uploads), Google profile images, and Google Fonts.

DirectiveValueWhy
default-src'self'The fallback rule. Any resource type not listed below may load only from your own origin.
object-src'none'Blocks old plugin content like Flash and Java applets.
base-uri'self'Stops an injected <base> tag from rewriting where relative links and scripts resolve to.
form-action'self'Forms may only submit back to your own origin.
img-src'self' data: blob: https://*.googleusercontent.com https://<cdn>Images from your origin, inline data:/blob: images, Google avatars, and your CDN host.
script-src'self' 'unsafe-inline' https://challenges.cloudflare.comScripts from your origin, inline scripts, and Turnstile. In development only, 'unsafe-eval' is also added.
style-src'self' 'unsafe-inline' https://fonts.googleapis.comStyles from your origin, inline styles, and Google Fonts stylesheets.
font-src'self' https://fonts.gstatic.comFont files from your origin and Google Fonts.
connect-src'self' https://*.r2.cloudflarestorage.com https://challenges.cloudflare.comWhere fetch()/XHR calls may go: your origin, R2 direct uploads, and Turnstile verification.
frame-ancestors'self'Which sites may frame your pages, only your own. This is the modern version of X-Frame-Options.
frame-src'self' https://challenges.cloudflare.comWhich sites you may embed in an iframe. Allows the Turnstile widget.
upgrade-insecure-requests(no value)Rewrites any http:// sub-resource to https:// before fetching it.

Two values are filled in at startup:

  • <cdn> is your CDN host, taken from the NEXT_PUBLIC_STORAGE_S3_CDN_URL env var (see Environment Variables). If that var is unset, the placeholder is empty.
  • 'unsafe-eval' is added to script-src only in development (the isDev flag), because hot reload needs it. Production builds drop it automatically.

'unsafe-inline' permits inline <script>/<style> and inline event handlers. It loosens the policy, but Next.js and the UI rely on inline styles and scripts, so the starter keeps it.

What you can change

Everything is in next.config.ts.

Add an external domain to a directive. Find the directive in the CSP array and append the origin. For example, to load an analytics script and call its API:

next.config.ts
`script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ""} https://challenges.cloudflare.com https://cdn.example.com`,
"connect-src 'self' https://*.r2.cloudflarestorage.com https://challenges.cloudflare.com https://api.example.com",

Match the directive to the resource type: a <script> goes in script-src, a fetch() or XHR call in connect-src, an <iframe> in frame-src, images in img-src, and web fonts in font-src and style-src.

Adjust Permissions-Policy. The full value is camera=(), microphone=(), geolocation=(), browsing-topics=(), accelerometer=(), gyroscope=(), magnetometer=(), payment=(), usb=(). Each feature=() turns that browser feature off for everyone. To turn one back on for your own site, replace the empty list with (self). For example, camera=(self) allows the camera. Leave the rest empty so unused features stay off.

Change no-index routes. Edit APP_CONFIG.noIndexRoutes in lib/config.ts. Those paths get noindex, nofollow.

Verify

curl -I https://yourdomain.com

Check the response for Content-Security-Policy, Strict-Transport-Security, and the rest. securityheaders.com gives a graded report, and the Network tab in DevTools shows the headers on any document request. After editing next.config.ts, restart the dev server or rebuild before testing.

On this page