Serving Wasm over HTTPS on localhost
This page answers one question: when does a WebAssembly project need HTTPS in local development, and what is the least painful way to provide it with a certificate your browsers actually trust?
Prerequisites
- [ ]
mkcert(brew install mkcert,winget install FiloSottile.mkcert, or a release binary on Linux). - [ ] A dev server that accepts a certificate and key: Vite, Node’s
httpsmodule, Caddy, or nginx. - [ ] Administrator rights once, to install a local certificate authority.
Why localhost is usually enough — and when it is not
Browsers treat http://localhost and http://127.0.0.1 as secure contexts, the same as HTTPS. Features gated on
a secure context — SharedArrayBuffer with cross-origin isolation, service workers, WebGPU, the Origin Private File
System, crypto.subtle — all work on plain HTTP as long as the hostname is localhost. For a single developer
testing on one machine, HTTPS is genuinely unnecessary.
Three situations change that. Testing from another device on the network means using an IP address or a hostname
like laptop.local, which is not a secure context over HTTP. Using a custom hostname such as app.test to mirror
production cookies or multi-domain setups loses the localhost exemption. And reproducing production behaviour
around mixed content, HSTS or secure cookies requires the real scheme. Each of these is common in WebAssembly
projects, because threaded builds, storage-heavy apps and GPU work all depend on secure-context APIs.
Step 1 — create a local certificate authority
mkcert creates a certificate authority on your machine and installs it into the system and browser trust stores.
Certificates it issues are then trusted like any public certificate — no warning pages, no “proceed anyway”.
mkcert -install
Created a new local CA 💥
The local CA is now installed in the system trust store! ⚡️
The local CA is now installed in the Firefox trust store (requires browser restart)! 🦊
The CA’s private key stays on your machine, in the directory printed by mkcert -CAROOT. Never share it or commit
it: anyone holding it can issue certificates your machine trusts for any domain.
Step 2 — issue a certificate for every name you use
List every hostname and address the dev server will be reached by. Include your LAN address if you test on phones:
mkdir -p .certs
mkcert -cert-file .certs/dev.pem -key-file .certs/dev-key.pem \
localhost 127.0.0.1 ::1 app.test 192.168.1.20
echo ".certs/" >> .gitignore
A certificate only covers the names it was issued for. If your laptop’s IP address changes on another network, reissue it — the browser’s error will say the certificate is not valid for the address you used.
Step 3 — serve with Vite, Node or Caddy
Vite takes the files directly:
// vite.config.js
import fs from "node:fs";
export default {
server: {
host: true, // listen on the LAN, not just localhost
https: {
cert: fs.readFileSync(".certs/dev.pem"),
key: fs.readFileSync(".certs/dev-key.pem"),
},
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
};
A plain Node server needs only the https module:
import https from "node:https";
import fs from "node:fs";
import { handler } from "./static-handler.js"; // your existing request handler
https.createServer(
{ cert: fs.readFileSync(".certs/dev.pem"), key: fs.readFileSync(".certs/dev-key.pem") },
handler
).listen(8443, "0.0.0.0");
Caddy is the shortest of all: for a name like app.test it issues its own locally trusted certificate
automatically on first run, with no mkcert step:
# Caddyfile
app.test {
root * ./dist
file_server
header {
Cross-Origin-Opener-Policy same-origin
Cross-Origin-Embedder-Policy require-corp
}
}
Map app.test to 127.0.0.1 in your hosts file, run caddy run, and accept the one-time prompt to install
Caddy’s CA.
Keeping certificates out of the repository and in the workflow
Certificates for local development sit in an awkward place: every developer needs one, but each must be generated on that developer’s machine, because each machine has its own CA. Committing certificates to the repository does not work — they would be signed by one person’s CA and trusted by nobody else’s browsers — and it trains people to commit private keys, which is a habit to avoid on principle.
The pattern that works is a script that generates the certificate on demand, checked by the dev command:
#!/usr/bin/env bash
# scripts/dev-cert.sh — create a dev certificate for this machine if missing
set -euo pipefail
mkdir -p .certs
if [[ ! -f .certs/dev.pem ]]; then
command -v mkcert >/dev/null || { echo "install mkcert first"; exit 1; }
ip=$(ipconfig getifaddr en0 2>/dev/null || hostname -I | awk '{print $1}')
mkcert -cert-file .certs/dev.pem -key-file .certs/dev-key.pem localhost 127.0.0.1 ::1 "$ip"
fi
The dev server configuration should then fall back to plain HTTP when the files are absent, so a contributor who
only needs localhost is not forced through the certificate setup. In Vite that is a conditional around the
https option; for other servers, the same check before choosing https.createServer or http.createServer.
Treat the CA itself with the same care as any credential. It lives outside the project, in your user profile, and it is trusted by your whole machine — not just for this project. On a shared or managed machine, check with whoever manages it before installing one.
Step 4 — trust the CA on test devices
Your phone does not trust your laptop’s CA until you install it there. Export the root certificate and install it on the device:
cp "$(mkcert -CAROOT)/rootCA.pem" ~/Desktop/dev-root-ca.pem
On iOS, AirDrop or email the file, install the profile in Settings, then enable full trust for it under General → About → Certificate Trust Settings. On Android, install it under Security → Encryption & credentials → Install a certificate → CA certificate. Some Android apps ignore user-installed CAs, but Chrome honours them for browsing. The rest of the device workflow is in testing a Wasm app on a phone during development.
Step 5 — verify the secure context, not just the padlock
A padlock means the connection is encrypted with a trusted certificate. What the Wasm app needs is a secure context and, for threads, cross-origin isolation. Check both from the console on the device you are testing:
console.table({
secure: isSecureContext,
isolated: crossOriginIsolated,
sab: typeof SharedArrayBuffer === "function",
opfs: typeof navigator.storage?.getDirectory === "function",
gpu: "gpu" in navigator,
});
If secure is true but isolated is false, the certificate is fine and the problem is headers or a cross-origin
subresource — see configuring COOP/COEP headers for SharedArrayBuffer.
Expected output
curl -sI --cacert "$(mkcert -CAROOT)/rootCA.pem" https://192.168.1.20:5173/ | head -3
HTTP/1.1 200 OK
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
On the device, the console table shows true in every row, and the threaded build starts its workers.
Gotchas
NET::ERR_CERT_AUTHORITY_INVALIDon the phone. The CA is installed but not trusted. On iOS, the extra trust switch under Certificate Trust Settings is easy to miss.NET::ERR_CERT_COMMON_NAME_INVALID. The address you used is not in the certificate. Reissue with the current IP address.- Firefox still warns after
mkcert -install. Firefox uses its own trust store and needs a restart; on Linux,certutilfrom NSS must be installed for mkcert to reach it. - Self-signed certificates “accepted” with a click-through. Some APIs and service workers refuse to work on pages loaded with a bypassed certificate error. Use a trusted local CA instead of clicking through.
Performance note
TLS adds a handshake to the first connection — about 15 ms on a LAN — and nothing measurable to subsequent requests on the same connection. HTTPS also enables HTTP/2 in most dev servers, which multiplexes requests; on a page that loaded a module plus eighty small ES modules in Vite’s unbundled dev mode, cold load over HTTPS with HTTP/2 was faster than plain HTTP/1.1.
Frequently Asked Questions
Is mkcert safe to use?
For development, yes — it is widely used. The risk is the CA key: anyone who obtains it can intercept your traffic.
Keep it on your machine and uninstall the CA (mkcert -uninstall) when you no longer need it.
Can I use a real certificate from a public CA instead? For a real domain pointing at your LAN, yes — a DNS-validated certificate works on every device without installing anything. It is more setup but helpful for teams with many test devices.
Do I need HTTPS for Wasm itself? No. WebAssembly runs on plain HTTP. HTTPS matters only for the secure-context APIs many Wasm apps use alongside it.
Will HTTPS change how my module is cached in development?
Browsers cache compiled WebAssembly per origin, and https://localhost:5173 is a different origin from
http://localhost:5173. Switching schemes therefore starts with a cold compile cache, which can make the first
load after the switch look slower than it really is.
What about tunnelling services? Tunnels give you a public HTTPS URL that forwards to your machine, which avoids certificates entirely for device testing. They route your traffic through a third party, so avoid them for anything sensitive.
Related
- Testing a Wasm app on a phone during development — the main reason to need this.
- Spectre and cross-origin isolation for Wasm — why the gated APIs are gated.
- Persisting a Wasm database to OPFS — a secure-context API Wasm apps rely on.
- Driving WebGPU from Rust Wasm — another.
← Back to Local Development Server Configurations