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
- Run
curl -sI https://yourapp.com | grep -i content-security. No output means no policy. - 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.
- Look in
index.htmlfor 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:
- connect-src: supabase-js calls
https://<ref>.supabase.cofor the database, auth and storage, and Realtime connects towss://<ref>.supabase.co/realtime/v1/websocket. - Stripe: Stripe's own CSP list for Stripe.js is
js.stripe.comand*.js.stripe.comin script-src and frame-src,api.stripe.comin connect-src, andhooks.stripe.comin frame-src for 3D Secure and other redirect flows. If you only redirect to hosted Checkout, you don't need Stripe.js on your page at all. - data: in img-src and font-src: Vite inlines small assets as data URIs by default, and its docs say to allow
data:for those directives or setbuild.assetsInlineLimit: 0. Never adddata:to script-src. - style-src 'self': Vite builds CSS into files, so this often works. Some UI libraries inject inline styles; if the report shows violations from them, add
'unsafe-inline'to style-src only. - frame-ancestors 'none': stops other sites from framing your app (clickjacking). Use
'self'if you frame your own pages.
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
- Report-only first. Send the policy as
Content-Security-Policy-Report-Only. The browser logs violations but blocks nothing. - 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.
- Remove inline scripts. Move any inline script in
index.htmlinto a file undersrc/. Vite'shtml.cspNonceoption 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.
- Enforce. Rename the header to
Content-Security-Policy, deploy, and run the console check again.
Where the header goes
- Netlify: a
_headersfile in the publish directory. In a Vite project, put it inpublic/so the build copies it intodist/. Netlify doesn't apply these headers to responses from functions or edge functions. - Cloudflare Pages: the same
_headersformat inpublic/. Each line is limited to 2,000 characters, which a long CSP can hit; tighten wildcards before you split anything. - Vercel: a
headersentry invercel.jsonwith"source": "/(.*)".
# 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
- Keep the policy in the repo so every deploy carries it.
- When the AI adds a new SDK, rerun the console check before shipping. New violations are your signal to update the list.
- A CSP doesn't hide secrets. Anything in a
VITE_variable is still public; see VITE_ variables are public and vibe coding security.
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.