Configuring Content Security Policy (CSP) in TanStack Start
updated:
The buzz around TanStack Start has really taken off since its recent RC release. I’ve been using it for a while (this very site runs on it!), and the latest version brings tons of improvements. One area that’s still a bit under-documented is how to configure a solid Content Security Policy (CSP) to mitigate security threats. The good news: with TanStack Start’s new middleware support, setting up CSP is now straightforward. In this post, I’ll walk through the approach I’ve been using.
Why Nonces?
There are a few ways to configure CSP, but TanStack Start has built-in support for nonces, so that’s what we’ll be using. When enabled, the framework automatically attaches nonces to our resources. All that’s left is to append your CSP headers through middleware.
Step 1: Global Middleware
First, we’ll want to apply CSP to every request. In TanStack Start, this is done by adding global middleware. The example below does four main things:
- Generates a strong cryptographic nonce,
- Defines (strict) CSP directives (
script-src
often needsstrict-dynamic
to allow trusted scripts to load additional ones), - Sets the CSP headers. I prefer using
Report-Only
in development to catch violations without blocking scripts, - Passes the nonce down to the next middleware in the chain.
import crypto from "node:crypto"; import { createMiddleware, createStart } from "@tanstack/react-start"; import { setResponseHeader } from "@tanstack/react-start/server"; const cspMiddleware = createMiddleware().server(({ next, request }) => { if (request.method !== "GET") { return next(); } const nonce = crypto.randomBytes(16).toString("base64"); const directives = [ "upgrade-insecure-requests", "default-src 'none'", "base-uri 'self'", "connect-src 'self'", "img-src 'self' data:", `script-src 'strict-dynamic' 'nonce-${nonce}'`, `style-src 'nonce-${nonce}'` "report-uri https://example.com/api/report-csp", "report-to csp-endpoint", ].join("; "); const headerName = import.meta.env.DEV ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy"; setResponseHeader(headerName, directives); setResponseHeader("Report-To", 'csp-endpoint="https://example.com/api/report-csp"'); return next({ context: { nonce }, }); }); export const startInstance = createStart(() => { return { requestMiddleware: [cspMiddleware], }; });
Step 2: Update your Router
Next, update your router to tell TanStack Start to use the nonce during Server-Side Rendering (SSR). That’s it, your script
and style
tags will automatically include their nonce attribute, and your app should continue to run normally.
import { createRouter } from "@tanstack/react-router"; import { getGlobalStartContext } from "@tanstack/react-start"; import { routeTree } from "./routeTree.gen"; export function getRouter() { const nonce = getGlobalStartContext()?.nonce; const router = createRouter({ routeTree, ssr: { nonce }, }); return router; }
Limitations & Advanced Patterns
While the steps above work today, keep in mind TanStack Start is still in RC. Future versions might change this behavior. A few caveats in my experience:
- Nonces are unique per request. Therefore, caching or prerendering (via TanStack Start) is not supported.
- TanStack Devtools (except from TanStack Query) don’t yet support nonces, so you’ll see plenty of CSP warnings in development. Thankfully, this code is stripped out in production.
- Third-party libraries might inject scripts or styles dynamically without exposing a way to attach a nonce. Inline styles are especially problematic since you can’t combine
unsafe-inline
with nonces. - Client-side nonce access: because the router is isomorphic, the nonce becomes
undefined
once the app is hydrated. If you need its value client-side, you can work around this using a global state manager. - Reporting CSP violations is a good practice, but services like Report URI can be overkill for hobby projects. Cloudflare does support reporting, but only if you configure CSP through their dashboard. Instead, I opted for a simple setup that logs violations to the console, which I can grep from my logs at a later time.
Conclusion
The RC release of TanStack Start makes CSP support easy to set up out of the box. With just a few lines of middleware, you can get a strong baseline in place. For larger or more complex apps, especially those relying heavily on third-party scripts, you’ll likely need to fine-tune your directives. However, this approach gets you started (pun intended).