CodX Editor lets you run a Node.js server without installing anything. You
write server.js, press run, and a real Node process starts —
inside the browser tab. No container, no remote machine.
That works because of WebContainer, which compiles Node to WebAssembly. And
WebAssembly needs SharedArrayBuffer, which browsers only hand out
to pages that are cross-origin isolated. Isolation means
sending two headers on every response:
Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp
We set them, Node ran in the browser, and everything looked fine. Then the adverts stopped appearing.
What those headers actually do
Cross-Origin-Embedder-Policy: require-corp tells the browser to
refuse any resource from another domain unless that domain explicitly opts in
with a Cross-Origin-Resource-Policy header of its own.
It is a blunt instrument, and deliberately so. The isolation it buys protects against a class of timing attacks, and the price is that everything the page pulls in from elsewhere has to agree to be embedded.
Advert networks do not agree. Google says so plainly in its Publisher Tag documentation: displaying ads means embedding cross-origin content, and making that content COEP-compatible would require changes to every resource in every ad, both the ones Google serves and the ones third parties serve.
The symptom is silence. No error dialog, no broken layout. The advert script is simply never fetched, the slot stays empty, and the page looks like a site that has no ads at all.
Why it was hard to spot
Three things made this take longer than it should have.
- The headers were global. They were applied in one middleware at the top of the server, so every page carried them — the homepage, the help page, the privacy policy. None of those pages need Node.js. All of them lost their ads.
-
Nothing visibly failed. Blocked subresources appear in the
browser console as
ERR_BLOCKED_BY_RESPONSE, but only if you have the console open and know to look. - It looks like an approval problem. An empty ad slot is exactly what you see while an application is still under review, so the natural assumption is that the problem is with the account, not the code.
Why the obvious fixes do not work
Switch to credentialless
Cross-Origin-Embedder-Policy: credentialless is the gentler option:
cross-origin resources load without opting in, just stripped of cookies. It
solves the problem for images and scripts.
It does not solve it for adverts, because an advert is an
<iframe>, and nested documents still have to carry their own
COEP header or be marked credentialless individually. Ad networks
inject their frames at runtime and do neither.
Sandbox the preview instead
Dropping allow-same-origin from the preview iframe gives it an
opaque origin, which sounds like isolation by another route. It also severs
window.parent, and the whole editor depends on that channel to
forward console output and handle navigation between project files. The cure
removes the feature.
What we settled on
There is no configuration where both work on the same page load. So the headers became conditional rather than global:
const alwaysIsolated =
requestPath === "/node-preview" ||
requestPath.startsWith("/vendor/webcontainer");
const editorWantsNode =
isEditorPath && String(req.query?.node || "") === "1";
if (alwaysIsolated || editorWantsNode) {
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
}
Every page loads without isolation by default, so adverts work everywhere. The
moment someone opens the Node.js runtime, the editor reloads once with
?node=1, the headers appear, and SharedArrayBuffer
becomes available. Project files live in browser storage, so the reload costs
nothing.
The trade-off is honest and narrow: while you are running Node, that one page shows no ads. Everywhere else on the site, and for every visitor who never touches the runtime, nothing changes.
What to take from this
- Security headers have a blast radius. One middleware at the top of a server applies to pages that have nothing to do with the feature it was added for.
- Silence is a symptom. COEP failures do not throw. If something third-party stops appearing and nothing errors, check your response headers before you check your account.
- Some features genuinely conflict. Not every requirement can be satisfied at once. Scoping the conflict to the smallest possible surface beats forcing an incompatibility to work.
If you are building anything with WebContainer, StackBlitz, or any other SharedArrayBuffer-dependent tool, and you also plan to monetise: decide early which pages need isolation, and keep it off everywhere else.