# Security headers cookbook Every response from every app emits six security headers and a per-request CSP nonce. This guide walks the wiring for each framework, shows how consumer code threads the nonce into inline scripts, explains CSP allowlist customisation, and covers Sentry nonce integration and verification. --- ## The six headers `buildSecurityHeaders(opts: SecurityHeadersConfig)` from `@repo/core-shared/security` always emits: | Header | Value | | --------------------------- | -------------------------------------------------- | | `Strict-Transport-Security` | `max-age=31536000; includeSubDomains; preload` | | `X-Frame-Options` | `DENY` | | `X-Content-Type-Options` | `nosniff` | | `Referrer-Policy` | `strict-origin-when-cross-origin` | | `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | | `Content-Security-Policy` | mode-dependent (see [CSP modes](#csp-modes) below) | `x-nonce` is also forwarded as an internal request header so server components can retrieve the nonce without another round trip. --- ## CSP modes ### `prod` — strict-dynamic + nonce ``` default-src 'self'; script-src 'strict-dynamic' 'nonce-{NONCE}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: {ALLOWED_IMG_ORIGINS}; font-src 'self' {ALLOWED_FONT_ORIGINS}; connect-src 'self' {ALLOWED_CONNECT_ORIGINS}; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; ``` `strict-dynamic` allows scripts loaded by a nonce-bearing script to run without listing each origin explicitly. ### `dev` — permissive for local tooling ``` default-src 'self'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: {ALLOWED_IMG_ORIGINS}; font-src 'self' {ALLOWED_FONT_ORIGINS}; connect-src 'self' ws: localhost:* 127.0.0.1:* {ALLOWED_CONNECT_ORIGINS}; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; ``` `unsafe-inline` / `unsafe-eval` permit Vite HMR and React DevTools. The `ws:` and `localhost:*` entries allow HMR websockets and local API calls. These never appear in `prod`. Both modes are selected automatically from `NODE_ENV`. No manual config is required. --- ## Per-framework wiring ### Next.js (`apps/web-next`) Create or update `middleware.ts` at the app root: ```ts // apps/web-next/middleware.ts import { withSecurityHeaders } from "@repo/core-shared/security/next"; import type { NextRequest } from "next/server"; export function middleware(request: NextRequest) { return withSecurityHeaders(request); } export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], }; ``` `withSecurityHeaders` generates a fresh nonce per request, sets all six headers on the response, and forwards the nonce in both the downstream request headers (`x-nonce`) and the response headers so server components can read it. ### TanStack Start (`apps/web-tanstack`) Security headers are applied via a Nitro/H3 server hook in `app.config.ts`: ```ts // apps/web-tanstack/app.config.ts import { defineConfig } from "@tanstack/start/config"; import { withSecurityHeaders } from "@repo/core-shared/security/tanstack"; interface H3SecurityEvent { node: { req: { headers: Record }; res: { setHeader: (name: string, value: string) => void }; }; } function applySecurityHeaders(event: H3SecurityEvent): void { const { nonce, headers } = withSecurityHeaders(); for (const [k, v] of Object.entries(headers)) { event.node.res.setHeader(k, v); } event.node.req.headers["x-nonce"] = nonce; } export default defineConfig({ server: { hooks: { request: applySecurityHeaders } }, }); ``` `withSecurityHeaders()` returns `{ nonce, headers }`. The hook applies the headers to the response and stores the nonce on the request so `getNonce(req)` can read it from any server loader. ### Payload CMS (`apps/cms`) The CMS is server-rendered without client-side JavaScript hydration, so no nonce is needed: ```ts // apps/cms/middleware.ts import { buildSecurityHeaders } from "@repo/core-shared/security"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; export function middleware(_request: NextRequest): NextResponse { const mode = process.env.NODE_ENV === "production" ? "prod" : "dev"; const secHeaders = buildSecurityHeaders({ mode }); const response = NextResponse.next(); for (const [name, value] of Object.entries(secHeaders)) { response.headers.set(name, value); } return response; } export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], }; ``` --- ## Nonce threading for inline scripts ### Reading the nonce on the server **Next.js** — `getNonce()` reads the `x-nonce` header injected by `withSecurityHeaders`: ```ts import { getNonce } from "@repo/core-shared/security/next"; // Inside a Server Component or route handler: const nonce = await getNonce(); ``` **TanStack Start** — `getNonce(req)` reads from the H3 request: ```ts import { getNonce } from "@repo/core-shared/security/tanstack"; import { getEvent } from "vinxi/http"; // Inside a loader: const nonce = getNonce(getEvent().node.req); ``` ### Exposing the nonce to the browser Expose the nonce via a `` tag so client-side code can read it without re-fetching: **Next.js root layout:** ```tsx // apps/web-next/src/app/layout.tsx import { getNonce } from "@repo/core-shared/security/next"; export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const nonce = await getNonce(); return ( {children} ); } ``` **TanStack Start root route:** ```tsx // apps/web-tanstack/src/routes/__root.tsx import { getNonce } from "@repo/core-shared/security/tanstack"; import { createRootRoute, Outlet } from "@tanstack/react-router"; export const Route = createRootRoute({ loader: async () => { try { const { getEvent } = await import("vinxi/http"); return { nonce: getNonce(getEvent().node.req) }; } catch { return { nonce: "" }; // client-side navigation — nonce already in DOM } }, component: () => { const { nonce } = Route.useLoaderData(); return ( <> ); }, }); ``` ### Using the nonce in your inline scripts Pass the nonce as the `nonce` attribute on any `