When you ship an app with Cursor, Lovable or v0, the server answers every request with a set of HTTP headers: invisible instructions the browser obeys before rendering anything. A handful of them decide a big part of your site's security, and almost no template ships with them on. This guide explains what each one does, which value to set, the mistakes that break things and how to configure them on Vercel, Next.js, Nuxt, Netlify, Cloudflare or nginx.
What a header is and how to see yours
A header is a line of text that travels with the response, in the form Name: value. The browser reads it and changes its behaviour: load a script or not, force HTTPS, allow your page inside an iframe or not. You can see them in the developer tools (F12, Network tab, click the main document, Response Headers section) or from the terminal:
curl -I https://yourdomain.com
If the ones below are not in that list, they are not configured. There are no defaults that save you.
The seven that matter
Content-Security-Policy (CSP)
This is the queen. It tells the browser which origins may load scripts, styles, images, fonts and connections. If an attacker manages to inject a <script> into your page, a solid CSP stops it from running. An honest starting point for a modern app:
Content-Security-Policy: default-src 'self'; script-src 'self' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
The classic mistake is putting 'unsafe-inline' in script-src because "otherwise it doesn't work": that throws away the main protection. Modern frameworks handle inline scripts with a nonce (a random value per request) or with hashes. And the safe way to start is report mode: the Content-Security-Policy-Report-Only header blocks nothing, it only tells you what it would block. Leave it like that for a few days, review the reports and switch to the real header once it breaks nothing.
Strict-Transport-Security (HSTS)
It forces the browser to use HTTPS for the stated time, even if the user types http:// or clicks an old link. It stops someone on the same network from downgrading the connection. The usual value:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Enable it only when your whole domain and its subdomains run over HTTPS: set it with one subdomain still on HTTP and you lock it out for a year in every browser that saw the header. Start with a short max-age if in doubt.
X-Frame-Options and frame-ancestors
They stop your site from loading inside another page's <iframe>. Without them, someone can embed your app in a fake site and trick users into clicking where they should not: that is clickjacking. X-Frame-Options: DENY or SAMEORIGIN handles it in older browsers; the modern form is the frame-ancestors directive inside CSP. Set both.
X-Content-Type-Options
One value: nosniff. It stops the browser from guessing a file's type and executing as a script something you served as text or an image. It is one line; there is no excuse to skip it.
Referrer-Policy
It controls how much of the origin URL is sent when navigating to another site or loading an external resource. Without control, you can leak private paths or tokens that travel in the URL. strict-origin-when-cross-origin is the sensible balance: your domain yes, the full path no.
Permissions-Policy
It declares which browser capabilities your page and its iframes may use: camera, microphone, geolocation, payments. If you do not use them, turn them off; then an injected script or a third-party iframe cannot use them either. For example: camera=(), microphone=(), geolocation=().
Cross-Origin-Opener-Policy
Less known: it isolates your window from the ones that opened it or that you opened with window.open, which cuts off a family of cross-tab attacks. same-origin is the usual value, unless you depend on third-party popups (some OAuth logins), where same-origin-allow-popups is the one to use.
The ones worth removing
X-Powered-By and a Server header with a version announce your exact stack to anyone looking for known flaws. In Next.js it goes away with poweredByHeader: false; in Express, with app.disable('x-powered-by'). And X-XSS-Protection is obsolete: modern browsers ignore it and in old ones it caused bugs of its own. Do not set it, or set it to 0.
How to set them depending on your host
Vercel, for any framework, in vercel.json:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" },
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
]
}
]
}
Next.js: the headers() function in next.config.js, with the same list; the CSP with nonce is generated in the middleware. Nuxt: routeRules in nuxt.config.ts with a headers object, or a server plugin if you need a nonce. Netlify: a _headers file in the published folder:
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Cloudflare: response header transform rules in the dashboard, no code involved. nginx on a VPS:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
I leave CSP out of these blocks on purpose: it is the only one that depends on which scripts and services your app uses, and it should be built in report mode before being pinned down.
Mistakes that break things
- HSTS before everything is on HTTPS. It locks out anyone with a subdomain still on HTTP. Verify first, enable after.
- CSP without a nonce on Next.js or Nuxt. Both inject inline scripts to hydrate the page; without a nonce a strict CSP blocks them and the app renders blank. Use the framework's nonce mechanism.
- Headers only on the HTML. API routes and static files need them too, especially
nosniff. Apply the rule to/(.*). - Duplicate headers. If the CDN and the app both add the same one, the browser may keep the strictest or ignore both. Pick one place.
- Forgetting subdomains (app., api., docs.): each one answers with its own headers.
Check them from the outside
These headers are public information: anyone can read them, including you. The free security headers checker lists the missing and misconfigured ones in seconds. And Scanaris runs the full read-only check, with around 45 other things, and for every missing header gives you a fix prompt ready to paste into your AI with the exact line for your host. It exploits nothing and never touches your server: it reads and tells you what to turn on. For the full context, see the security guide for AI-built apps and the launch checklist.
Frequently asked questions
Do headers slow the site down?
No. They are a few hundred bytes per response and the browser processes them in microseconds. HSTS even speeds things up, because it skips the HTTP-to-HTTPS redirect on later visits.
What if a header breaks something in production?
Remove or relax it and redeploy: the effect is immediate, except for HSTS, which browsers remember for the max-age. That is why CSP is tested in report mode and HSTS is enabled last.
Do I need CSP if my app is small?
If your app has a form or shows user content, yes: it is the defence that turns an XSS into a minor incident. Start with the other headers, which are trivial, and add CSP in report mode when you can spend an afternoon on it.