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 .wasm files 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.

Where packaged modules live on each platform On macOS, modules go in the app bundle's Contents/Resources directory and are covered by the bundle signature. On Windows, they install beside the executable or in the package's resources, read-only. On Linux, they live in the package's data directory, inside the AppImage mount or the Flatpak's /app tree. Writable data goes elsewhere on every platform. platform read-only module location writable data location macOS (.app) Contents/Resources/wasm ~/Library/Application Support Windows (MSI / MSIX) install dir or package resources %APPDATA% / %LOCALAPPDATA% Linux (deb / rpm) /usr/share/<app>/wasm ~/.local/share/<app> AppImage / Flatpak inside the mount or /app XDG data dirs

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.

From build output to a signed, installed module The build produces the .wasm files. The packager copies them into the platform's resource location. The whole bundle or installer is signed and, on macOS, notarised. At runtime the app resolves the module through the framework's resource API, and writes caches to a per-user data directory. build .wasm release, wasm-opt copy into resources packager config sign + notarise modules covered resolve at runtime framework resource API caches elsewhere per-user data dir

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 .wasm files 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.

Update size when only the Wasm module changes Megabytes downloaded by users to fix a bug in the Wasm module, with a full application update, with a delta update of the application, and with a separately delivered module. MB downloaded per user full app update 92 MB delta app update 4.1 MB separate module update 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.

← Back to Wasm in Extensions & Desktop Apps