TanStack Start Advanced
Content Security Policy
What c15t adds to the page
A TanStack Start app with c15t adds these to the page, and your policy must allow them:
| What | Directive |
|---|---|
The <style> elements the banner, dialog and other stock components render with c15t's rules | style-src with the request nonce |
The c15t stylesheet, only if you link it yourself with styles: false | style-src 'self' |
The <style id="c15t-theme"> element, if you render ConsentTheme | style-src with the request nonce |
<script> elements the script loader creates for scripts entries, both src and inline | script-src with the request nonce, or each vendor's host |
The inline consentPrefetchHead script, on prerendered pages that use it | script-src with its hash |
Browser requests to /api/c15t/*, when you mount the consent route | connect-src 'self' |
Browser requests to your backend's /init and /subjects | connect-src with the backend origin |
Iframes inside ConsentGate, and the frames, images and requests your vendors load | frame-src, img-src, connect-src for each vendor |
Requests the Start server makes, such as the manifest fetch in
createConsentStateHandler and the proxied saves from
createConsentRoute, run outside the browser. The browser's policy does
not apply to them.
Pass the request nonce to c15t
TanStack Router stamps a nonce on the scripts, styles and links it renders when
the router has ssr.nonce. Give c15t the same value, so the script loader,
the <style> elements the stock components render, and ConsentTheme use it
too. This takes three steps.
1. Create the nonce and the policy in request middleware. Generate a new nonce for every request, send the policy header, and hand the nonce to the rest of the request through the middleware context:
Add cspMiddleware to requestMiddleware in src/start.ts, creating the file
with createStart if your app has none. If the list also has
consentRequestMiddleware(), put cspMiddleware first. Replace
https://<your-backend-host> with the origin of the backend URL from your Inth
project or self-hosted backend, and add your vendors' hosts.
2. Give the router the nonce. getRouter runs on the server after the
global request middleware, so it can read the nonce from the Start context. In
the browser, TanStack Router reads it back from the
<meta property="csp-nonce"> tag that HeadContent renders:
3. Pass the router's nonce to ConsentRoot. In the root component from the
quickstart,
read the nonce with useRouter from @tanstack/react-router and pass it as
options.nonce:
If you render ConsentTheme, pass it the same value:
<ConsentTheme theme={theme} nonce={nonce} />.
ConsentRoot reads options.nonce when it mounts, and the script loader
stamps it on every <script> it creates. A nonce on a single scripts entry
takes precedence for that element. Each full page load gets the nonce of its
own request. Do not serve nonce pages from a shared cache, because every
visitor would get the same nonce.
What the nonce does not cover
- Scripts that a vendor script loads itself, such as the tags a tag manager
injects.
'strict-dynamic'in the policy above allows scripts that a nonced script adds. Without it, list each vendor's hosts inscript-src. - Inline
styleattributes. In the browser, c15t components set their inline styles through React, and a policy does not restrict styles set that way. If the console showsRefused to apply inline stylefor a server-rendered c15t element, addstyle-src-attr 'unsafe-inline'. A nonce cannot allow style attributes. - Iframes, images and requests your vendors make. Add their hosts to
frame-src,img-srcandconnect-src. A YouTube player insideConsentGateneedshttps://www.youtube-nocookie.cominframe-src, for example. - The DevTools panel. It adds a
<style>element without a nonce, so under this policy it renders unstyled. Load it only in development.
Allow the consent backend
Which consent requests the browser makes depends on your rendering setup:
| Setup | Browser requests | Policy |
|---|---|---|
| Quickstart | /init and /subjects on your backend URL | connect-src with the backend origin |
Consent route with createConsentStateHandler({ routePrefix: '/api/c15t' }) | /api/c15t/init on your origin, /subjects on your backend URL | connect-src 'self' plus the backend origin |
Consent route with proxy: true on the route and on createConsentStateHandler({ proxy: true, routePrefix: '/api/c15t' }) | Every consent request on your origin | connect-src 'self' |
Under an IAB TCF policy the browser also loads the Global Vendor List. With the
consent route and routePrefix, it comes through /api/c15t/init, which
'self' covers. Otherwise the browser fetches it from the URL the policy
provides, so allow that host in connect-src. See
IAB TCF.
Prerendered pages and SPA mode
A prerendered page is the same HTML for every visitor, so it cannot carry a per-request nonce. Use host allowlists and hashes instead:
- List
'self'and each vendor's script host inscript-src. consentPrefetchHeadadds an inline script whose content depends only on its options. Load the page with the policy enforced, copy thesha256-...value from theRefused to execute inline scriptconsole error, and add it toscript-src. Recompute it when you change the options.ConsentThemeoutput depends only on the theme, so a hash works for it instyle-srcthe same way. Recompute it when you change the theme.
TanStack Start adds its own inline scripts to prerendered pages. Its documentation covers how to allow those.
Verify
Build the app, start the production server, and load a page with the policy enforced, not in report-only mode. Test under a policy that asks for a choice, such as an EU opt-in policy.
- The banner renders with its theme colors, and the console shows no
Refused toerror that mentions a c15t element, your backend or a vendor. - In the Elements panel,
<style id="c15t-theme">and the<meta property="csp-nonce">tag carry the same nonce as the response'sContent-Security-Policyheader. Browsers hide thenonceattribute fromgetAttribute, so read the element'snonceproperty in the console. - Before a choice, DevTools Network shows no vendor requests.
- Click Accept All. The script loader's
<script>elements, with ids startingc15t-, carry the nonce, and the vendor requests appear. - The save to
/subjectssucceeds. Aconnect-srcviolation here means the backend origin is missing. - Reload. The header has a new nonce, and the elements carry the new value.