---
title: Content Security Policy
description: Pass a per-request CSP nonce to ConsentRoot in Next.js and allow
  the consent backend in connect-src.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Pass the request nonce to ConsentRoot

Read the nonce from the `x-nonce` request header in the root layout and pass it
as `options={{ nonce }}` on `ConsentRoot`. This is the standard Next.js
nonce pattern: your `proxy.ts` (Next.js 16) or `middleware.ts` (Next.js 15)
generates a nonce per request, sets the `Content-Security-Policy` response
header with `'nonce-<value>'` in `script-src` and `style-src`, and forwards the
same value on the request as `x-nonce`. See the
[Next.js CSP guide](https://nextjs.org/docs/app/guides/content-security-policy)
for the header-generating proxy.

If you also run [`c15tProxy`](/docs/frameworks/next/geography-headers), set
both `x-nonce` and the generated `Content-Security-Policy` header on
`request.headers` before calling it, and set the policy on the returned
response as well. Next.js reads the nonce for its own bootstrap and page
scripts from the request-side policy header, so forwarding only `x-nonce`
leaves the framework scripts without a nonce and the browser blocks hydration.
`c15tProxy` copies the incoming request headers into the forwarded request, so
both headers travel with the geography headers.

Then read the header in the root layout and pass it to `ConsentRoot` as
`options.nonce`. A string can cross from a Server Component to a Client
Component, so no wrapper is needed:

```tsx title="app/layout.tsx"
import { ConsentBanner, ConsentDialog, ConsentRoot } from 'c15t/next';
import { resolveConsent } from 'c15t/next/server';
import { headers } from 'next/headers';
import { Suspense } from 'react';
import type { ReactNode } from 'react';

import './globals.css';

async function ResolvedConsent({ children }: { children: ReactNode }) {
	const nonce = (await headers()).get('x-nonce') ?? undefined;
	const state = await resolveConsent();

	return (
		<ConsentRoot state={state} options={{ nonce }}>
			{children}
			<ConsentBanner />
			<ConsentDialog />
		</ConsentRoot>
	);
}

export default function RootLayout({ children }: { children: ReactNode }) {
	return (
		<html lang="en">
			<body>
				<Suspense fallback={null}>
					<ResolvedConsent>{children}</ResolvedConsent>
				</Suspense>
			</body>
		</html>
	);
}
```

Reading `headers()` keeps the route dynamic, which a per-request nonce requires
anyway. Do not combine a nonce-based policy with `cacheComponents: true`
(Partial Prerendering): the static shell is rendered once at build time, so its
scripts cannot carry a per-request nonce and the browser blocks them. Next.js
documents this limitation in its CSP guide. For a prerendered shell use the
hash-based policy that guide describes; a host-only `script-src` such as
`'self'` does not authorize Next.js's inline bootstrap scripts and would need
`'unsafe-inline'`, which defeats the policy. Otherwise keep the route fully
dynamic and use the nonce.

`options.nonce` is read when `ConsentRoot` mounts. The provider keeps the latest
value in a ref, but the script loader is created once per runtime, so a nonce
that changes during client-side navigation is not applied to scripts already
created. Each full page load gets the fresh nonce from its own request.

## What the nonce applies to

c15t stamps a nonce on three kinds of DOM nodes:

* Every `<script>` element the script loader creates for a `scripts` entry,
  both `src` and inline `textContent` scripts, gets `options.nonce`. A `nonce`
  set on an individual `scripts` entry takes precedence for that element.
* The `<style>` elements that the banner, dialog and other stock components
  render with c15t's rules get `options.nonce`. With a nonce set, they render
  next to each component instead of moving into `<head>`, because React drops
  the nonce of a style it moves.
* The inline `<script>` that a server-rendered `ConsentBanner` puts before its
  buttons gets `options.nonce`. It holds a tap the visitor makes before the
  page hydrates. If the policy blocks it, the banner still works after
  hydration, but a tap before then does nothing.
* The `<style id="c15t-theme">` element that `ConsentTheme` renders with the
  theme's `--c15t-*` custom properties gets its `nonce` prop. Pass it the same
  request nonce: `<ConsentTheme theme={theme} nonce={nonce} />`.

It does not apply to anything else:

* Scripts that a vendor script loads itself, such as a tag manager injecting
  its tags, do not receive the nonce. Add `'strict-dynamic'` to `script-src` so
  scripts loaded by a nonced script are allowed, or list those vendor hosts
  explicitly.
* The prebuilt stylesheet `c15t/next/styles.css`, if you import it with
  `styles: false`, is a regular stylesheet from your own origin. It is covered
  by `style-src 'self'`, not by the nonce. Use that setup when a page cannot
  carry a per-request nonce.
* Inline `style` attributes on rendered components. A nonce cannot authorize
  style attributes, and browsers ignore `'unsafe-inline'` in a `style-src` list
  that also contains a nonce or hash. Allow them with a separate
  `style-src-attr 'unsafe-inline'` directive, or with `'unsafe-hashes'` plus
  the matching `'sha256-...'` values; hashes only work for static values, not
  for the animated collapse heights. The IAB TCF dialog, the consent dialog
  trigger toolbar and the animated collapse used inside the dialogs render
  inline `style` attributes. Check the
  browser console with your policy enforced to see whether your setup triggers
  a `style-src` violation.
* Iframes, images or requests made by vendor scripts. Allow those hosts in
  `frame-src`, `img-src` and `connect-src` according to each vendor.

## Allow the consent backend in connect-src

The browser sends consent submissions to `${backendURL}/subjects`, and, when
the browser initializes or retries after a failed server prefetch, requests to
`${backendURL}/init` or the manifest URL. Add the backend origin to
`connect-src`, and the manifest origin too when the mode's `manifestURL` is an
absolute URL on a different host such as a CDN:

```txt title="Content-Security-Policy (partial)"
connect-src 'self' https://<your-backend-host> https://<your-manifest-host>;
```

Use the endpoint host supplied by your Inth project or your self-hosted backend.
With `proxy: true` and `routePrefix: '/api/c15t'`, as
[Optimization](/docs/frameworks/next/optimization#keep-browser-consent-requests-on-your-origin)
shows, the browser sends every consent request to `/api/c15t` and `'self'`
covers them; the Next.js server connects to the backend outside the browser's
CSP.

IAB TCF policies can fetch the Global Vendor List from the URL the policy
provides; allow that host in `connect-src` if you use the
[IAB TCF add-on](/docs/frameworks/next/iab). Server-side prefetch runs
in Next.js and is not subject to the browser's CSP.

## Verify

Load a page with the policy enforced, not in report-only mode, and open the
browser console. The banner should render with its theme colors and no CSP
violation should mention `c15t-theme`. Grant a category that gates one of your
`scripts` entries, then check:

* The injected `<script>` element carries the request's nonce. Browsers hide
  the value from `getAttribute('nonce')`; read the element's `nonce` property
  in the console or inspect it in the Elements panel.
* The `<style id="c15t-theme">` element carries the same nonce and the
  `--c15t-*` variables are applied to the banner.
* No `Refused to load the script` or `Refused to apply inline style` errors
  mention a c15t element. A refused vendor dependency points to a missing
  `'strict-dynamic'` or vendor host.
* The consent submission to `/subjects` succeeds. A `connect-src` violation
  here means the backend origin is missing.

Reload with a different nonce and confirm the elements update. A stale nonce
after a full page load usually means a cached HTML response; nonce-based pages
must not be served from a shared cache.
