Before you register or mount anything, there's one question that decides whether a given bundle can be an island at all:
Was it built to mount into a container someone hands it — or to be the whole page?
Everything else follows from that. Note what the question is not about: it says nothing about size, richness, statefulness, internal routing, or whether you call it a "SPA". A full file-management applet — drag-and-drop, virtual scrolling, its own toolbars and internal views, tons of state — sitting in the middle of a page is a perfect island. That is exactly what this library is for (see Philosophy: islands are autonomous dashboards, org-browsers, applets). An island can be a whole application. It just isn't the page.
So the axis is embeddable component vs page owner — a packaging decision, made by how the app is built, not by how complex it is.
Island-able: the requirements
An app can be mounted inline (and get the boundary:
context / live / api / channel / bus) when its bundle:
- Mounts into the
targetit's handed — never grabs#app,document.body, or any fixed element it expects to find on the page. - Exports a mount function —
export default (target, opts) => { setProps, destroy }(the mount contract). It does not run and mount itself as a side effect of being imported. - Carries its own dependencies — bundles its framework/vendor code. It does
not assume page globals like
window.$, jQuery, or a global Bootstrap are already loaded. - Resolves assets relative to itself — sibling files via
new URL("./thing.css", import.meta.url), or CSS injected by JS. No absolute root paths (/css/…,/js/…,/assets/…), which break the moment the bundle is served under a different origin or base path. - Has a stable entry filename (
main.mjs) — not a per-build hash likeindex-a1b2c3.js, so it can be registered and re-served. - Scopes its styles — shadow DOM or scoped/prefixed classes, so it doesn't restyle the host page.
- Routes internally — in-memory or hash state it owns. It does not try to drive the browser URL/history, which belongs to the host page.
Meet those and the same app works in both delivery modes (:direct /
:proxy), single- or multi-file, on a LiveView and a plain page — no variants.
Authoring apps shows how to produce this from Svelte, React,
Lit or vanilla JS; the mechanics are a library build (e.g. Vite build.lib),
not an app build.
Page-owning: the anti-pattern
A default framework app build (what vite build gives you without build.lib)
produces the opposite. Tell-tale signs a bundle is a page owner, not an island:
- ships an
index.htmland auto-mounts to a fixed#appon import; - absolute root asset URLs (
/css/…,/js/…,/vite.svg), and font/imageurl(/…)inside its CSS; - relies on page-global vendor scripts loaded by its own
<script>tags (jQuery, Bootstrap); - a hashed entry filename;
- wants to own the browser URL/history.
None of these are about the app being "too big" — they're build-output choices.
Re-target the same application as a library build (mount into target, export
the fn, bundle its deps, relative assets) and it becomes a first-class island.
Can't repackage it? Use an iframe
When you don't control the build — or it genuinely is a whole standalone app you
can't re-target — don't force it into an island. Embed it in an <iframe>. A
page-owning build already ships an index.html with absolute paths, which is
exactly what an iframe wants; host it at its own path/subdomain and the "island"
becomes a thin adapter that renders an <iframe src="…"> sized to the container.
The trade-off: an iframe is a separate document, so it does not get the
in-process boundary. Configuration and messaging cross the frame via the URL and
postMessage, not context / bus directly. Full isolation is the upside — its
globals, CSS, and absolute paths can't collide with the host.
Rule of thumb
- You control the app's build → make it island-able (a library build meeting the list above), and it plugs straight into the boundary.
- You don't — or it's a whole app, not a component → iframe it.
The proxy is orthogonal to this choice: it only decides where the bytes come from (same-origin vs cross-origin), never whether the thing is mountable in the first place.