Skip to content
StackSiren

How do I add security headers on Vercel? (vercel.json)

By StackSiren · Published

Add a headers array to vercel.json (any framework) or a headers() function to next.config.js (Next.js), apply it to /(.*), and redeploy. Then confirm with curl -I against your production domain, because a typo or a conflicting rule fails silently.

Vercel doesn't add most security headers for you. The main exception is HSTS: Vercel's encryption docs describe Strict-Transport-Security being sent for *.vercel.app domains. Don't assume the same for your custom domain; check what it actually returns before you rely on it. Everything below is safe to paste in and adjust.

Which headers you need

Header What it does Starter value
Strict-Transport-Security Forces HTTPS on future visits max-age=63072000; includeSubDomains; preload
X-Content-Type-Options Stops MIME-type sniffing nosniff
X-Frame-Options Stops other sites framing yours (clickjacking) DENY
Referrer-Policy Limits URL leakage to other sites strict-origin-when-cross-origin
Permissions-Policy Turns off browser features you don't use camera=(), microphone=(), geolocation=()
Content-Security-Policy Controls where scripts, styles and connections can load from Start in report-only mode (below)

Only add preload to HSTS if you intend to submit your domain to the HSTS preload list and every subdomain serves HTTPS. Removing a domain from that list is slow.

The full explanation of each header is in the security headers fix guide.

Option 1: vercel.json (works for any framework)

Put this in vercel.json at the project root:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
        { "key": "Content-Security-Policy-Report-Only", "value": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://*.supabase.co; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" }
      ]
    }
  ]
}

Notes:

  • source uses Vercel's path-matching syntax. /(.*) matches every path.
  • If vercel.json already exists, merge the headers key in rather than replacing the file. JSON doesn't allow comments or trailing commas, and an invalid file fails the deploy.
  • This works for Vite apps (for example Bolt or Lovable exports deployed to Vercel), static sites and Next.js alike.

Option 2: next.config (Next.js and v0 apps)

// next.config.ts
import type { NextConfig } from "next";

const securityHeaders = [
  { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
  { key: "X-Content-Type-Options", value: "nosniff" },
  { key: "X-Frame-Options", value: "DENY" },
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
  { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
];

const nextConfig: NextConfig = {
  async headers() {
    return [{ source: "/:path*", headers: securityHeaders }];
  },
};

export default nextConfig;

Pick one place for each header. If the same header is set in both vercel.json and next.config, it's hard to know which one wins, so keep them in one file.

Adding a Content Security Policy without breaking your app

CSP is the header most likely to break something, so roll it out in two steps.

Step 1: report-only. Ship Content-Security-Policy-Report-Only (as in the vercel.json above). The browser enforces nothing but logs violations in the DevTools console. Click through your app, including sign-in, payments and any embedded widgets, and add each legitimate source you see to the right directive. Typical additions:

  • Supabase: connect-src https://YOUR-PROJECT.supabase.co wss://YOUR-PROJECT.supabase.co
  • Stripe Checkout or Elements: script-src https://js.stripe.com; frame-src https://js.stripe.com https://checkout.stripe.com
  • Google Fonts: style-src https://fonts.googleapis.com; font-src https://fonts.gstatic.com
  • Analytics: your provider's documented script and connect domains

Step 2: enforce. When the console is quiet, rename the header to Content-Security-Policy.

'unsafe-inline' in script-src weakens CSP a lot. Next.js documents a nonce-based approach using middleware, which lets you drop it. It's more work and makes pages dynamic, so many small apps start with the policy above and tighten later. Either way, frame-ancestors 'none' gives you modern clickjacking protection alongside X-Frame-Options.

Step 3: Deploy and verify with curl

Push the change so Vercel redeploys, then check your production domain (only sites you own):

curl -sI https://yourapp.com/ | grep -iE 'strict-transport|x-content-type|x-frame|referrer-policy|permissions-policy|content-security'

You should see one line per header. Then check a deep link and a static asset, because headers can differ by path:

curl -sI https://yourapp.com/dashboard | grep -i x-frame-options
curl -sI https://yourapp.com/favicon.ico | grep -i x-content-type-options

If nothing shows:

  1. Confirm the deployment you're hitting is the new one (Vercel dashboard → Deployments).
  2. Check vercel.json is at the root of the directory Vercel builds from. In a monorepo, that's the project's Root Directory setting.
  3. Make sure no Middleware or Route Handler overwrites the response headers.

Our security headers checker runs the same checks and explains each result.

Single-page apps: headers and rewrites together

Vite apps from Bolt or Lovable that you deploy to Vercel usually need a rewrite so deep links like /dashboard load index.html. Headers and rewrites live side by side in the same file:

{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
      ]
    }
  ]
}

Vercel serves a static file that exists on disk before applying the rewrite, so /assets/*.js still loads normally. Header rules are matched against the requested path, so the same rule covers the rewritten pages and the assets.

If your app is hosted somewhere other than Vercel (for example Netlify, Lovable's own hosting or Replit), the syntax is different. The security headers guide has per-host instructions; for hosts that don't let you set headers, a proxy or CDN in front is the usual workaround.

Common mistakes

  1. Setting headers in a <meta> tag. Only CSP partly works that way, and frame-ancestors is ignored in a meta tag. Send real HTTP headers.
  2. Copying a strict CSP from another site. It will block your own Supabase, Stripe or analytics calls. Build yours from report-only logs.
  3. Testing the preview URL only. Custom domains and vercel.app URLs can behave differently. Test the domain users visit.

CORS is a different header set

Access-Control-Allow-Origin and friends aren't security headers in the same sense. They relax the browser's same-origin rules for your API. Don't add Access-Control-Allow-Origin: * site-wide in vercel.json "to be safe". It's only needed on API routes that other origins must call. See the CORS misconfiguration guide.

Preview deployments

Headers in vercel.json and next.config apply to previews too, which is good: you can test CSP changes on a preview URL before production. Previews have their own URLs, so test the production domain separately after you promote.

FAQ

Does Vercel add security headers automatically? Vercel documents HSTS on its domains. The others (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) are up to you. Check your own domain with curl -I.

Will these headers slow my site down? No meaningful effect. They're a few hundred bytes per response.

My app uses an iframe embed of itself. Is X-Frame-Options: DENY a problem? Use SAMEORIGIN and frame-ancestors 'self' instead, or list the specific origins that may frame you in frame-ancestors.

Is X-XSS-Protection still needed? No. Modern browsers have removed the XSS auditor it controlled, and MDN recommends a CSP instead. Leave it out or set it to 0.

Do headers fix leaked keys or bad RLS? No. Headers protect visitors' browsers. Secrets and database access are separate problems: see the exposed API keys and Supabase RLS guides.

How do I keep them from regressing? Add the curl check to CI, or use scheduled scans. StackSiren's first scan is free, and paid plans start at $29/mo (Solo) with scheduled rescans.

Check your app now

Free passive scan, results in under a minute.

Run a free scan

Fix guides for this topic

Platform checklists: Vercel · v0 · Cursor

Related posts

Sources