Browser / WasmEdge isomorphism

Use this profile when you want one compiled .wasm artifact that can be loaded unchanged in:

Canonical Build Rule

Declare:

json
{
  "runtimeTargets": ["browser", "wasmedge"]
}

That target pair now defaults to the shared single-thread artifact profile.

The compiler logic lives in src/compiler/compileModule.js.

The practical effect is:

Use the pure ["wasmedge"] target when you want maximum WasmEdge-native guest capability and do not need browser loading from the same binary.

Pthreads variant (shared-memory, isomorphic threading)

When a module needs real guest threads (the emscripten-pthreads thread model, default for ["wasmedge"]), the SDK compiles it through the wasi-threads toolchain (clang --target=wasm32-wasip1-threads -pthread), not Emscripten -pthread (which emits a browser-only Web-Worker build that cannot thread under WasmEdge). It enforces the wasi-threads link flags and validates the emitted .wasm — the artifact must import wasi.thread-spawn, export wasi_thread_start, be a shared-memory/atomics wasm, and carry no Emscripten worker hooks, or the compile is rejected. That guardrail is documented in docs/isomorphic-pthreads.md. This present document covers the portable single-thread loading profile; read the pthreads doc before shipping a threaded WasmEdge artifact.

Cross-Origin Isolation Is Required For Any wasi-threads/wasi-sequential Guest

Settled policy (module-sdk-target-forces-sab-coop-coep, arbitrated 2026-07-28, closed 2026-08-06): a host serving a module built through the wasm32-wasip1-threads toolchain — the emscripten-pthreads (real threading) model or the wasi-sequential model — to a browser MUST serve that page cross-origin isolated (Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp, or the equivalent). There is no build-flag escape hatch:

This means an inherently-sequential guest (wasi-sequential, no real threading) still needs a shared-memory instantiation — same as a real threaded guest — because it shares the same compiled triple. The portable single-thread profile this document otherwise covers (Emscripten/ emception, the ["browser"] / ["browser","wasmedge"] default: no shared memory, no atomics) is the one exception and needs no cross-origin isolation. Check which profile a guest actually uses — runtimeTargets alone does not tell you; an explicit threadModel does — before assuming either way.

How production achieves it today (both patterns are live and crossOriginIsolated-verified, not hypothetical):

  1. Native headers, when the host directly fronts the origin (e.g. a Caddy-fronted droplet): set Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp at the web server, which survives a CDN in front of it.
  2. A COI service worker, when the host is a static CDN that cannot set custom response headers (GitHub Pages is the concrete case: Pages sends no COOP/COEP at all). The worker intercepts navigation/asset fetches and re-serves them with the isolation headers injected, then the page does one capped self-heal reload. Reference implementation: coi-serviceworker.js + coi-bootstrap.js in the spaceaware-ui (sdn-js) and OrbPro Pages surfaces — verified live via window.crossOriginIsolated === true and typeof SharedArrayBuffer !== "undefined" in a real browser, per deployment/topology.json. Do not invent a second shim; port that one.

Either path additionally constrains every other subresource the page loads to be CORP/CORS-clean under require-corp — third-party embeds, fonts, tiles, imagery all have to cooperate. This is exactly the surface the standing "node UIs load ZERO external-origin bytes" law removes as a concern for the surfaces that already follow it; a new module-serving surface that does NOT yet follow that law has to solve subresource compatibility separately, before COI, not after.

The guardrail, concretely: verify crossOriginIsolated/SharedArrayBuffer with a real browser (live-verify.mjs-style, not a raw curl -I — a service-worker-injected header is invisible to a plain HTTP request) as part of standing up ANY new surface that serves a wasi-threads/wasi-sequential artifact, before assuming module instantiation works. A silent regression here fails as an instantiation error in the browser console, not a build error — there is currently no automated CI check for it; add one alongside the new surface's own verify tooling rather than assuming this document is enough.

Canonical Module Repo Layout

Module repos should publish the shared compiled artifact under a stable runtime path, not a plugin-named filename:

If a repo also ships a browser-specific adapter, place it under:

That keeps the artifact name stable across repos and lets runtime intent live in the path rather than in the filename.

Toolchain Options

The recommended browser-side build selector for module repos is:

The isomorphic contract is the compiled wasm path, not the browser wrapper. A repo can publish only dist/isomorphic/module.wasm and still satisfy the browser/WasmEdge shared-artifact requirement when it loads through the SDK harnesses.

Loader Entry Points

The supported browser/WasmEdge entry points are:

On the server path, loadModule(...) now chooses the raw WasmEdge command harness automatically for standalone artifacts with _start. The --serve-plugin-invoke runner protocol remains available for explicit runtime-host / runner-backed flows.

What The Browser Shims Cover

The browser edge shims map host capabilities onto browser-native surfaces:

These are host shims, not raw WasmEdge socket imports. When an embedding host needs to override the reference behavior, pass capabilityAdapters keyed by the canonical capability ids. That same generic async capability boundary is shared by BrowserHost, NodeHost, createRuntimeHost(), loadModule(...), and createBrowserModuleHarness(...).

Current Boundary

One binary can load in both browser and WasmEdge today when it stays within the shared profile:

Not browser-portable from the same raw guest binary:

Today’s portable split is:

For browser-hosted networking and IPFS/protocol work, use the browser edge shims or host-delegated adapters instead of relying on raw WasmEdge socket extensions.

Checked-In Demo

The canonical example is:

That example includes:

Both demos load the same generated artifact:

Streaming Into The Same Artifact

If that shared artifact owns resident state, such as an SDN module that imports flatsql internally, stream raw FlatBuffer frames into the live instance with createModuleFlatBufferStreamPump(...).

Use:

That path is documented in docs/flatsql-streaming-standard.md.

SDN Stack

Connected sites