rom-weaverA ROM & Patch toolkit

Self-hosting the webapp

rom-weaver is a static webapp. The browser-facing URL must use HTTPS (except for localhost). Host it on its own HTTPS subdomain or under a dedicated path such as https://example.com/rom-weaver/. A subdomain is the safest choice; a subpath is also supported because the build uses relative asset URLs and registers its service worker with a relative scope.

Do not mount rom-weaver at the root of an origin that also serves other apps. At the root, its service worker can control every path on that origin. Under /rom-weaver/, it controls only that path.

Docker

Build from source with Compose

Docker Compose builds the WASM module, bundles the webapp, and starts the included static server from a checkout:

git clone https://github.com/rom-weaver/rom-weaver.git
cd rom-weaver
docker compose up --build --detach
curl --fail --silent --show-error http://localhost:8080/health

This path only requires Docker with Compose; the image installs the required Rust, WASI SDK, Binaryen, and Node.js toolchains. The first build compiles the full WASM application and can take several minutes. Later builds reuse Docker's layer cache when their inputs have not changed.

To use another host port:

PORT=3000 docker compose up --build --detach

The container listens on port 8080 over plain HTTP. This is suitable when an HTTPS reverse proxy terminates TLS. The proxy must present a certificate that the browser trusts and forward the request to the container. For an Nginx subpath route:

location = /rom-weaver {
    return 308 /rom-weaver/;
}

location = /rom-weaver/weave {
    return 301 /rom-weaver/apply$is_args$args;
}

location = /rom-weaver/weave/ {
    return 301 /rom-weaver/apply$is_args$args;
}

location = /rom-weaver/weave.html {
    return 301 /rom-weaver/apply$is_args$args;
}

location = /rom-weaver/weave/index.html {
    return 301 /rom-weaver/apply$is_args$args;
}

location /rom-weaver/ {
    proxy_pass http://127.0.0.1:8080/;
}

The trailing slashes on both location and proxy_pass are significant: the proxy removes /rom-weaver/ before forwarding the request. The container adds the required COOP/COEP headers, serves SPA fallbacks, and serves the build's precompressed Brotli files.

For a dedicated subdomain, route its / location to the same container.

If you do not have a reverse proxy, Compose can terminate HTTPS in the container. If you do not provide a certificate pair, it generates a temporary self-signed certificate for localhost that expires after seven days:

HTTPS_PORT=8443 docker compose up --build --detach

Open https://localhost:8443/. A browser may allow you to proceed through the warning for local testing, but an expired or untrusted certificate can still prevent service-worker registration. For reliable service-worker and WASM thread support, install/trust the certificate or use a trusted certificate.

For a trusted certificate, put fullchain.pem and privkey.pem in a host directory and mount it with HTTPS_CERT_DIR. The container uses those default filenames when both are present:

HTTPS_PORT=8443 HTTPS_CERT_DIR=/path/to/certs \
  docker compose up --build --detach

To use different filenames or mounted paths, set both HTTPS_CERT and HTTPS_KEY to paths inside /certs. HTTPS_PORT is the host port and enables the container's TLS listener; it is not used together with PORT. The generated certificate is never suitable for production or public/LAN use.

Useful lifecycle commands:

docker compose logs --follow webapp
docker compose down

Static files

Install the system tools from the development guide, then build the static files from a checkout:

git clone https://github.com/rom-weaver/rom-weaver.git
cd rom-weaver
mise trust
mise install
npm ci
npm ci --prefix packages/rom-weaver-webapp
mise run build-wasm-prod
npm --prefix packages/rom-weaver-webapp run build

Upload everything under packages/rom-weaver-webapp/dist/ to your HTTPS host. Preserve the directory structure. The build emits raw assets; generic hosts should enable Brotli or gzip compression when available, especially for the WASM file. The Docker image is the only distribution that adds static .br siblings, because its bundled server is configured to consume them; it gzips on demand for clients that cannot take brotli.

The rom-weaver-webapp.tar.gz asset on each GitHub release contains this raw build, so unpacking it is an alternative to building from a checkout.

The build includes directory-index pages for /apply, /create, /trim, and /tools, so ordinary static servers can resolve direct visits and refreshes without rewrite configuration. A server that disables directory indexes must instead fall back to index.html for those navigation requests. Redirect /rom-weaver to /rom-weaver/ when using a subpath so relative assets, History API routes, and the service-worker scope resolve consistently. Explicit directory-document URLs such as /apply/index.html are normalized in the browser to the clean /apply route without another request. The old /weave forms are permanent-redirected to /apply by the Docker image and Cloudflare Pages build; configure the equivalent redirect on other static hosts.

Cloudflare-compatible hosts read the generated _headers file. On other hosts, the equivalent cache policy is:

/assets/*                 Cache-Control: public, max-age=31536000, immutable
/cache-service-worker.js  Cache-Control: no-cache

Only /assets/* uses immutable caching because those filenames contain content hashes. Do not apply that policy to HTML, the manifest, robots.txt, sitemap.xml, or other stable filenames.

Cross-origin isolation

The threaded WASM runtime requires SharedArrayBuffer and crossOriginIsolated. HTTPS is required outside localhost.

Prefer adding these response headers to every rom-weaver response, scoped only to its subdomain or path:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin

Do not apply them site-wide unless every application on the origin is expected to use those policies. Under Cross-Origin-Embedder-Policy: require-corp, any cross-origin resource loaded by the app must opt in through CORS or a compatible Cross-Origin-Resource-Policy header.

These headers can be scoped to the rom-weaver subpath. They do not need to be applied to unrelated applications on the same origin. The Docker image sends them on every response from its container; a reverse proxy or static host must preserve them, or add the same headers to responses under /rom-weaver/.

When a static host cannot set these headers, rom-weaver's service worker can add them for responses within its scope. See Service worker and subpaths for the bounded reload and fallback behavior.

After deployment, open the browser console and confirm:

crossOriginIsolated === true

If it is false, check the document's COOP/COEP response headers, HTTPS trust, and whether cache-service-worker.js controls the page.

Service worker and subpaths

Production builds register cache-service-worker.js using the app's relative asset base. When the app is served at /rom-weaver/, the service worker's default scope is /rom-weaver/; it does not control the origin root or sibling applications. Redirect /rom-weaver to /rom-weaver/ so relative assets, registration, and scope resolve to the same directory.

The service worker precaches the build, checks for updates, and can serve same-origin navigation and manifest requests from its cache. It can also add the cross-origin isolation headers to responses inside its scope when the host cannot configure them. It cannot alter the very first document response before it controls the page, so server or proxy headers remain the preferred setup.

On first install, the worker claims the app and the client may reload once to gain control and establish isolation. The boot gate retries a stalled controlled-but-unisolated page only within a bounded budget, then releases the page instead of reloading forever. If crossOriginIsolated is still false, the threaded WASM runtime will not be available; fix the response headers, HTTPS trust, service-worker scope, or browser support rather than treating the fallback as equivalent to a correctly configured deployment.

Host integration

To preload remote sources or files already stored in same-origin OPFS, use the webapp integration APIs.