Packaging Wasm Files in Desktop Installers
This page answers one task: a desktop application — Electron, Tauri, a native app embedding a webview or a Wasm runtime — ships one or more .wasm
modules, and they must be found, trusted and updatable on every platform’s installer format.
Prerequisites
- [ ] A desktop app that loads
.wasmfiles at runtime. - [ ] The packaging tool for your stack: electron-builder or Forge, the Tauri bundler, or platform tools (pkgbuild, WiX/MSIX, deb/rpm/AppImage/Flatpak).
- [ ] Code-signing identities for the platforms you ship to.
What can go wrong with a file that just needs to be read
A .wasm file is data: the application reads it and compiles it. That makes packaging look trivial, and in development it is — the file sits next to the
code. Installed applications are different. On macOS, files live inside an .app bundle with a strict layout, and the whole bundle is signed and notarised;
adding or modifying files after signing breaks the signature. On Windows, files are installed under Program Files (read-only for normal users) or into a
per-user directory, and MSIX packages are virtualised. On Linux, the same app may arrive as a .deb, an .rpm, an AppImage that mounts itself at a random
path, a Flatpak with a sandboxed file system, or a Snap. Code that finds the module with a path relative to the working directory, or that writes next to
the module, fails in some of these.
The reliable approach has three parts: put modules where each platform expects read-only application resources, resolve their location through the framework’s resource API rather than hard-coded paths, and treat anything downloaded later as untrusted until verified.
Step 1 — declare modules as application resources
Tell the packager that .wasm files are resources, not code to transform or bundle away:
// electron-builder (package.json "build" section)
{ "extraResources": [{ "from": "dist/wasm", "to": "wasm", "filter": ["**/*.wasm"] }] }
// Tauri 2 (tauri.conf.json)
{ "bundle": { "resources": { "../dist/wasm/*.wasm": "wasm/" } } }
For modules loaded by the webview front end, the front-end bundler usually emits them into the web assets, which the framework serves itself; only modules loaded by native or Node code need explicit resource entries.
Step 2 — resolve paths through the framework
Never compute module paths from process.cwd() or relative to the executable by string manipulation. Use the framework’s resolver, which knows the
installed layout:
// Electron main or utility process
const wasmPath = app.isPackaged
? path.join(process.resourcesPath, "wasm", "engine.wasm")
: path.join(__dirname, "..", "dist", "wasm", "engine.wasm");
// Tauri 2, Rust side
let path = app.path().resolve("wasm/engine.wasm", tauri::path::BaseDirectory::Resource)?;
let bytes = std::fs::read(path)?;
Native apps embedding wasmtime or another runtime use the platform’s bundle APIs — NSBundle.main.url(forResource:) on macOS, the executable’s directory
on Windows, and $APPDIR or XDG data directories on Linux. Centralise the logic in one function and log the resolved path at startup in debug builds.
Step 3 — sign and notarise with the modules inside
On macOS, include the modules in the bundle before signing, so they are covered by the code signature and notarisation. Do not write to the bundle at runtime — caches, compiled code and downloaded modules belong in Application Support. On Windows, sign the installer and the executables; data files are covered by the installer’s signature, and MSIX packages are signed as a whole. Some Wasm runtimes cache compiled native code; point those caches at a writable per-user directory, since the install directory is read-only for normal users.
Step 4 — verify integrity, especially for downloaded modules
Modules installed with the app are protected by the platform’s signing. Modules downloaded later — plug-ins, language packs, machine-learning models, updated engines — are not. Verify each one before compiling it: check a SHA-256 hash against a signed manifest, or verify a detached signature with a public key embedded in the app, as in verifying Ed25519 signatures in Wasm. Store downloaded modules in the per-user data directory, never inside the signed bundle.
Step 5 — update modules without reinstalling the app
Shipping the module separately from the app lets you fix module bugs without a full app update, which on some platforms requires store review. Keep a compatibility version: the app declares which module ABI versions it supports, the update server offers only compatible modules, and the app falls back to its bundled copy if a downloaded module fails verification or instantiation. Record the module version in crash reports. If store policies restrict downloading executable code — some mobile and Mac App Store policies do — check whether WebAssembly modules count as code under the policy before relying on this.
Size and installer budgets
WebAssembly modules are usually small next to an Electron or webview runtime, but machine-learning models and large engines compiled to Wasm can be tens or hundreds of megabytes. Installers compress their payloads, so the download size is closer to the compressed module size; still, consider shipping large optional modules on demand rather than in every install. Delta updates, supported by several updaters, transfer only changed files, so keeping large modules stable between releases — and separate from frequently changing assets — shortens updates. Measure the installed size and the update size for each platform when adding a module, since a 40 MB model that changes every release costs every user 40 MB per update.
Testing installed builds on every platform
The only reliable test is installing the real package. In CI, build the installer for each platform, install it in a clean virtual machine or container
(macOS runners, Windows runners, and Linux containers for deb, rpm and AppImage), launch the app, and run a smoke test that loads each module and calls an
export. That catches missing resources, wrong paths, signature problems and read-only directory assumptions. Check notarisation status on macOS with
spctl --assess and installed file permissions on Linux. These tests are slower than unit tests, so run them on release branches if not on every commit.
Antivirus and enterprise environments
Corporate machines add obstacles that development machines lack. Antivirus products occasionally quarantine unfamiliar binary files, and a .wasm
file in a user-writable directory can be flagged or locked during scanning, causing intermittent load failures right after an update. Keeping modules
inside the signed installation and downloading updates into a dedicated per-user directory reduces false positives, and signing installers with a
reputable certificate helps more. Enterprise deployments may also block downloads entirely, so the bundled module must always remain a working fallback.
Group-policy controlled machines sometimes mount home directories over the network, where per-user caches are slow; let the cache location be configured
and fail gracefully to compiling without a cache.
Expected output
On macOS, Contents/Resources/wasm/engine.wasm is covered by the notarised signature; on Windows, the module installs read-only and caches go to
%LOCALAPPDATA%; on Linux, deb, AppImage and Flatpak builds all resolve the module through the same function; and a downloaded update module is rejected
when its signature does not verify.
Gotchas
- Writing inside the app bundle. It breaks macOS signatures and fails on read-only installs. Use per-user data directories.
- Paths relative to the working directory. Launchers start apps from arbitrary directories. Use resource APIs.
- Bundlers inlining or renaming modules. Configure them to emit
.wasmfiles as resources. - Unverified downloaded modules. Verify hashes or signatures before compiling.
- Testing only the development build. Install the real package on each platform.
Performance note
Reading and compiling a 1.4 MB module from the resources directory took about 35 ms on first launch on a mid-range laptop; with a per-user compiled-code cache it took about 6 ms on later launches. Shipping the module separately reduced a typical fix update from 92 MB to 0.6 MB.
Frequently Asked Questions
Do Wasm modules need their own code signature? They are data to the OS; the bundle or installer signature covers them. Downloaded modules need your own verification.
Can I compile modules ahead of time for the installer? With server-side runtimes such as wasmtime, yes — but precompiled code is platform-specific and must be built per target.
Where should a Wasm runtime cache compiled code? In a per-user cache directory, keyed on the module hash and runtime version.
Do Flatpak and Snap sandboxes affect modules? Only file access: modules inside the package are readable; writable data must use the sandbox’s data directories.
Why does the module fail to load right after an update on some machines? Antivirus scanning can lock new files briefly. Retry loading, and keep a bundled fallback.
Should Wasm files be signed with the installer? Yes — include them in the signed bundle so tampering is detected along with native files.
Related
- Running Wasm in Electron apps — runtime loading in Electron.
- Using Wasm in a Tauri app — resources in Tauri.
- Embedding wasmtime in a Rust application — native hosts.
- Publishing Wasm release artifacts from CI — producing the files to package.
← Back to Wasm in Extensions & Desktop Apps