If you typed something like add security headers to my app into the Lovable prompt and the preview did not visibly change, the cause is structural, not a bug in the prompt. A Lovable app is a static frontend built with Vite, and HTTP response headers are set by whatever serves the dist folder over the wire, not by the React code inside the bundle. Lovable can edit configuration files for you, but it cannot rewrite the response headers of the Lovable Cloud preview URL.
Short answer
A Lovable app ships as a static Vite build, so security headers are set by the host, not by the bundle. The cleanest pattern is a vercel.json or netlify.toml at the repo root that sets Content-Security-Policy, X-Frame-Options, Strict-Transport-Security, X-Content-Type-Options, and Referrer-Policy, plus a meta http-equiv CSP committed in index.html as a backstop. Per the Lovable documentation on external deployment, Lovable apps build with npm run build and output to dist, which is exactly the shape every static host expects.
What you should know
- The Lovable Cloud preview is not where security headers belong. Headers reach real users only on the production host you deploy the build to, not on the Lovable preview origin.
- Vite does not inject HTTP headers. The Vite build produces static HTML, CSS, and JS. Headers come from Vercel, Netlify, Cloudflare Pages, Nginx, or whichever layer fronts those static files.
- CSP frame-ancestors replaces X-Frame-Options in modern browsers. The OWASP Clickjacking Defense Cheat Sheet states X-Frame-Options is obsoleted in favor of frame-ancestors, while recommending both for defense in depth.
- Meta tag CSP cannot set frame-ancestors. Per the MDN reference on Content-Security-Policy, the meta http-equiv form does not support frame-ancestors, report-uri, or report-to, so clickjacking protection has to come from an HTTP header.
- A default Lovable app already talks to Supabase. Any CSP you write has to include the project URL under connect-src, plus Supabase Storage URLs under img-src if the app loads stored assets.
- Strict-Transport-Security is the cheapest header. A single line that prevents protocol downgrade attacks, but it must only be set on HTTPS responses and never on a host you might want to revert to HTTP.
- Headers should be deployed in report-only mode first. Content-Security-Policy-Report-Only lets the browser log violations without breaking the page, which is how you find the long tail of external origins the bundle actually depends on.
Why does a Lovable prompt to add security headers seem to do nothing?
The Lovable AI assistant edits files in the project repository. When you ask it to add security headers, the most useful thing it can produce is a host configuration file (vercel.json, netlify.toml, _headers for Cloudflare Pages, or an Nginx server block in a Dockerfile) or a meta tag inside index.html. Both are committed to your Git repository the same way any other code change is.
The Lovable preview URL, however, is served by Lovable's own edge layer. That layer does not read your vercel.json. The preview is meant to let you click around the app in development, not to act as a hardened production host. So the prompt did the right thing in the codebase. The headers will appear once you deploy that codebase to a host that honors the configuration file.
This is the same reason a tiny meta tag CSP in index.html shows up in the preview, while a vercel.json header block does not. The browser reads the meta tag from the served HTML; the meta tag is part of the bundle. The HTTP header is set by the host.
What is the minimum set of headers worth sending on a Lovable app?
For a typical single-page Lovable app on Vercel, the floor is five headers. Each one closes a well-documented class of bug or attack, and the cost per header is small.
| Header | Value to start with | What it blocks |
|---|---|---|
| Content-Security-Policy | default-src 'self'; connect-src 'self' https://.supabase.co; img-src 'self' data: https://.supabase.co; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none' | XSS, clickjacking, unauthorized data exfiltration |
| X-Frame-Options | DENY | Legacy clickjacking, browsers that ignore CSP frame-ancestors |
| Strict-Transport-Security | max-age=63072000; includeSubDomains; preload | Protocol downgrade and HTTPS strip attacks |
| X-Content-Type-Options | nosniff | MIME confusion attacks on uploaded or user-controlled files |
| Referrer-Policy | strict-origin-when-cross-origin | Leaking the full referrer URL to third-party domains |
The CSP value is the load-bearing one. Read it left to right: default-src 'self' means same-origin only, then each directive layers exceptions on top. The connect-src and img-src lines have to name the Supabase project explicitly because every Lovable app talks to Supabase by default. If the app also uses Stripe, add https://*.stripe.com and https://js.stripe.com to script-src and connect-src.
How do I configure vercel.json with the right headers for a Lovable app?
The vercel.json file lives at the repository root, next to package.json. Vercel reads it during the build and applies the headers on every response. The relevant block looks like this:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Content-Security-Policy", "value": "default-src 'self'; connect-src 'self' https://YOUR-PROJECT.supabase.co; img-src 'self' data: https://YOUR-PROJECT.supabase.co; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'" },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
]
}
],
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
A few things to notice. The source: /(.*) catches every path, which is what you want for an SPA. The rewrites block at the bottom is the standard fix for React Router 404s on hard refresh, unrelated to security but commonly forgotten in the same vercel.json. The YOUR-PROJECT placeholder must be replaced with the real Supabase project ID. Per the Vercel documentation on system headers, custom headers configured in vercel.json apply to all responses for the matched source pattern.
The Lovable AI assistant can write this file for you. The prompt that works in practice is something like: write a vercel.json at the repo root that sets Content-Security-Policy, X-Frame-Options, HSTS, X-Content-Type-Options, and Referrer-Policy for an SPA build, and keep the Supabase project URL in connect-src and img-src.
How do I add the same headers on Netlify or Cloudflare Pages?
Netlify uses a different file shape but the values are the same. Create a netlify.toml at the repo root:
[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "default-src 'self'; connect-src 'self' https://YOUR-PROJECT.supabase.co; img-src 'self' data: https://YOUR-PROJECT.supabase.co; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'"
X-Frame-Options = "DENY"
Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Cloudflare Pages reads a plain text file named _headers at the build root (so place it in public/_headers so Vite copies it into dist):
/*
Content-Security-Policy: default-src 'self'; connect-src 'self' https://YOUR-PROJECT.supabase.co; img-src 'self' data: https://YOUR-PROJECT.supabase.co; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'
X-Frame-Options: DENY
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
The three files do the same thing, written in three different shapes. Pick the one that matches your host and commit it. The Lovable assistant can produce any of them from a prompt, as long as you name the file and the host.
How do I keep CSP from breaking Supabase, Stripe, or Google Fonts?
The first deploy with a real CSP almost always breaks something visible. A logo fails to load, a Stripe checkout iframe stays blank, a Supabase realtime websocket disconnects. The reason is almost always a missing origin in the right directive. The repair pattern is the same every time.
Deploy with Content-Security-Policy-Report-Only instead of Content-Security-Policy for the first 48 hours. The browser still logs every violation in the console and at the report endpoint (if you set one), but does not block the request. You read the console of the deployed app on a real device, write down every blocked origin, and add it to the matching directive. Example failures and their fixes:
Refused to connect to 'wss://YOUR-PROJECT.supabase.co/...'means the Supabase realtime websocket needswss://YOUR-PROJECT.supabase.coadded to connect-src.Refused to load the script 'https://js.stripe.com/v3/'means Stripe Checkout needshttps://js.stripe.comin script-src andhttps://*.stripe.comin connect-src and frame-src.Refused to load the font 'https://fonts.gstatic.com/...'means Google Fonts needshttps://fonts.gstatic.comin font-src andhttps://fonts.googleapis.comin style-src.Refused to execute inline scriptis the trickiest. Vite sometimes inlines a small bootstrap script in index.html. The fix is either a script hash, a nonce injected at deploy time, or moving the inlined block to a separate file. For most Lovable apps, switching to a hash is the lower-friction path.
Once the console is clean for 48 hours, flip the header name from Content-Security-Policy-Report-Only to Content-Security-Policy and redeploy. This is the rollout pattern recommended by the OWASP CSP cheat sheet and by most browser vendors.
When does a meta tag CSP make sense for a Lovable app?
A meta tag CSP, written as <meta http-equiv="Content-Security-Policy" content="..."> inside index.html, is the only line of defense that survives a host migration. If you move the Lovable app from Vercel to Netlify, or from Netlify to a Cloud CDN, the vercel.json stops being read on day one. The meta tag travels with the bundle.
The limits to remember: per the MDN reference on CSP, the meta form does not honor frame-ancestors, report-uri, or report-to. So clickjacking protection still has to come from an HTTP header. The meta tag is useful as a redundant copy of the same-origin defaults (default-src 'self', script-src, style-src, connect-src), not as the only place those values exist.
For builders who want an external automated read of a compiled or deployed build before submission, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning aligned with the OWASP Mobile Application Security Verification Standard for AI-coded and no-code apps, including the web layer that fronts a mobile webview.
What to watch out for
The two failure modes that swallow the most time on Lovable apps are silent breakage and false security. Silent breakage looks like a working app on the Vercel preview URL and a broken app on the production custom domain, because the apex domain is missing a redirect to the canonical host that the CSP allows. False security looks like a strict CSP in vercel.json that the team never deployed, because the Lovable preview URL still serves no headers and the team only ever tests the preview.
The other recurring trap is 'unsafe-inline' on script-src. It is tempting to add it once to silence the console, but it disables the main protection CSP gives against reflected XSS. If a directive needs inline scripts to load, the right move is a per-build hash or nonce, not the unsafe keyword. The same logic applies to 'unsafe-eval' for any app that uses runtime template evaluation; almost no Lovable-generated frontend genuinely needs it.
Finally, Strict-Transport-Security with the preload flag is sticky. Once a domain is added to the browser HSTS preload list, removing it is slow. Set HSTS without preload on a domain that might switch hosts, and only add preload once the production host is stable for at least three months.
Key takeaways
- The Lovable prompt can write the right files (vercel.json, netlify.toml, _headers, meta tag), but the headers reach users only when the build is deployed to a host that reads those files.
- A workable floor for a Lovable app is five headers: CSP with frame-ancestors 'none', X-Frame-Options DENY, HSTS, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin.
- Roll out CSP with Content-Security-Policy-Report-Only for at least 48 hours, watch the console for every Supabase, Stripe, and font origin you missed, then flip the header name.
- For teams that want an independent automated read of the deployed app and the compiled mobile shell against OWASP MASVS, PTKD.com (https://ptkd.com) is one platform built specifically for that pre-submission step.
- Avoid 'unsafe-inline' on script-src and the preload flag on HSTS until the production host and inline script story are stable; both are easy to add and hard to remove.



