ShipWatchFree scan

A working Content-Security-Policy for Vite + Supabase + Stripe apps

Most AI-built React apps ship with no Content-Security-Policy at all, because the first strict policy you try breaks Supabase or Stripe. This page gives you a starter policy that already allows both, a way to test it without breaking production, and the file to put it in on Netlify, Vercel or Cloudflare Pages.

Short answer: Send a Content-Security-Policy response header from your host, not a meta tag. Start from default-src 'self', add *.supabase.co (HTTPS and WSS) to connect-src and Stripe's hosts to script-src and frame-src, ship it as Content-Security-Policy-Report-Only first, fix what the console reports, then enforce.

This page is about the contents of the policy for a typical Lovable, Bolt or hand-rolled Vite + React app. For the other response headers (HSTS, nosniff, framing) and the exact file syntax for each host, see security headers on Netlify and Vercel.

The problem

Without a CSP, any script that gets into your page runs with full access: it can read the Supabase session from local storage and send it anywhere. A CSP is a list of allowed sources that the browser enforces. If a script or request isn't on the list, the browser blocks it and logs a violation.

The reason most apps skip it: a strict policy written from a blog post blocks the Supabase API, Realtime WebSockets and Stripe's iframes, the app breaks, and the header gets deleted. The fix is to start from what your stack actually needs.

Check it yourself

  1. Run curl -sI https://yourapp.com | grep -i content-security. No output means no policy.
  2. If a policy exists, open DevTools, go to Console, reload, and click through login, a data page and checkout. Every blocked resource shows up as a CSP violation naming the directive.
  3. Look in index.html for inline <script> blocks. Those need special handling (below).

A starter policy for Vite + Supabase + Stripe

This is one header value, shown on several lines for reading. Put it on a single line in your config.

default-src 'self';
script-src 'self' https://js.stripe.com https://*.js.stripe.com;
connect-src 'self' https://*.supabase.co wss://*.supabase.co https://api.stripe.com;
frame-src https://js.stripe.com https://*.js.stripe.com https://hooks.stripe.com;
img-src 'self' data: blob: https://*.supabase.co https://*.stripe.com;
font-src 'self' data:;
style-src 'self';
object-src 'none'; base-uri 'self'; form-action 'self';
frame-ancestors 'none'

Why each line is there:

If you load analytics, fonts or a chat widget from another domain, add that exact host to the matching directive. Avoid 'unsafe-eval' and bare https:; they undo most of the protection.

Roll it out without breaking production

  1. Report-only first. Send the policy as Content-Security-Policy-Report-Only. The browser logs violations but blocks nothing.
  2. Click through every flow. Sign up, log in, load data, upload a file, pay with a test card. Note each violation and decide: add the host, or remove the code that needs it.
  3. Remove inline scripts. Move any inline script in index.html into a file under src/. Vite's html.cspNonce option exists, but its docs warn the nonce must be unique per request, which a static host can't provide.
    Move every inline <script> in index.html into a module file under src/ and import it from main.tsx. Do not use eval, new Function, or inline event handler attributes like onclick="...". List any third-party domains the app loads scripts, styles, images or API calls from, so I can add them to my Content-Security-Policy.
  4. Enforce. Rename the header to Content-Security-Policy, deploy, and run the console check again.

Where the header goes

# public/_headers (Netlify or Cloudflare Pages)
/*
  Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://js.stripe.com https://*.js.stripe.com; connect-src 'self' https://*.supabase.co wss://*.supabase.co https://api.stripe.com; frame-src https://js.stripe.com https://*.js.stripe.com https://hooks.stripe.com; img-src 'self' data: blob: https://*.supabase.co https://*.stripe.com; font-src 'self' data:; style-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'

Make it stick

What the free scan checks here (and what it doesn't)

The scan checks that your live URL sends a Content-Security-Policy header, that framing is blocked by X-Frame-Options or frame-ancestors, plus HSTS, nosniff and HTTPS. It also reads your HTML and up to 25 linked scripts for leaked secret keys. It does not evaluate how strict your directives are, and it doesn't read a report-only header as enforcement. Judging the policy is [manual].

FAQ

Does a CSP stop my API keys from leaking?
No. A CSP limits which scripts can run and where the page can send data, which blunts XSS. A secret key in your JavaScript bundle is readable by anyone either way. Move secrets server-side first, then add the CSP.
Can I use Vite's html.cspNonce on Netlify or Vercel static hosting?
Not safely on its own. Vite's docs say the nonce must be unique for every request. A static host serves the same file to everyone, so a fixed nonce gives no protection. Keep inline scripts out of index.html instead.
Does the free scan check whether my CSP is good?
It checks that a Content-Security-Policy header is present and whether it, or X-Frame-Options, blocks framing. It does not grade the directives, so a weak policy still counts as present. Use your browser console to judge the policy itself.

Scan your app free in 30 seconds

Paste your live URL for a plain-English report with fix prompts. If you keep shipping, weekly monitoring re-checks it: Solo $19/mo or Pro $49/mo.

Scan my app free