DEV Community

Vishal Chauhan
Vishal Chauhan

Posted on

How to Add Security Headers to Your AI-Built App: A 7-Step Beginner Guide to HTTPS and CSP

Your app is live, people are signing up, and then someone runs a free security scanner on it and sends you a screenshot full of red: "Missing Content-Security-Policy. Missing X-Frame-Options. Grade: D."

You are not alone. In October 2026, the team behind the ShipSniffer scanner ran a passive scan on 99 live apps launched on Show HN (mostly built with Lovable, Bolt, Replit and Cursor). By their count, 91% had no Content-Security-Policy and 88% had no protection against being framed by another site. They call their numbers indicative, since automated scans make mistakes, but the pattern is clear: AI builders get you to a live URL and skip this step.

The fix is mostly copy-paste. Security headers are a few lines of config that tell the browser how strict to be with your pages. This guide gives you 7 steps, with configs for Vercel, Next.js and Netlify, a note for Lovable and Bolt, and a prompt for your AI.

Quick glossary

  • Response header: a small line of text your server sends along with each page, like X-Content-Type-Options: nosniff. Visitors never see it, but the browser follows it.
  • HSTS (Strict-Transport-Security): tells the browser "only ever load this site over HTTPS, for the next N seconds."
  • CSP (Content-Security-Policy): a list of where your page is allowed to load scripts, styles, images and connections from. If an attacker manages to inject a script, a good CSP can stop it from running.
  • Report-only mode: a way to test a CSP without blocking anything. The browser just complains in the console.
  • Clickjacking: another site loads your app invisibly in an iframe and tricks users into clicking your buttons. frame-ancestors and X-Frame-Options stop this.

One honest note before we start: headers are a safety net, not a lock. They do not fix a database with no access rules or a secret key in your frontend. If you use Supabase, do the Row Level Security lockdown first.

Step 1: Check what your site sends today

Many hosts already send some headers, so look first. Open a terminal and run this against your live URL (not the editor preview):

curl -sI https://yourapp.com | grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy"
Enter fullscreen mode Exit fullscreen mode

curl -I asks for the headers only. The grep part filters the list down to the six headers this guide covers. No output means none of them are set.

Some real defaults, so you know what to expect:

  • Vercel's docs say custom domains get Strict-Transport-Security: max-age=63072000; automatically, and .vercel.app URLs get a preloaded version.
  • Netlify says it redirects HTTP to HTTPS and sends HSTS on its netlify.app subdomains.
  • When I ran curl on a lovable.app response and a bolt.host app on October 25, 2026, both already sent HSTS, X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin. The Bolt app also sent a minimal CSP. Neither sent a full CSP or frame protection. Platform defaults change, so check your own app.

Write down what you found:

Header check (date: ____ )   URL: ____________________
Strict-Transport-Security:  yes / no   value: ________
Content-Security-Policy:    yes / no / report-only
X-Frame-Options or frame-ancestors: yes / no
X-Content-Type-Options:     yes / no
Referrer-Policy:            yes / no   value: ________
Permissions-Policy:         yes / no
Enter fullscreen mode Exit fullscreen mode

Step 2: Add the four easy headers

These four almost never break anything, so ship them first:

X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Frame-Options: DENY
Enter fullscreen mode Exit fullscreen mode

What each one does, in plain words:

  • nosniff stops the browser from guessing a file's type. MDN notes it makes the browser block a script or stylesheet whose content type doesn't match. It is the only valid value.
  • strict-origin-when-cross-origin sends other sites only your domain (not the full page URL) when someone clicks a link out. It's already the browser default, but setting it stops scanners flagging it.
  • Permissions-Policy with () turns a feature off for your page and every iframe in it. Only list features you don't use. If your app uses the camera, leave camera out.
  • X-Frame-Options: DENY blocks other sites from framing you. Use SAMEORIGIN if your own site needs to iframe its own pages.

Vercel (any framework) - create vercel.json in the project root:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
        { "key": "X-Frame-Options", "value": "DENY" }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Next.js - you can use headers() in next.config.js instead:

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
          { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
          { key: 'X-Frame-Options', value: 'DENY' },
        ],
      },
    ]
  },
}
Enter fullscreen mode Exit fullscreen mode

Netlify - add a plain text file named _headers to your publish folder. For a Vite app, put it in public/ so the build copies it into dist/:

/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  X-Frame-Options: DENY
Enter fullscreen mode Exit fullscreen mode

One Netlify catch from its docs: these custom headers don't apply to responses from functions, edge functions or server-rendered pages. Those need to set headers in code.

Step 3: Get HSTS right (and skip "preload" for now)

HSTS is hard to undo. Once a browser sees max-age=63072000 (two years, in seconds), it refuses plain HTTP for your domain for that long.

Rules I'd follow:

  1. If your host already sends HSTS (Step 1), leave it alone. A Netlify forum thread shows that adding your own can produce two HSTS headers, which can confuse browsers and fails the preload check.
  2. If you must add it, only do so once every page and subdomain works on HTTPS:
Strict-Transport-Security: max-age=63072000; includeSubDomains
Enter fullscreen mode Exit fullscreen mode
  1. Leave out preload unless you've read hstspreload.org. Netlify's docs warn that preloading "is not easily reversible." includeSubDomains also covers things like blog.yourapp.com, so make sure they all have HTTPS too.

Step 4: Start your CSP in report-only mode

A CSP is the most useful header and the easiest to get wrong. A strict policy pasted blindly will block your analytics, your fonts, your Supabase calls and maybe your whole app. So you test it first with Content-Security-Policy-Report-Only, which MDN describes as monitoring violations "without enforcing" them. Vercel's own security headers guide recommends the same start.

A beginner starter policy (one line in the real header):

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://YOUR-PROJECT.supabase.co wss://YOUR-PROJECT.supabase.co; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
Enter fullscreen mode Exit fullscreen mode

Swap YOUR-PROJECT for your Supabase project ID, or delete it if you don't use Supabase. The wss:// part is for realtime connections.

Add it with the same config file as Step 2. Then:

  1. Deploy, open your live app in Chrome, and open DevTools (right-click, Inspect, Console tab).
  2. Click through everything: sign up, log in, pay, upload, every main screen.
  3. Look for messages starting with [Report Only] Refused to.... Each one names a blocked source and which directive blocked it.
  4. Add each legit source to the right directive. For example, if Google Fonts shows up, add https://fonts.googleapis.com to style-src and https://fonts.gstatic.com to font-src.

Hypothetical example: your console says a script from https://plausible.io was refused under script-src. You'd add https://plausible.io to script-src, and probably to connect-src too, since analytics scripts send data back.

Chrome may also warn that a report-only policy has no reporting endpoint. That's fine here; the console is your report. (MDN covers the report-to setup if you want reports emailed or logged later.)

Next.js heads-up: Next.js injects small inline scripts, so script-src 'self' will report a lot. The official Next.js CSP guide offers two paths: 'unsafe-inline' in next.config.js (simpler, weaker), or nonces generated per request in proxy.ts (stronger, but forces dynamic rendering). In local dev, React also needs 'unsafe-eval'. Production does not.

Also note: a CSP in a <meta> tag can't do report-only mode or frame-ancestors, according to MDN. Use real headers.

Step 5: Switch CSP to enforce mode

After a few days with no new console messages during normal use, rename the header from Content-Security-Policy-Report-Only to Content-Security-Policy. Keep the same value.

Then:

  1. Deploy and click through the full flow again, watching for anything that breaks.
  2. If something breaks, add the missing source. Don't "fix" it with * or 'unsafe-eval'; Vercel's guide warns against both.
  3. Keep X-Frame-Options: DENY from Step 2. Modern browsers use frame-ancestors 'none', and the Next.js docs call X-Frame-Options superseded by it, but keeping both is harmless and covers old browsers.

If you need your app embedded somewhere (say, a widget on a customer's site), change frame-ancestors 'none' to frame-ancestors 'self' https://customer-site.com and drop X-Frame-Options: DENY for that path.

Step 6: Lovable, Bolt and other hosted builders

I couldn't find a custom response header setting in Lovable's hosting docs, and third-party reviews of Bolt hosting report the same (checked October 2026). Your options:

  1. Accept the defaults for a prototype. Step 1 shows they already cover HTTPS, HSTS and nosniff.
  2. Deploy the code to Vercel or Netlify. Lovable's docs explain how to deploy its projects to Vercel, Netlify or Cloudflare Pages, and Bolt still offers a Netlify option. Then use the Step 2 configs.
  3. Put Cloudflare in front of your custom domain and add headers with a Response Header Transform Rule.

Here's a prompt for Cursor, Claude Code or any AI coding tool:

Add HTTP security headers to this app. We deploy on [Vercel / Netlify / other].
1. Tell me which file to use (vercel.json, next.config.js, or public/_headers) and why.
2. Add: X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin,
   Permissions-Policy disabling camera, microphone and geolocation (unless we use them -
   search the code first), X-Frame-Options DENY.
3. Do NOT add Strict-Transport-Security if our host already sends it.
4. Add a Content-Security-Policy-Report-Only header (not enforced). List every external
   domain the code loads scripts, styles, fonts, images or API calls from, and include
   them in the right directives. Do not use * or 'unsafe-eval'.
5. Change only the config file. Show me the diff and a curl command to verify.
Enter fullscreen mode Exit fullscreen mode

Step 7: Verify, then re-check after big changes

After each deploy:

  1. Re-run the curl command from Step 1 on your homepage and one logged-in page.
  2. Run a free scanner: securityheaders.com or Mozilla's HTTP Observatory. Both grade your headers and explain what's missing.
  3. Do one last full click-through.

Disclosure: I work on Siteory. Its full audit (you get one free with a verified account at siteory.co) checks these same headers on your HTML page, including weak CSP values like unsafe-inline, and gives you a fix prompt to paste into your AI tool. As its own methodology page says, header work rarely moves rankings, and a scan is not a penetration test.

Re-check whenever you add a new script, analytics tool, payment provider or CDN, since each one can trip a working CSP.

Common mistakes

  • Pasting a strict CSP straight into enforce mode. Blank pages and broken logins. Always start report-only.
  • Testing the editor preview instead of the live URL. Previews often run on a different domain with different headers.
  • Adding HSTS preload on day one. Hard to undo. Skip preload until you're sure.
  • Fixing CSP errors with * or 'unsafe-eval'. That turns the policy back into a sign that says "anything goes."
  • Using a <meta> CSP and thinking you're covered against clickjacking. frame-ancestors only works as a real header.
  • Disabling features you use. camera=() will break your video-call feature. Search your code first.
  • Assuming headers replace real security. They don't fix missing database rules or leaked keys.

Your action summary

  1. Run curl -sI on your live URL and fill in the header check.
  2. Add nosniff, Referrer-Policy, Permissions-Policy and X-Frame-Options with your host's config.
  3. Leave HSTS to your host if it already sends it. No preload yet.
  4. Add a CSP in report-only mode and fix every console warning.
  5. Switch to enforced CSP after a few clean days.
  6. On Lovable or Bolt, keep the defaults or deploy the code where you control headers.
  7. Verify with curl and a free scanner, and re-check after every new integration.

Most of this is one config file and one afternoon. Do the four easy headers today, start the report-only CSP tonight, and that red scanner screenshot turns into something you'd be happy to share.

Top comments (0)