Security headers are instructions your server sends with every response. They tell the browser to refuse dangerous behaviour: running injected scripts, being framed by another site, downgrading to HTTP, or guessing file types. On Vercel you add them with a few lines in vercel.json, with no code changes and no extra cost.
The copy-paste vercel.json
Put this in the root of your project next to package.json. If you already have a vercel.json, merge the headers array into it.
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" },
{ "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", "value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" }
]
}
]
}
"source": "/(.*)" applies the headers to every path. Commit, push, and Vercel applies them on the next deployment.
What each header does
- Strict-Transport-Security (HSTS): tells browsers to use HTTPS only for this domain for two years. Only add
preloadand submit to hstspreload.org once every subdomain serves HTTPS, because it is hard to undo. - X-Content-Type-Options: nosniff: stops browsers from treating an uploaded text file as JavaScript.
- X-Frame-Options: DENY and CSP
frame-ancestors 'none': stop other sites from loading yours in an invisible iframe to trick clicks (clickjacking). UseSAMEORIGIN/'self'if you frame your own pages. - Referrer-Policy: stops full URLs, which can contain tokens or IDs, from leaking to third-party sites.
- Permissions-Policy: turns off browser features you don't use, so injected code can't request the camera or location.
- Content-Security-Policy (CSP): the most powerful one. It lists where scripts, styles, images and network calls may come from. If an attacker injects a
<script>pointing at their server, the browser refuses to run it.
Adapting the CSP to a real app
The strict policy above suits a site that serves everything from its own domain. Most vibe-coded apps call other services, so you will need to add their origins:
- Supabase:
connect-src 'self' https://YOUR-PROJECT.supabase.co wss://YOUR-PROJECT.supabase.co(thewss:entry is for Realtime) - Stripe.js:
script-src https://js.stripe.com; frame-src https://js.stripe.com https://hooks.stripe.com; connect-src https://api.stripe.com - Google Fonts:
style-src https://fonts.googleapis.com; font-src https://fonts.gstatic.com - Vercel Analytics / Speed Insights: served from your own domain under
/_vercel/, so'self'covers them - Images from a CDN: add the CDN host to
img-src
Frameworks such as Next.js and Vite inject inline scripts for hydration. If those break, the quick fix is 'unsafe-inline' in script-src, which weakens protection against XSS. The better fix in Next.js is a nonce-based CSP set in middleware. See the Next.js docs on Content Security Policy.
Roll out CSP safely with Report-Only
To avoid breaking production, first ship the policy under the key Content-Security-Policy-Report-Only. The browser then logs violations in the console without blocking anything. Click through the app (sign-in, checkout, uploads), fix anything it reports, then rename the key to Content-Security-Policy.
Headers for specific paths
Rules are evaluated in order, and a later matching rule can override a header. For example, to allow one embeddable widget page to be framed:
{ "source": "/embed/(.*)", "headers": [
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
{ "key": "Content-Security-Policy", "value": "frame-ancestors https://partner.com" }
]}
Next.js alternative
In Next.js you can set the same headers in next.config.js with async headers() { return [{ source: '/(.*)', headers: [...] }] }. Pick one place, either that or vercel.json, not both.
Test that they are live
curl -sI https://yourapp.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy"
Or run a free ShipCheck scan. It checks HTTPS and each of these headers, flags weak CSPs, and gives you a fix for anything missing. Using Netlify instead? See security headers on Netlify.