Svelte 5 doesn’t make CSP hard, but it does force you to be honest about how your app renders, hydrates, and injects code. That’s a good thing.

If you’re building with Svelte 5 and runes, the CSP story is mostly about three things:

  1. avoiding inline script and style surprises
  2. handling nonces correctly for SSR
  3. not breaking hydration or third-party tooling

Runes themselves don’t need special CSP directives. $state, $derived, and $effect are compile-time language features. CSP doesn’t care that you used runes. CSP cares whether the generated output includes inline JavaScript, inline styles, eval-like behavior, or external resources from origins you didn’t allow.

This guide is the practical version: what to set, what breaks, and what to paste into your app.

The short answer

For a typical SvelteKit app on Svelte 5, you usually want:

  • object-src 'none'
  • base-uri 'self'
  • frame-ancestors 'none'
  • form-action 'self'
  • default-src 'self'
  • a strict script-src using nonces
  • a controlled style-src
  • explicit connect-src, img-src, font-src, and frame-src as needed

If you can use nonces for scripts, do it. If you can avoid 'unsafe-inline' in styles, do that too. In practice, many apps keep 'unsafe-inline' for styles longer than they should.

A solid baseline CSP for SvelteKit

This is a good starting point for a self-hosted SvelteKit app with SSR:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{NONCE}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none'

A few opinions here:

  • I like default-src 'self' as the base.
  • I use 'strict-dynamic' when I trust my nonce-bearing bootstrap scripts.
  • I keep object-src 'none' always.
  • I set frame-ancestors 'none' unless I explicitly want embedding.

If you need ready-made policy patterns, https://csp-examples.com is useful for comparing shapes of policies.

What Svelte 5 runes change

Almost nothing at the CSP layer.

This rune-heavy component:

<script>
	let count = $state(0);
	let doubled = $derived(count * 2);

	$effect(() => {
		console.log('count changed', count);
	});

	function increment() {
		count += 1;
	}
</script>

<button onclick={increment}>Clicked {count} times</button>
<p>Doubled: {doubled}</p>

does not require special CSP treatment just because it uses runes.

What matters is:

  • the generated client bundle is loaded safely
  • any inline hydration bootstrapping is nonce-protected
  • you don’t add unsafe third-party snippets without accounting for them

The common mistake is blaming runes for a CSP error that was actually caused by analytics, a consent banner, a dev tool, or an injected inline script in your app template.

SvelteKit nonce setup

If you’re serving SSR HTML, nonces are the cleanest route.

Generate a nonce per request, attach it to your CSP header, and make sure SvelteKit uses it for any inline scripts it emits.

A common server hook pattern looks like this:

// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { randomUUID } from 'node:crypto';

export const handle: Handle = async ({ event, resolve }) => {
	const nonce = randomUUID();
	event.locals.nonce = nonce;

	const response = await resolve(event, {
		transformPageChunk: ({ html }) => html.replace('%csp_nonce%', nonce)
	});

	response.headers.set(
		'content-security-policy',
		[
			"default-src 'self'",
			`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
			"style-src 'self'",
			"img-src 'self' data: https:",
			"font-src 'self'",
			"connect-src 'self'",
			"frame-ancestors 'none'",
			"base-uri 'self'",
			"form-action 'self'",
			"object-src 'none'"
		].join('; ')
	);

	return response;
};

Then in your app template:

<!-- src/app.html -->
<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		%sveltekit.head%
	</head>
	<body data-sveltekit-preload-data="hover">
		<div style="display: contents">%sveltekit.body%</div>
	</body>
</html>

Depending on your setup, SvelteKit can handle CSP nonces directly through framework support rather than manual placeholder replacement. Check the official docs for your exact version:

My advice: verify the final rendered HTML in production and confirm every inline script has the same nonce as your header. Don’t assume.

Report-Only first, always

Before enforcing, use Report-Only:

response.headers.set(
	'content-security-policy-report-only',
	[
		"default-src 'self'",
		`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
		"style-src 'self'",
		"img-src 'self' data: https:",
		"font-src 'self'",
		"connect-src 'self'",
		"frame-ancestors 'none'",
		"base-uri 'self'",
		"form-action 'self'",
		"object-src 'none'"
	].join('; ')
);

This catches the weird stuff: browser extensions aside, the real offenders are usually tag managers, A/B testing tools, and “temporary” inline snippets someone pasted six months ago.

Development vs production

Your production CSP should be strict. Your dev CSP can be looser.

Vite and hot module reload often need extra allowances in dev, especially ws: or wss: for HMR and sometimes less strict script handling.

A dev-only policy might look like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline' 'unsafe-eval';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' ws: wss: http: https:;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none'

I would never ship that to production, but I also wouldn’t waste time trying to make local HMR perfectly strict unless there’s a compliance reason.

Third-party services: real-world policy example

Here’s a real policy shape from headertest.com:

content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-YmE3YmM0NWUtNjgxNC00MDY2LTgyZDktMmNkMTc4N2RiYjhh' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'

This is pretty representative of what happens once marketing gets involved:

  • Google Tag Manager needs script and connect allowances
  • Google Analytics expands connect-src
  • Cookiebot adds script, style, frame, and connect sources
  • websocket endpoints show up in connect-src

If your Svelte app uses those same vendors, this is the kind of policy you end up with.

Copy-paste version for SvelteKit:

const csp = [
	"default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com",
	`script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com`,
	"style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com",
	"img-src 'self' data: https:",
	"font-src 'self'",
	"connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com",
	"frame-src 'self' https://consentcdn.cookiebot.com",
	"frame-ancestors 'none'",
	"base-uri 'self'",
	"form-action 'self'",
	"object-src 'none'"
].join('; ');

Would I love to remove 'unsafe-inline' from style-src here? Yes. In real deployments, consent tooling and injected UI often make that annoying fast.

Common Svelte CSP breakages

1. Inline scripts in app.html

If you add something like this:

<script>
	window.appTheme = localStorage.getItem('theme') || 'light';
</script>

your CSP will block it unless you use a nonce or hash.

Safer version with a nonce:

<script nonce="%csp_nonce%">
	window.appTheme = localStorage.getItem('theme') || 'light';
</script>

But honestly, I try to keep custom inline scripts out of app.html unless they’re absolutely necessary.

2. Third-party snippets pasted into components

This is a classic footgun:

<svelte:head>
	<script>
		(function () {
			console.log('tracking init');
		})();
	</script>
</svelte:head>

That inline script needs a nonce too, and depending on how it’s rendered, you may not get one automatically. Better option: move the code into a module you control and load it as part of your app bundle.

3. WebSocket failures in dev or realtime apps

If your app uses a live backend or HMR, you need ws: or explicit websocket origins in connect-src.

Example:

connect-src 'self' https://api.example.com wss://realtime.example.com

4. Style attribute and injected CSS issues

Some UI libraries or consent managers inject styles dynamically. That often pushes teams toward:

style-src 'self' 'unsafe-inline'

Not ideal, but common. If you can move styles into static CSS files or use nonce-based style tags, do it.

A stricter production template

If your app is mostly first-party code and you don’t have messy third-party embeds, I’d start here:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{NONCE}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  frame-src 'none';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';
  upgrade-insecure-requests

That’s a decent “grown-up app” policy.

Final sanity checklist

Before you call your Svelte 5 CSP done, check these:

  • every SSR response gets a fresh nonce
  • script-src includes that nonce
  • any inline script in the rendered HTML actually has the nonce
  • dev-only allowances are not shipped to production
  • connect-src covers your APIs, analytics, and websockets
  • frame-src covers consent or embedded widgets if needed
  • you’re not using 'unsafe-eval' in production
  • you’re not using 'unsafe-inline' for scripts in production

Svelte 5 runes don’t complicate CSP. Third-party scripts do. SSR does. Careless inline code does.

If you keep your app bundle first-party, use nonces properly, and add external origins one by one instead of dumping in wildcards, CSP with SvelteKit is pretty manageable. That’s the version I’d ship.