When I started Vibecodr.Space, I had a simple, slightly dangerous idea: a "post" should not be a screenshot of code, it should be the code. Click it, it runs. Share it, it runs. Embed it, it runs. Remix it, it runs.
That sentence contains the entire security problem.
If you let strangers run arbitrary JavaScript inside a product that also has accounts, profiles, storage, and a social graph, you are basically building a tiny browser inside the browser. And the browser already has a job. It is very opinionated about what is allowed, what is isolated, and what breaks when you get cute with iframes.
This is the story of how I stopped trying to outsmart the browser and started treating isolation as a product feature.
The first failure mode: "It works on my site, but it faceplants on Reddit"
In the early versions, everything looked fine when I tested on vibecodr.space. Then I would post a link on Reddit, someone would embed it, and I would get the worst kind of bug report:
"It loads, but it doesn't."
You could see the runtime iframe render under the hood, but the UI layer above it would get stuck in a "loading" state. Or the app would run, but the handshake between frames would never complete. Or fetches that worked locally would silently fail inside an embed.
Those bugs are maddening because they are not "your code is wrong." They are "your model of the browser is wrong."
The recurring root causes were always the same. Origin and sandbox rules are not intuitive, and "null" is a real origin. CSP and frame-ancestors are the only consistent way to enforce embed rules. CORS is not just about making requests work, it is also about limiting ambient authority. Caching is a security feature when access rules can change or embedding can be revoked.
At some point I stopped patching symptoms and rewrote the shape of the system.
The design that finally held: three planes, three responsibilities
The architecture that worked in production is a three-plane chain:
External Site -> Embed Wrapper (policy gate) -> Player (control plane) -> Runtime Frame (execution cage)
That sounds obvious in hindsight, but I had to discover why every shortcut turns into a long-term tax.
The embed wrapper exists to do one job extremely well: enforce where something is allowed to be embedded. That decision belongs at the edge, in a way the browser actually respects. CSP frame-ancestors is that mechanism. It is boring, strict, and reliable.
The player exists to do the product job: sizing, controls, prompts, postMessage coordination, errors, and the human-friendly face of the vibe. It is a control plane.
The runtime frame exists to be a cage. It runs the user code with the tightest constraints I can make reliable.
If you blur these responsibilities, you get either insecurity or fragility, usually both.
Why the player needed its own origin
A big question I get is: "Why not just serve the player from vibecodr.space?"
Because the player is embed-facing. It must be framable by anyone. If it shares an origin with your authenticated app, you have just welded "framable by anyone" to the same origin that holds privileged cookies, account state, and internal surfaces.
Even if modern browsers often block third-party cookies in iframes, that is not a security model. That is a browser policy that varies by browser, by setting, and over time.
So I wanted an invariant I could actually reason about:
The embed player origin must be cookie-less and unprivileged.
That is why I treat player.vxbe.space (my dedicated embed/player subdomain) as a public control plane origin. It can talk to the API for public reads. It can never rely on ambient cookies. If the player ever needs to do something sensitive, it must use an explicit, intentional auth flow, not "oops, cookies were present."
This single decision made several other decisions cleaner, especially around sandboxing and CORS.
Sandboxing and the "null origin" trap
This matters because a lot of normal web app behavior silently assumes origins are not null. When they are, things break in ways that often do not throw helpful errors.
If you build anything with nested iframes, you eventually meet:
event.origin === "null"
That happens when the browser considers a browsing context to have an opaque origin, often because of iframe sandbox without allow-same-origin.
Here is the painful part: the sandboxed-origin flag is inherited by descendants. If your outer embed iframe is opaque, the nested player does not magically become a stable origin again. It inherits the sandboxed-origin behavior.
That is why a seemingly neat idea ("outer opaque for safety, inner stable for reliability") falls apart in practice. In a three-plane chain, the browser will not allow you to have an opaque outer ancestor and a stable-origin nested child. You either embrace null-origin everywhere, or you keep the embed chain in stable-origin mode.
So I made a call that surprises some people: I allow allow-same-origin on the embed wrapper iframe and on the inner player iframe.
This is the key nuance. allow-same-origin does not grant the embedding page any new ability to read the iframe. The embedder is still cross-origin from embed.vxbe.space. What allow-same-origin does is give the iframe itself a stable identity so postMessage, CORS, and origin checks behave predictably.
It is only safe if those origins are truly unprivileged and do not run arbitrary scripts. That is why the player gets its own cookie-less origin, and why the embed wrapper keeps a locked-down CSP.
The alternative is to treat null-origin as a first-class mode everywhere, with special-case CORS and special-case messaging rules. That can work, but it is fragile by design. I would rather keep origins stable and keep privileges absent.
CSP is the contract, dispatch is the enforcer
If creators can decide where their vibes can be embedded, you need an enforcement point that cannot be bypassed by clever front-end code.
CSP frame-ancestors is that enforcement point. The browser enforces it before your JavaScript runs.
The complication is that runtime frames are served through a dispatch layer (Cloudflare Workers routing) and can be nested under multiple ancestors in different contexts: first-party viewing, player embeds, external sites, and more.
So I standardized something that became a guiding principle: if two components need the same policy decision, they must share the same source of truth.
I created an ALWAYS_ALLOWED_ANCESTOR_ORIGINS set, and I make two places share it.
First, the dispatch worker that merges frame-ancestors into the final CSP.
Second, the runtime bridge that decides which parent origins are allowed to handshake via postMessage.
This prevents an entire class of "works in one place, breaks in another" bugs, where your CSP allows a chain but your runtime bridge rejects it, or vice versa.
Caching: cache the expensive bytes, not the policy envelope
There is a temptation to cache everything, because vibes are code and code can be heavy.
The trick is to cache the right layer.
The runtime frame document is not the heavy part. It is a small HTML scaffold with headers that encode policy: CSP, permissions policy, referrer policy, and sometimes values that affect runtime behavior.
Caching that HTML aggressively is low value and high risk. It is the part most likely to cause "why is this embed behaving differently today?" confusion, especially if embed rules can change or embedding can be revoked.
So the runtime frame HTML uses conservative caching:
Cache-Control: private, max-age=0, must-revalidate
Then I cache the expensive bytes separately: bundles and compiled artifacts. Those can be cached hard only when the URL is truly immutable. If an artifactId is immutable, the bundle can be:
Cache-Control: public, max-age=31536000, immutable
If it is not immutable, it should not pretend to be.
Revocation should be real. If someone turns off embedding, I want it to take effect reliably, not "sometime after the CDN feels like it."
CORS split-by-sensitivity: reliability without ambient authority
Early on, I made the classic mistake: I tried to fix embed breakage by widening CORS defaults. It works, until it does not.
The better model is to treat API endpoints by threat profile.
Public reads that the embed player needs can allow the player origin. They can be GET-only, credential-less, and explicitly Vary by Origin.
Authenticated endpoints and write endpoints should be locked to the primary app origins only. Even if you use Authorization headers and not cookies, narrowing CORS reduces the blast radius of future mistakes. It turns "a bug" into "a contained bug."
This is not about pretending CORS is authentication. It is about preventing a framable surface from becoming a universal browser-readable window into your API if you ever slip up.
postMessage: the handshake that prevents "whispering into the wrong ear"
Even with CSP and sandboxing, frames still need to talk. The player needs to coordinate with the runtime. The runtime needs to request capabilities (clipboard, downloads, sizing, etc.). That means postMessage.
The rule I settled on is strict but simple. Only accept messages from window.parent. Require a per-session secret. Require the verified origin to match what was negotiated. Always send outbound messages with targetOrigin set to the verified parent origin.
If a flow ever involves a null origin, it is treated as lower trust and must be explicitly enabled. In normal embed flows, I avoid null origin by using allow-same-origin plus a dedicated player origin.
This is the difference between "it works" and "it still works when you share it on the internet."
What this gave me, beyond security
The funny thing is that the biggest payoff was not "I am safer." It was "I am calmer."
When a new embed bug happens now, I can reason about it. I can inspect a CSP header and know whether the browser will frame it. I can look at Origin and predict whether messaging should succeed. I can read cache headers and predict whether a policy change will take effect.
Security is not just protection. It is predictability.
If you are building anything that runs untrusted code, my biggest advice is: do not treat isolation as a pile of headers. Treat it as an architecture. The browser is already a security system. Your job is to align with it, not fight it.
---
If you want to play with the results, Vibecodr.Space is a social runtime where posts are executable, embeddable apps. The only way that idea survives contact with the real internet is if the sandbox is both tight and boring.
I am beginning to love boring.
Braden
