Mark van Seventer


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:

  1. Generates a strong cryptographic nonce,
  2. Defines (strict) CSP directives (script-src often needs strict-dynamic to allow trusted scripts to load additional ones),
  3. Sets the CSP headers. I prefer using Report-Only in development to catch violations without blocking scripts,
  4. Passes the nonce down to the next middleware in the chain.
src/start.ts

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.

src/router.ts

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:

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).