cd /news/developer-tools/next-dev-renders-but-nothing-works-y… · home topics developer-tools article
[ARTICLE · art-108650] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

`next dev` Renders but Nothing Works: Your CSP Is Missing `unsafe-eval`

A developer of AI Change Watch, a small independent project, discovered that their Content-Security-Policy (CSP) was breaking all client-side interactivity in Next.js development mode. The issue arises because Next.js dev server uses eval for module compilation and React Refresh, which requires 'unsafe-eval' in the CSP, while production builds do not. The developer offers two solutions: conditionally widening the CSP in development or testing client behavior with a production build.

read5 min views1 publishedAug 24, 2026

I run AI Change Watch, a small independent project that

At some point I added a Content-Security-Policy. It was correct. It shipped. Production was fine.

And then, locally, every interactive thing on the site stopped working.

next dev

starts. The page loads. It looks exactly right — the layout, the data, the styles, all

of it. Then:

onClick

anywhere firesNo error page. No red overlay. No failed request in the Network tab. The server rendered the HTML and

sent it, so the page you are looking at is real — it is just completely inert. Nothing hydrated.

If you have not hit this before, the natural first guess is your own component. That is where I went,

and it is the wrong place, because every component is fine.

The console has it, but you have to be looking:

Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an
allowed source of script in the following Content Security Policy directive:
"script-src 'self' 'unsafe-inline' …"

And the reason it is easy to miss is that it is not a JavaScript error. It does not have a stack. It

does not point at your file. It appears once, near the top, above whatever else the page logged, and it

names a directive rather than a component.

Next's development server compiles modules and hands them to the browser wrapped in eval

— that is

how the dev devtool

setting works, and it is what React Refresh needs to swap a component without

re the page. Fast Refresh is built on it.

A production build does not do that. next build

emits static chunks. There is no string being

evaluated at runtime, so there is nothing for 'unsafe-eval'

to permit.

Which produces the trap:

The CSP is correct for production and fatal in development — and development is where you spend all

your time.

You will not catch it in CI, because CI builds. You will not catch it in preview, because preview

builds. You catch it the moment you try to click something locally, and by then you are three commits

into a feature and looking for the bug in your own diff.

Because it is in next.config

, and that file has no idea which mode it is running in unless you tell

it:

// next.config.mjs
async headers() {
  return [{ source: '/:path*', headers: securityHeaders }];
},

source: '/:path*'

means every path. There is no dev/prod branch, so next dev

serves the same header

next start

does. That is a reasonable default — you generally want to develop against the headers

you ship — it just happens to be wrong for this one directive.

Option A — widen the policy in development only.

const isDev = process.env.NODE_ENV === 'development';

const csp = [
  "default-src 'self'",
  // 'unsafe-eval' is DEV-ONLY: the dev server evaluates compiled modules as strings (that is what
  // React Refresh is built on), and a production build never does. Shipping it would be a real
  // widening of the policy for zero benefit.
  `script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ''}`,
  // The HMR socket, same reasoning. In production nothing connects back to the dev server.
  `connect-src 'self'${isDev ? ' ws: wss:' : ''}`,
  // …the rest
].join('; ');

The comment is not decoration. A conditional in a security header is exactly the kind of line that gets

"cleaned up" six months later by someone who reads it as an inconsistency, so the reason it is

conditional has to sit next to it.

Option B — stop testing client behaviour in next dev.

next build && next start

This is what I actually do, for a reason that has nothing to do with CSP: this project deploys to

Cloudflare Workers through OpenNext, and next dev

is not the runtime it ships on. Behaviour I verify

in dev is behaviour I verified somewhere the code will never run. So for anything client-side I build

and serve the real thing.

The cost is real — you lose Fast Refresh, and a rebuild per change is slow enough to change how you

work. If your production runtime is Node, Option A is the better trade. If it isn't, Option B was

going to be necessary anyway and this just makes it obvious sooner.

The thing worth taking away isn't the directive. It's this:

A security header set innext.config

applies to the dev server, and the dev server has different

requirements than the thing you deploy.

unsafe-eval

is the one that produces a silent failure, which is why it costs the most time. But the

same category catches you elsewhere:

directive what dev needs that prod doesn't
script-src
'unsafe-eval' for the module runtime / React Refresh
connect-src
ws: / wss: for the HMR socket
style-src
'unsafe-inline' if your prod build extracts CSS but dev injects it

If you are about to add a CSP to a Next app, the fastest check is not a code review. It is:

next dev

Thirty seconds, and it is the only test that distinguishes "rendered" from "working". Everything else

about a dead page looks identical to a live one.

The tracker this came out of is at aichangewatch.com — it watches AI vendor docs for changes. Its CSP still has no 'unsafe-eval' in production, which is the point.

── more in #developer-tools 4 stories · sorted by recency
── more on @ai change watch 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/next-dev-renders-but…] indexed:0 read:5min 2026-08-24 ·