Many apps. One page. One accessibility tree.
Billing owns the invoices page. Payments owns a widget. Everybody wants to deploy on their own schedule, without a release train.
That is the entire requirement. Every architecture below is an attempt at it, and each one breaks somewhere specific.
| Approach | What breaks | Why |
|---|---|---|
| One big app | one deploy for everyone | Their broken build is your broken build. Upgrading React becomes a company-wide project. |
| iframes | the box never opens | Cannot lay out with the page. A modal inside cannot cover the page. Screen readers see separate documents. Each reloads its own framework. |
| Module Federation | one global scope | Two copies of React fight. One team's polyfill patches a prototype and breaks another team's code — on a Tuesday, with no error pointing at the cause. |
| Web components | one registry, one realm | customElements.define is global and one-shot per tag name, so two versions of the same widget cannot coexist in a document. And the code still runs in the page's single JavaScript context. |
One team's bad deploy, slow endpoint, or uncaught exception does not take the page down with it. The blast radius stops at their box.
Billing upgrades React when Billing is ready. There is no shared version to negotiate and no company-wide migration to schedule.
No build-time contract between teams. The integration is a name and a URL, resolved at runtime — not an import resolved at compile time.
Ship on your own schedule. No release train, no coordinated cutover, no waiting for the slowest team in the group.
These are properties of the running system, not of the repository layout. Four repos that must deploy together are still one application — a distributed monolith wearing a micro-frontend's clothes.
Each app's JavaScript executes in its own hidden, isolated JavaScript context. Own window, own globals, own module registry.
Its rendered HTML sits in the main page, in normal layout flow, in one accessibility tree.
Concretely: <fragment-slot> owns a shadow root in the host page, and
inside the realm the fragment's document is a facade over it.
Members that touch the DOM are virtualized onto that shadow root; the rest pass through to
the realm's own document.
One independently deployed app. An ordinary web app on an ordinary server — usually containing no Braid code at all.
The app that owns the page: layout, navigation, and the places fragments appear.
<fragment-slot> — the element in the host's markup that says a fragment goes here.
The isolated JS context a fragment runs in. In practice: a hidden, same-origin <iframe>.
Where the fragment's DOM actually lands, inside the slot. Styles do not leak either way.
The gateway writing a fragment's HTML into the page server-side, before the browser sees it.
| Normal iframe | Braid realm | |
|---|---|---|
| Where the fragment's JS runs | in the iframe | in the iframe |
| Where the fragment's DOM appears | in the iframe | in the host page |
| Participates in page layout | no | yes |
| Separate accessibility tree | yes | no — one tree |
The iframe is used purely as a second JavaScript world, and its own document is nearly empty. A Worker has no DOM and cannot run synchronously with the page, so there is no third option.
document actually resolves to<fragment-slot name="billing"> #shadow-root <braid-document> <button id="pay">Pay</button>
querySelector virtualized → the shadow root cookie native → the realm's own doc
// billing app, unmodified document .querySelector('#pay') .addEventListener( 'click', pay);
The app never learns any of this happened. Members that touch the DOM are virtualized onto the slot's shadow root; the rest pass through native to the realm's own document. Anything unaudited passes through too, and logs a diagnostic in development rather than failing silently.
The default, and the path most teams take. An app you already have — Angular, React, whatever it is — composed with zero code changes. Configuration only.
Inside its realm it gets a convincing document, window, location, and history, all wired to its shadow root and its route.
Contract mode: no document facade, no window patches, no emulation of anything. The element is created inside the realm, so it upgrades against that realm's own definition, then moves into the host's DOM — adoption keeps its class and behaviour.
It needs no HTML at all. A widget that ships only a script is a complete fragment.
Why the second one exists at all: custom element registries are per window. Because the element is defined and created inside its own realm, two fragments can register the same tag name — or different majors of the same component library — and never collide. That is the web-components row from earlier, answered. It also gets a real props and events channel instead of an emulated one.
HTML arrives → JavaScript boots → slot fetches the fragment → fragment appears. The user watches an empty box.
The gateway fetches the host's HTML and the fragment's at the same time, splicing the second into the first as it streams past.
The markup is wrapped in a declarative shadow root — HTML that tells the browser "make this a shadow root as you parse it". When the slot's JavaScript starts, it finds the DOM already correct and adopts it rather than fetching anything.
This is not an SSR-only feature. A server-rendered fragment arrives already painted. A SPA fragment's shell arrives in the same response and boots in the realm — no separate document round trip either way. The requirement sits on the host, which must render server-side so there is a stream to splice into; no fragment needs SSR at all.
curl the page and it is right there.
Fragment markup lands in the host's DOM — so if it could execute, it would execute in the host's world. Before it arrives: every <script> is retyped inert, inline handlers stripped, meta refresh defanged.
Scripts are re-activated inside the realm, where they were always meant to run.
Braid never modifies globals or prototypes on the host page. Even compat's heavy emulation is confined to the fragment's own realm.
There is a test suite whose entire job is to fail if Node.prototype or history.pushState is touched on the host.
The alternative is the failure everyone has lived: a library patched a global, and three months later an unrelated feature broke with no trail leading back.
GET /billing/invoices — the gateway is middleware in front of the host, so it sees this first. It checks the registry: does any fragment declare it appears on this URL?<fragment-slot name="billing">, it injects billing's HTML as a declarative shadow root — after defusing it.The browser receives one HTML response containing both apps' markup, and paints it.
/__braid/doc/… instead.)GET /__braid/realm/billing/billing/invoices — a nearly empty stub carrying three things: protocol version, adapter name, and a <base> pointing into billing's asset namespace.replaceStates the realm's URL so the router sees its own route. Only then are the inert scripts run — until the facade exists there is no correct document for them to see.<base>: /__braid/frag/billing/main.js. The gateway strips the prefix and forwards to billing's server, which sees the paths it would serve standalone. It never learns it is embedded.
Once, and not everything. Each node is guarded, so a script re-inserted into
the DOM does not run twice. Non-JS types — application/json,
ld+json — are data blocks: they stay in the host DOM, unexecuted. And realms
share no module registry, so a tag the host and a fragment both include (Adobe, your
telemetry SDK) loads once per realm.
| /__braid/realm/:id/:route | The iframe stub. Identical for every user, varies on nothing. max-age=3600 with a year of stale-while-revalidate: the hour bounds how fast an adapter or protocol change reaches clients, the SWR year means nobody ever waits on the revalidation. |
| /__braid/doc/:id/:route | The fragment's prepared HTML, when nothing was pierced in: a client-side navigation (no document request reached the gateway), a slot created after load, or a pierce that failed or timed out. |
| /__braid/frag/:id/* | Everything the fragment's own code asks for, forwarded verbatim with the prefix stripped. |
| /__braid/registry | Paginated JSON list of fragments, filtered by who is asking. Off unless configured. |
Why realm and frag are separate: a stub varies on nothing;
assets vary by user, cookie, and encoding. One prefix for both would force the safest thing in
the system and the least safe thing to share cache rules.
A fragment that is a screen. It lives at certain routes, follows host navigation, and is fetched at the page's own path.
On /billing/invoices, the gateway asks billing's server for /billing/invoices.
A fragment that is a widget on the page — horizontally integrated rather than owning a route. A notifications panel, a sidebar, a global search box. It appears on many pages, and its content lives at one fixed address.
The page still renders. The slot is marked data-braid-fallback and the client tries to boot the fragment itself — a transient failure self-heals rather than becoming a broken page.
A manifest can opt into a visible error instead, when a missing section is worse than a visible failure.
Each fragment has a timeoutMs budget. Past it the gateway stops waiting and degrades to the same fallback. One slow widget cannot hold the whole page.
Measured in the POC: a 2s widget against a 400ms budget delayed the page by 407ms, not 2s.
Concurrent identical fragment fetches also collapse into one — not a cache, so no TTL and no dependence on which instance a request lands on.
Routes, the fragments they compose, the origins serving them. Routes carrying more than one fragment are highlighted, because that is where the coupling is.
Overlapping pierce patterns, incomplete manifests, empty access rules — surfaced at publish time rather than in production.
A pinned HEAD over published registry snapshots. Rolling back is re-pointing it, and nothing else.
Fragments never import each other — so the honest graph isn't dependencies. It's who shares a page, and who shares a deploy.
No bundler plugins. No shared-dependency negotiation. No webpack configuration. Step 3 is not a Braid step at all.
| Symptom | Cause | Fix |
|---|---|---|
| Content flickers, then re-renders. Two realms per slot. | The host framework discarded the server-rendered DOM and rebuilt it, so the new slot booted a second realm. | hydration, both bootstraps |
| A widget is an empty shell, or 404s, on every page. | It is bound when it should not be, so the gateway asks its server for the page's path. |
bound: false + src |
| Fragment CSS 404s; content renders unstyled. | Its relative URLs resolved against the host page — the response never went through the re-rooting transform. | fetch via /__braid/doc/ |
| A routed fragment hijacks the host URL. | A compat realm is a real iframe, so its boot navigation joins the joint session history. | own the route |
| A fragment vanishes on some page loads. | A shared cache served a composed page to a request that wanted the plain shell. | never edge-cache pierced pages |
| /__braid/frag/:id/* | Aggressively. The bulk of your traffic. Immutable if the fragment's build hashes filenames — the id is in the path, so two fragments' assets can never collide. |
| /__braid/realm/:id/* | Aggressively. Tiny and identical for a given URL. Caching it removes a round trip from every fragment boot. |
| /__braid/doc/:id/* | Follow the fragment's own headers. That team's call, and it often reflects a signed-in user. |
| a pierced page | Never at the edge. The gateway marks composed pages private, so a correct shared cache refuses them even if your shell said public. |
Where it runs: the gateway is a fetch handler, and
streaming is the requirement that eliminates platforms. Cloudflare Workers is the
best fit; ECS/Fargate, Cloud Run and Container Apps are all fine.
Lambda@Edge: avoid — no response streaming, so you keep composition
and lose the reason for it. CloudFront Functions: cannot — no network
access at all.
| braid-explained.md | The vocabulary and the page-load walkthrough this talk followed. |
| braid-poc.md | A running example: three frameworks and a server-rendered widget on one page, with the commands to start it. |
| braid-failure-modes.md | What goes wrong in practice, by symptom. Read this before debugging anything. |
| braid-from-module-federation.md | If you already know Module Federation, start here instead. |