Guide

Security headers, in plain words.

Five response headers do most of the work. Here is what each one does, how strict to make it, and where to put it on the platform you deploy to.

A security header is a line your server adds to every response. It tells the browser how to treat your pages: only over HTTPS, never inside someone else's frame, only scripts from places you trust. None of them change how your app works for a normal visitor. They close doors that a browser would otherwise leave open by default.

Most hosting platforms send none of them unless you ask. That is why our free check often lists three or four Fix items on an app that is otherwise well built. The good news: these are usually a ten-minute change in one config file.

The five that matter

1. Strict-Transport-Security (HSTS)

Tells the browser to use HTTPS for your domain for a set time, even if someone types http:// or follows an old link. Without it, the very first request can travel unencrypted on a café network before your redirect kicks in. Use a max-age of at least six months (15552000 seconds). Add includeSubDomains only when every subdomain serves HTTPS.

2. Content-Security-Policy (CSP)

Lists where scripts, styles, images and connections may come from. If an attacker manages to inject a script tag, the browser refuses to run it because its source is not on the list. A CSP is the most useful header and the one most likely to break something, so start in report-only mode, watch the console, then switch it on. Avoid 'unsafe-inline' in script-src: it lets injected inline scripts run and removes most of the benefit.

3. Framing: frame-ancestors or X-Frame-Options

Stops other sites loading your app inside an invisible frame and tricking users into clicking buttons they cannot see. frame-ancestors 'none' inside your CSP is the modern form; X-Frame-Options: DENY covers older browsers. Sending both is fine.

4. X-Content-Type-Options: nosniff

Tells the browser to trust the content type you send rather than guessing. It stops an uploaded file that claims to be an image from being run as a script. There is only one value, and it never breaks anything.

5. Referrer-Policy

Controls how much of the current URL is sent to the next site when a user clicks a link. Password-reset tokens and invoice IDs in URLs have leaked this way. strict-origin-when-cross-origin sends only your domain to other sites and keeps the full path for your own.

A sixth, Permissions-Policy, switches off browser features you do not use, such as the camera or geolocation. It is worth adding, which is why the check shows it as a note rather than a Fix.

A safe starting set

These values suit most small apps. The CSP below allows your own origin, Supabase and one analytics host as an example; adjust the hosts to the services your app really calls, and check the browser console after deploying.

Strict-Transport-Security: max-age=63072000; includeSubDomains
Content-Security-Policy: default-src 'self'; script-src 'self' https://analytics.example.com; connect-src 'self' https://*.supabase.co https://analytics.example.com; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; frame-src 'none'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

'unsafe-inline' in style-src is a common compromise for apps built with CSS-in-JS. It is far less risky than the same value in script-src.

Where to put them

Vercel

Add a headers block to vercel.json at the root of the project. It applies to every route, static or not.

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains" },
        { "key": "Content-Security-Policy", "value": "default-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
      ]
    }
  ]
}

Netlify and Cloudflare Pages

Both read a plain _headers file from the folder you publish (often public/ or dist/). Indent each header under the path.

/*
  Strict-Transport-Security: max-age=63072000; includeSubDomains
  Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin

Next.js (any host)

Return the headers from next.config.js. This works on Vercel and on any Node host running next start.

const securityHeaders = [
  { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
  { key: "Content-Security-Policy", value: "default-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'" },
  { key: "X-Frame-Options", value: "DENY" },
  { key: "X-Content-Type-Options", value: "nosniff" },
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
];

module.exports = {
  async headers() {
    return [{ source: "/(.*)", headers: securityHeaders }];
  },
};

nginx

Add the lines inside the server block. The always flag sends them on error pages too.

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'; base-uri 'none'; object-src 'none'" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Check your work

After deploying, open the browser's developer tools, reload the page and look at the response headers of the first document request. Or ask from a terminal:

curl -sI https://yourapp.com | grep -iE "strict-transport|content-security|x-frame|nosniff|referrer"

Then click through the parts of your app that load third-party content: sign-in, payments, maps, embedded video. If the console shows a CSP violation, add that host to the right directive rather than loosening the whole policy.

Headers are one layer. They make injected code harder to run and pages harder to misuse, but they do nothing for database rules or leaked keys. Those are covered in our guides to exposed env files and Supabase RLS.