Running WebContainer in the Browser Without Breaking Firebase or Razorpay
I broke Razorpay and Firebase with a single line of code.
Running WebContainer in the Browser Without Breaking Firebase or Razorpay
I broke Razorpay and Firebase with a single line of code.
Here’s what that line was, why it had to be there, and how I fixed the two features it took down with it.
The problem
I was building NexusAI — an AI platform that handles code, image generation, PPTs, PDFs, search, and analyzing uploaded files and images. One of its features lets you write code with AI and see it run live in the browser. No backend spin-up, no Docker, no waiting.
To run real Node.js inside the browser, I used StackBlitz’s WebContainers. Genuinely impressive engineering.
But it comes with one non-negotiable requirement: cross-origin isolation. Two headers — COOP and COEP — have to be set on the page.
Quick context if you haven’t run into these before: browsers normally let pages of different origins interact a bit — open popups, share some memory, load each other’s resources. COOP (Cross-Origin-Opener-Policy) and COEP (Cross-Origin-Embedder-Policy) shut that down. They lock a page into its own isolated world, where it can’t be touched by — or reach out to — anything from a different origin. That isolation is exactly what unlocks SharedArrayBuffer, which is how WebContainers run Node.js at near-native speed in the browser.
The catch: that same isolation is what breaks Razorpay and Firebase. Both rely on opening a popup window and talking back to your page through it — the very cross-origin interaction COOP/COEP exists to block.
Turn them on, and Razorpay’s checkout popup goes silent. Firebase auth breaks too. Any window.open()-based flow just stops working, with no obvious error to point at.
I spent 3 days on workarounds. Relaxed headers. “Credentialless” mode. Reordering scripts. None of it worked — because this wasn’t a bug. It was two real requirements that can’t both be true on the same page, at the same time.
The fix: stop isolating the page. Isolate the piece that needs it.
Instead of forcing the whole app into isolation, I split it into three pieces:
- The app your users see — runs completely normal. No isolation. Razorpay and Firebase both work exactly as before.
- A second, invisible page — fully isolated, its only job is booting WebContainers.
- A small WebSocket gateway in between, relaying messages both ways.
The isolated page sits in a hidden iframe the user never sees. It boots the Node.js runtime, mounts the project files, runs npm install, starts the dev server, and reports back a live preview URL. The main app just drops that URL into an iframe and shows it.
Why WebSockets, not postMessage
My first instinct was window.postMessage between iframes. Made sense on paper — it’s built in, no extra server, low latency.
It broke under isolation for two reasons: cross-origin popups lose their window.opener once COOP is set, and postMessage listeners don’t survive an iframe reload cleanly. If the isolated host reloaded mid-session, the connection was just gone.
So I moved the whole bridge onto a persistent WebSocket instead. Both sides register a role, then relay everything through one gateway:
// Both the frontend and the isolated host connect here
ws.send(JSON.stringify({ type: "REGISTER", role: "nexusai-app" }));
ws.onmessage = (event) => {
const { type, payload } = JSON.parse(event.data);
if (type === "BRIDGE" && payload === "HOST_PONG") {
// host is alive — safe to send the project files now
sendBridge({ cmd: "RUN_PROJECT", files: currentFiles });
}
};
One detail that saved me a lot of pain: don’t send project files until you’ve seen a HOST_PONG back. Without that handshake, I’d occasionally fire files at a host that hadn’t finished booting yet — and it would just silently drop them.
How I actually debugged it
The failure mode was never a clean error — it was Razorpay’s popup just not opening, or Firebase auth hanging with no console output. That’s the annoying part of COOP/COEP bugs: the browser doesn’t tell you why.
The one command that saved me:
console.log(window.crossOriginIsolated);
Run it on the main app — should be false. Run it on the isolated host — should be true. If either one is wrong, nothing downstream matters yet.
What the isolated host is actually doing
It’s worth being specific here, because “boots WebContainers” hides a few real steps:
- It converts the incoming file list into a nested file-system tree and mounts it.
- It spawns npm install and streams the logs back over the WebSocket, so the UI can show real progress instead of a frozen spinner.
- Once install finishes, it spawns the dev server and waits for WebContainer’s server-ready event, which hands back a live preview URL.
- From then on, every keystroke in the editor becomes a WRITE_FILE message sent straight to the host — which triggers Vite’s HMR inside the container, so the preview updates instantly without a full reload.
None of this is visible to the user. They just see “Installing…” turn into a working preview.
I also seriously considered two other bridge methods before landing on WebSockets:
Method Pros Cons postMessage No extra server, lowest latency Dies under COOP, drops messages across reloads WebSocket gateway Origin-agnostic, survives reloads, one clean protocol Needs a running server, small network hop HTTP polling Dead simple Not real-time, bad for streaming logs and HMR
For anything that needs to stream logs or push file writes in real time, polling was never really in the running. It came down to postMessage vs. WebSockets, and isolation made that decision for me.
The actual lesson
Cross-origin isolation isn’t something you switch on for an app. It’s something you scope to the one component that genuinely needs it, and bridge everything else across a boundary.
Anything involving WebContainers, sandboxed iframes, WASM threads, or SharedArrayBuffer will eventually collide with something else in your stack — auth popups, payment widgets, third-party embeds. Design for that isolation boundary from day one. Retrofitting it later costs you days, not hours.
Building this taught me more about the browser’s security model than any docs page did.
Full architecture, code, and a repo to try it yourself — dropping in the comments.
If you’ve hit your own cross-origin nightmare, I’d genuinely like to hear how you solved it. 👇
WebDevelopment #SoftwareEngineering #JavaScript #SystemDesign #WebContainers
메타데이터
- post_id
- 63d49a56d7c8
- slug
- running-webcontainer-in-the-browser-without-breaking-firebase-or-razorpay-63d49a56d7c8
- url
- https://medium.com/@sudhanshukhosla123/running-webcontainer-in-the-browser-without-breaking-firebase-or-razorpay-63d49a56d7c8
- canonical_url
- https://medium.com/@sudhanshukhosla123/running-webcontainer-in-the-browser-without-breaking-firebase-or-razorpay-63d49a56d7c8
- author_url
- https://medium.com/@sudhanshukhosla123
- status
- ok
- fetched_at
- 2026-08-25 05:24:19