@k8ordo/server

Set a CSP

A Content-Security-Policy keeps a script that slipped into the content from running. In this mode the framework makes a new nonce for every answer, and a guard.ts writes the policy that names it as a header.

On this page

What the framework signs

The framework decides no policy. It signs the inline scripts it writes into the HTML, and the policy that allows them is the application’s to write.

The inline scripts the framework writes are the payload it puts into the HTML for hydration, and React’s own.

An inline script the application writes itself is allowed by the policy the same way — @k8ordo/color-scheme’s, which sets the colours before the first paint, among them.

Write the policy in guard.ts

The framework makes a nonce for every answer and puts it on its inline scripts and its module script. nonce() from @k8ordo/server/runtime returns it, so the root guard.ts writes it into the policy.

src/routes/guard.ts
import { nonce, responseHeaders } from '@k8ordo/server/runtime';

export default function guard() {
  const policy = [
    `script-src 'nonce-${nonce()}' 'strict-dynamic'`,
    "object-src 'none'",
    "base-uri 'none'",
  ].join('; ');
  responseHeaders().set('content-security-policy', policy);
}

The root guard.ts runs before every answer but a redirect.ts, so the page, its payload and the 404 for a URL nothing matched all carry the same policy.

The module script carries the nonce too, so under 'strict-dynamic' it loads the rest of the client.

Sign the application’s own inline scripts

An inline script of the application’s own carries the same nonce. For @k8ordo/color-scheme, pass nonce={nonce()} to ColorSchemeProvider in the root layout.

src/routes/layout.tsx
import { ColorSchemeProvider } from '@k8ordo/color-scheme';
import { nonce } from '@k8ordo/server/runtime';
import type { ReactNode } from 'react';

export default function RootLayout({
  children,
}: {
  children: ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ColorSchemeProvider nonce={nonce()}>
          {children}
        </ColorSchemeProvider>
      </body>
    </html>
  );
}

nonce() returns the same value anywhere the request is being answered, the render included: signing a script is not writing the response.

Called outside a request — at a module’s top level, say — it throws.

Do not cache an answer that carries a nonce

A nonce is worth something only while it is new to each answer. Kept in a shared cache such as a CDN, the same nonce goes out again and again, and an attacker who learned it can inject a script that carries it. Keep answers that carry one out of shared caches.

k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2