Vigilante’s town is a PixiJS 8 scene inside a React 19 and Vite app, hosted on Vercel. Locally it was fine. In production, the settlement scene broke.
The screen became a blank canvas and one console error.
Nothing in the code had changed between the two environments. The page’s security headers had.
Dev and production aren’t the same browser environment
Our production Content Security Policy is set in vercel.json. It’s strict on purpose: scripts only from our own origin and a short list of payment and ad domains, and no 'unsafe-eval'. That blocks eval() and new Function().
npm run dev doesn’t serve those headers. So anything that evaluates code at runtime works locally, and you only find out in production.
Why a renderer needs eval
For speed, PixiJS generates small pieces of JavaScript at runtime, such as the functions that upload shader uniforms, and compiles them with new Function(). It’s a common performance trick, and a strict CSP blocks it.
The fix: one import with a confusing name
PixiJS 8 ships eval-free versions of that generated code. You opt in with a side-effect import before creating the renderer:
// Production CSP has no 'unsafe-eval'; this swaps Pixi's generated // shader/uniform code for eval-free versions. import 'pixi.js/unsafe-eval'; Despite the name, this import lets Pixi work without unsafe-eval. We kept the policy strict and fixed the renderer, rather than loosening security for one game scene.
Making sure it can’t come back
- A unit test holds both halves in place. It checks that the production CSP still has no 'unsafe-eval', and that the scene file still starts with that import. If someone “cleans up” the odd-looking import, or loosens the policy, the test fails.
- Anything that adds a library, worker, canvas or WebGL code, or an external request gets tested as a production build served with the real headers, not just in npm run dev.
Other ways to handle it
- Add 'unsafe-eval' to the policy. It’s the easiest option and the worst one: it weakens the whole app to fix one scene.
- Serve different headers in dev. It catches this class of bug earlier, but the two drift apart easily.
- Test a production build locally with the real headers. That’s what we do now for anything risky.
What the scene is for?
The Pixi scene is a town that your real GitHub work rebuilds. Every verified day drops a crate, and the crates build Lantern District.
Have you been caught by a dev/production header mismatch? What broke for you?