I’m sure you considered the option of not using Draco already. I’ll dump the suggestion from ChatGPT bellow, perhaps some other options are viable. We run draco in a worker, perhaps you could relax the rules there?
You’re hitting the browser’s CSP guardrail: by default, compiling/instantiating WebAssembly is treated like “eval”. If your script-src forbids it, calls like WebAssembly.compile/instantiate will be blocked with the error you posted. Modern CSP solves this with a dedicated switch: add 'wasm-unsafe-eval' to script-src (narrower than unsafe-eval, which also enables JS eval/new Function). MDN Web Docs+1
What in PlayCanvas uses WebAssembly (and is gated by CSP)
- Draco mesh decoder (for
KHR_draco_mesh_compression). Configure with dracoInitialize({ jsUrl, wasmUrl, ... }). api.playcanvas.com
- Basis/KTX2 transcoder (for
KHR_texture_basisu, .ktx2). Configure with basisInitialize({ wasmUrl, ... }). api.playcanvas.com
- Ammo.js physics (if enabled). PlayCanvas ships the physics engine as a WASM build. developer.playcanvas.com
- Meshoptimizer decoder (if using
EXT_meshopt_compression): the popular decoder is WASM-based. npm+1
- These (and other engine WASM helpers) are configured via
pc.WasmModule.setConfig(...). Note there’s a fallbackUrl to a JS build, but see caveat below. api.playcanvas.com
CSP-friendly options (from strict → flexible)
- Keep main page strict; allow WASM only in a worker
- Serve your Draco/Basis/physics worker script from a URL that responds with a relaxed CSP header, and load it as a Worker. Dedicated workers can have their own CSP (Chrome fixed this long ago), so the main document can stay locked down.
- Main page CSP example (only allows workers from a safe host):
Content-Security-Policy:
default-src 'self';
script-src 'self'; /* no eval in the page */
worker-src https://assets.example-cdn.com blob:;
connect-src 'self' https://assets.example-cdn.com;
- Worker response header (on
https://assets.example-cdn.com/decoders/draco-worker.js):
Content-Security-Policy:
default-src 'none';
script-src 'self' 'wasm-unsafe-eval';
```Docs: worker CSP & `worker-src` directive; dedicated workers use the CSP delivered with the worker script response. [MDN Web Docs+1](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/worker-src?utm_source=chatgpt.com)
2. **Allow WASM in the page, but not JS eval**
* Add **`'wasm-unsafe-eval'`** to `script-src` (do **not** add `unsafe-eval`). Example:
Content-Security-Policy:
default-src ‘self’;
script-src ‘self’ ‘wasm-unsafe-eval’;
worker-src ‘self’ blob:;
* This enables `WebAssembly.compile/instantiate` without opening up general JS eval. [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src?utm_source=chatgpt.com)
3. **Avoid runtime decoders entirely (no WASM needed)**
* **Don’t use Draco or Meshopt codecs** at runtime. Instead, ship **plain glTF** optimized with **`KHR_mesh_quantization`** (no decoder required). `gltfpack` does this by default:
gltfpack -i in.glb -o out.glb
That preserves good size wins via quantization, while remaining CSP-friendly. [meshoptimizer.org+1](https://meshoptimizer.org/gltf/?utm_source=chatgpt.com)
* For textures, avoid `.ktx2` (Basis) under strict CSP because it needs a WASM transcoder; use PNG/JPEG or pretranscoded GPU-native formats delivered conditionally per device. [github.khronos.org+1](https://github.khronos.org/KTX-Software/ktxjswrappers/msc_basis_transcoder.html?utm_source=chatgpt.com)
> ⚠️ **About JS fallbacks**
> Many asm.js/JS “fallbacks” (including Emscripten builds) use `new Function` under the hood and are *also* blocked when `unsafe-eval` is disallowed. So relying on `WasmModule.setConfig({ fallbackUrl: ... })` usually won’t help under a strict CSP. [api.playcanvas.com+1](https://api.playcanvas.com/engine/classes/WasmModule.html)
# Detection & graceful fallback
You can proactively test whether WASM compilation is allowed and choose assets accordingly:
async function canUseWasm() {
if (!(‘WebAssembly’ in window)) return false;
try {
// minimal empty module
const bytes = new Uint8Array([0,97,115,109,1,0,0,0]);
await WebAssembly.compile(bytes);
return true;
} catch (e) { return false; }
}
(async () => {
const wasmOk = await canUseWasm();
// Only initialize decoders when WASM is permitted
if (wasmOk) {
dracoInitialize({
jsUrl: ‘/vendors/draco/draco.wasm.js’,
wasmUrl:‘/vendors/draco/draco.wasm.wasm’,
numWorkers: 1
});
// basisInitialize(…) similarly if you use KTX2
}
// Choose a CSP-safe asset if WASM is blocked
const url = wasmOk ? ‘/models/thing-draco.glb’
: ‘/models/thing-quantized.glb’; // no Draco, uses KHR_mesh_quantization
app.assets.loadFromUrl(url, ‘container’, (err, asset) => {
if (err) {
// last-resort fallback to fully uncompressed
app.assets.loadFromUrl(‘/models/thing-uncompressed.glb’, ‘container’, () => {});
} else {
const entity = asset.resource.instantiateRenderEntity();
app.root.addChild(entity);
}
});
})();
PlayCanvas APIs referenced above: `dracoInitialize`, `basisInitialize`, and the `loadFromUrl` callback signature. [api.playcanvas.com+2api.playcanvas.com+2](https://api.playcanvas.com/engine/functions/dracoInitialize.html)
# Practical checklist
* If you control headers: prefer **`'wasm-unsafe-eval'`** (not `unsafe-eval`). [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src?utm_source=chatgpt.com)
* If your org forbids even that:
* Move decoders to a **Worker** served with its own CSP that allows `'wasm-unsafe-eval'`; ensure your page’s **`worker-src`** allows that origin (and `blob:` if you use Blob workers). [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/worker-src?utm_source=chatgpt.com)
* Ship **quantized (non-Draco, non-Meshopt) glTF** and **non-KTX2 textures** (or pretranscoded GPU-native textures). [meshoptimizer.org+1](https://meshoptimizer.org/gltf/?utm_source=chatgpt.com)
* Expect Draco, Basis/KTX2, Ammo, and Meshopt decoders to require WASM. Wire them up with `dracoInitialize`/`basisInitialize`/`WasmModule` only when your preflight allows it. [api.playcanvas.com+2api.playcanvas.com+2](https://api.playcanvas.com/engine/functions/dracoInitialize.html)
If you share your current CSP header and which PlayCanvas features you’re using (Draco, Basis, Meshopt, Ammo), I can suggest the minimal, targeted change that keeps your policy tight while unblocking the pipeline.