Security

    How do I add security headers to a Lovable app?

    A developer reviewing the Network tab of a Lovable app preview while editing a vercel.json file with Content-Security-Policy and X-Frame-Options headers

    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.

    HeaderValue to start withWhat it blocks
    Content-Security-Policydefault-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-OptionsDENYLegacy clickjacking, browsers that ignore CSP frame-ancestors
    Strict-Transport-Securitymax-age=63072000; includeSubDomains; preloadProtocol downgrade and HTTPS strip attacks
    X-Content-Type-OptionsnosniffMIME confusion attacks on uploaded or user-controlled files
    Referrer-Policystrict-origin-when-cross-originLeaking 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 needs wss://YOUR-PROJECT.supabase.co added to connect-src.
    • Refused to load the script 'https://js.stripe.com/v3/' means Stripe Checkout needs https://js.stripe.com in script-src and https://*.stripe.com in connect-src and frame-src.
    • Refused to load the font 'https://fonts.gstatic.com/...' means Google Fonts needs https://fonts.gstatic.com in font-src and https://fonts.googleapis.com in style-src.
    • Refused to execute inline script is 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.
    • #lovable
    • #security-headers
    • #content-security-policy
    • #x-frame-options
    • #vite
    • #vercel
    • #ai-coded-apps

    Frequently asked questions

    Can I add security headers from inside the Lovable prompt?
    Partially. Lovable can edit index.html, vercel.json, netlify.toml, and any source file in the project, so it can add a meta tag CSP or a host config file when you ask it directly. It cannot inject HTTP response headers on the Lovable Cloud preview URL, because those are served by Lovable's own edge layer. The headers reach users only after you deploy to a host you control.
    Does Lovable Cloud already send a Content-Security-Policy header?
    On the live Lovable preview URL, no full CSP is sent by default at the time of writing. The preview is intended for review, not production traffic. Once you publish through Lovable Cloud, the served domain still does not auto-generate a project-specific CSP. The expected pattern is to deploy the same repo to Vercel, Netlify, Cloudflare Pages, or a Cloud CDN and configure headers there.
    Is X-Frame-Options still needed if I already set CSP frame-ancestors?
    Setting both is the safer default. The OWASP Clickjacking Defense Cheat Sheet notes X-Frame-Options is obsoleted by the frame-ancestors directive, but recommends keeping both for defense in depth because some embedded browsers and corporate proxies still rely on the older header. The cost of sending two headers that say the same thing is one extra line per host config.
    Will a strict CSP break my Lovable app if it uses Supabase or Stripe?
    Yes if you do not add those origins to connect-src and script-src. A default Lovable app calls the Supabase project URL, sometimes Stripe, sometimes Supabase Storage for images. Every external origin the bundle talks to has to appear in the right CSP directive. The fastest way to find them is to deploy with Content-Security-Policy-Report-Only first and read the browser console for blocked requests.
    Can I just set the CSP through a meta tag in index.html?
    You can, and it works for most directives, but not all. Per MDN, the meta http-equiv form does not support report-uri, report-to, or frame-ancestors. Clickjacking protection through frame-ancestors must be sent as an HTTP header, which means the host has to be involved. A meta CSP is useful as a backstop, not as the only line of defense.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free