July 20, 2026 • 8 min read
Here's a scenario I lived through way too many times before I fixed it. You're building a web app, or a PWA with all the trimmings: service worker, geolocation, maybe camera access for a QR scanner. On your laptop, at http://localhost:5173, it's perfect. Buttery animations, install prompt working, everything green in the console. You feel like a genius.
Then you grab your phone, connect it to the same Wi-Fi, type in your laptop's IP address, and suddenly your app is a different, worse app. No install prompt. No location permission dialog. The service worker refuses to register and just silently gives up like a coworker who stopped caring around 4:45pm. You didn't break anything. You just found out localhost has been lying to you the whole time.
A bunch of powerful browser APIs (service workers, geolocation, camera and mic access) only work in what browsers call a secure context. A secure context is either https:// or, as a special exception, localhost itself. That exception is exactly what's been hiding this problem from you.
On your laptop, localhost gets a free pass. The instant your phone hits your laptop's IP address instead (which it has to, because phones don't know what your localhost is), you're on plain http://, over a network, with none of the secure-context exceptions. So the exact features that matter most for a PWA are the first ones to disappear, and they disappear silently. No error banner. No helpful message. Just missing behavior, which is somehow worse.
Secure context = https:// or localhost. Your phone loading your laptop's IP address over plain http is neither, so anything that requires a secure context just quietly opts out.
None of these were actually solving the problem: they were all just working around not having HTTPS locally. Here's what I actually did instead, running Vite + React on a Linux machine, testing against an Android phone on the same Wi-Fi.
This is the address your phone will use to reach your laptop. On Linux:
hostname -IYou'll probably get more than one address back, something like 192.168.124.85 172.17.0.1 172.18.0.1, especially if Docker is installed:
192.168.x.x → your actual local network IP. Use this one.172.x.x.x → Docker's internal network, minding its own business. Ignore it: your phone can't see it and never will.So the address I actually want is 192.168.124.85, and eventually I want to be opening https://192.168.124.85:5173 on my phone.
By default, Vite only exposes your dev server to localhost; your phone, sitting on the same Wi-Fi, still can't reach it. Fix that first, before worrying about certificates at all:
export default defineConfig({
server: {
// Allow access from other devices on the network
host: true,
port: 5173,
},
});At this point http://192.168.124.85:5173 loads on your phone. Progress! But it's still plain http://, which means it's still not a secure context, which means the service worker and geolocation prompts are still ghosting you. This step only solved reachability, not security. Time for certificates.
mkcert is the tool that fixes this properly. Instead of a self-signed certificate that every browser screams at you about, mkcert creates a local Certificate Authority (CA) and installs it into your machine's trust store. Any certificate that CA issues afterward is trusted automatically, no "Your connection is not private" wall, no clicking through red warnings.
sudo apt install libnss3-tools
sudo apt install mkcertIf mkcert isn't in your distro's package manager, grab the binary directly from its GitHub releases page instead, same tool, just a different install path.
mkcert -installThen find where mkcert keeps that CA:
mkcert -CAROOTInside that folder you'll find two files: rootCA.pem and rootCA-key.pem.
rootCA.pem is the public certificate other devices need to trust: that's the one you'll copy to your phone. rootCA-key.pem is the private key for your CA. Never move that one anywhere. If it leaks, someone could mint certificates your machine trusts.
The doc says to "copy rootCA.pem to your phone" like it's obvious, but doesn't say how. Easiest way: send the file to yourself over WhatsApp or Telegram. If you'd rather skip an app, spin up a one-line file server instead and grab the cert over Wi-Fi:
cd "$(mkcert -CAROOT)"
python3 -m http.server 8000
# then on your phone, visit:
# http://192.168.124.85:8000/rootCA.pemrootCA.pem you just downloaded.The exact wording and depth of this menu shifts depending on your phone's manufacturer. Samsung especially enjoys hiding it one folder deeper than everyone else. After installing, Android shows a permanent notification that a certificate authority is installed on the device. That's expected: you did just tell your phone to trust an authority you invented in your terminal five minutes ago.
From this point on, your phone trusts any certificate mkcert creates.
mkcert 192.168.124.85 localhostSwap in your own IP from Step 1. This creates two files:
192.168.124.85+1.pem → the certificate192.168.124.85+1-key.pem → the private keyimport { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import fs from "fs";
export default defineConfig({
plugins: [react()],
server: {
// Allow access from other devices
host: true,
// Enable HTTPS using mkcert certificates
https: {
key: fs.readFileSync("./192.168.124.85+1-key.pem"),
cert: fs.readFileSync("./192.168.124.85+1.pem"),
},
},
});If your backend API needs HTTPS too so your app is secure end-to-end, the same certificate files work there: swap app.listen(...) for https.createServer({ key, cert }, app).listen(...) using the exact same key/cert pair.
npm run devYou should see something like:
Local: https://localhost:5173
Network: https://192.168.124.85:5173Open the Network URL on your phone, still on the same Wi-Fi.
Padlock icon, no warnings, secure context achieved. The service worker registers. "Add to Home Screen" shows up unprompted. Location and camera permission dialogs actually fire. Everything behaves exactly like it will in production, hours before you've deployed anything, from your own couch. The whole setup ends up looking like this:
Linux Machine
│
│ HTTPS
▼
Vite React App
192.168.124.85:5173
│
▼
Phone Browser“You're not testing a simulation of the real experience anymore. You're testing the real experience, just early.”
mkcert -install or reinstall anything on your phone; the CA stays valid).That's it. No tunnels, no staging deploys just to check a button, no more finding out your PWA doesn't quite work the way you thought after you've already told a client it does.