Security headers on Vercel
Add a "headers" array to vercel.json in your project root with the source "/(.*)" and your security headers, then redeploy. For Next.js apps on Vercel, set them in next.config.ts or proxy.ts instead so the config lives with the framework.
Updated
Check your live site now
Free, no signup, read only. A grade and plain fixes in seconds.
When to use vercel.json for security headers
Vercel lets you set response headers in vercel.json for any framework. Which file you use depends on what you deployed:
- Next.js (including apps generated by v0): use next.config.ts, and proxy.ts for a nonce-based CSP. The Next.js guide covers both.
- Vite, React, Vue, Astro or plain HTML built to static files: use vercel.json. There is no server of your own to set headers from, so the platform config is the right place.
- A mix, such as a static front end with a few serverless API routes: vercel.json covers both, since the header rules apply by path.
Vercel redirects http to https for you and serves every deployment with a certificate. What stays your job is telling browsers how to treat the page once it arrives, which is what these headers do.
The vercel.json security headers block
Put this in vercel.json at the root of the project (in a monorepo, the root directory you set for that project in Vercel). If you already have a vercel.json, add the "headers" key to the existing object. Headers take effect on the next deployment.
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains"
},
{
"key": "Content-Security-Policy",
"value": "default-src 'self' https: data: blob: 'unsafe-inline' 'unsafe-eval'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests"
},
{
"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=()"
}
]
}
]
}The source "/(.*)" is a pattern that matches every path. You can add more entries with narrower sources, for example a longer cache header for "/assets/(.*)", without repeating the security headers there.
includeSubDomains in HSTS applies the https rule to every subdomain of the domain that sends it. That is what you want in almost every case, but check first that nothing on a subdomain (an old blog, a mail admin page) is still http only.
Tightening the CSP on a static Vercel site
The CSP in the block above is a starter policy that allows inline scripts, so it won't break your site and a header checker will still call it weak. A static site can't use per-request nonces without a server, but it often doesn't need them. A production Vite build loads its JavaScript from files under /assets rather than inline script tags, so script-src 'self' is usually enough.
Roll a stricter policy out in report-only mode first. This example is for a Vite app that talks to Supabase; replace YOUR-PROJECT with your project ref, and add any other APIs, fonts or analytics hosts you use:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Content-Security-Policy-Report-Only",
"value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://YOUR-PROJECT.supabase.co wss://YOUR-PROJECT.supabase.co; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'"
}
]
}
]
}- Add the Report-Only header alongside the starter CSP and deploy.
- Open the site with the browser console showing, sign in, and use every main feature. Each blocked resource appears as a CSP report in the console.
- Add the hosts that are legitimately yours to the right directive, redeploy and repeat.
- When the console is clean, replace the starter Content-Security-Policy value with this policy and remove the Report-Only header.
Report-only mode only logs to the browser console unless you add a report-uri or report-to directive pointing at an endpoint that collects reports. For a small app, clicking through with the console open is usually enough.
Vercel security header gotchas
- A vercel.json in the wrong folder is silently ignored. It has to sit in the project's root directory as configured in Vercel's project settings.
- Inline scripts added by an analytics snippet or chat widget in index.html will be blocked by script-src 'self'. Move them into a file, or add their sha256 hash to script-src.
- Preview deployments use the same vercel.json, so they get the headers too. If deployment protection is on for previews, curl will get a login response rather than your page; test the production domain.
- Headers set by your own serverless function code are sent as well. If a function sets the same header, check with curl which value actually arrives.
How to confirm the headers on your Vercel domain
curl -sI https://your-app.vercel.app | grep -i -E 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'Check your custom domain as well as the vercel.app address. Each header from the block should appear once. If one is missing, the usual cause is a vercel.json that wasn't picked up, so check the deployment's source files in the Vercel dashboard.
Questions
Does Vercel set security headers by default?
Vercel handles https and redirects plain http, but the CSP, frame protection, nosniff, Referrer-Policy and Permissions-Policy are left to you. Run curl -sI on your deployment to see what is sent today.
Can I use vercel.json headers with Next.js?
Yes, they are applied, but keeping headers in next.config.ts means they also work in local development and on other hosts. Pick one place so you never have two versions of the same header.
How do I add a nonce-based CSP on Vercel?
You need code that runs per request. On Next.js that is proxy.ts. On a static site, skip nonces and use script-src 'self' plus hashes for any inline scripts you really need.