How to test a PWA on iPhone (and from localhost)

Why your PWA works on localhost and breaks on the phone, how to get HTTPS to an iPhone in five minutes, and what iOS still does differently in 2026.

Updated 5 min read

PWAiPhone 17 Pro · 402 × 874
iPhone 17 Pro home screen with an installed PWA icon

A PWA has two lives. In the first it’s a website in Safari. In the second, after Add to Home Screen, it opens as an app: no toolbar, its own icon, content running up to the Dynamic Island and the home indicator, and (if your service worker is right) working offline. Most PWA bugs live in the second life, and that’s the one almost nobody tests before shipping.

This guide covers the part the other tutorials skip: getting your local build onto a real iPhone with HTTPS, and what changed in iOS 26.

The four ways to test, and what each catches

Method Service worker Installed (standalone) layout Real iOS behavior Setup
Desktop Chrome, localhost Yes Desktop window only No None
Framed device simulation (Breakpane App mode) Yes, Chromium Yes, with each iPhone’s safe areas No Minutes
iPhone + HTTPS tunnel Yes Yes Yes 5 minutes
iPhone + mkcert on your LAN Yes Yes Yes 15 minutes, once
Xcode iOS Simulator (Mac only) Yes Yes Mostly Xcode install

Why it works on localhost and breaks on the phone

Service workers only register in a secure context. Browsers treat http://localhost as secure, so everything works on your laptop. Your phone can’t reach your laptop’s localhost; it reaches http://192.168.0.12:5173, which is plain HTTP on an IP address, so it is not a secure context. The page loads, but navigator.serviceWorker is undefined, nothing caches and the install does not behave like an app.

Quick check in the phone’s console (or as a temporary on-page log):

console.log(window.isSecureContext, 'serviceWorker' in navigator);
// false false  → you need HTTPS

You have two ways to fix it.

Option A: an HTTPS tunnel (fastest)

A tunnel gives your dev server a public https:// URL with a valid certificate. Nothing to install on the iPhone.

# Cloudflare quick tunnel, no account needed
cloudflared tunnel --url http://localhost:5173

# or ngrok
ngrok http 5173

Open the printed https://….trycloudflare.com URL in Safari on the iPhone. Two things to know: Vite blocks unknown hosts, so allow the tunnel host, and hot reload over a tunnel can be flaky, so expect to refresh by hand.

// vite.config.ts
export default defineConfig({
  server: { allowedHosts: ['.trycloudflare.com', '.ngrok-free.app'] },
});

The URL changes each time you start a quick tunnel. Since the installed app is tied to its origin, a new URL means a new install.

Option B: mkcert on your network (best for daily work)

mkcert creates a local certificate authority and certificates your devices trust. One time setup:

mkcert -install
mkcert 192.168.0.12 localhost
# creates 192.168.0.12+1.pem and 192.168.0.12+1-key.pem

Then trust the CA on the iPhone:

  1. Find the CA with mkcert -CAROOT and send rootCA.pem to the iPhone (AirDrop or email).
  2. Install it in Settings › General › VPN & Device Management (iOS shows it as a downloaded profile).
  3. Turn on full trust in Settings › General › About › Certificate Trust Settings. Without this step Safari still rejects the certificate.

Point your dev server at the files:

// vite.config.ts
import fs from 'node:fs';
export default defineConfig({
  server: {
    host: true,
    https: { key: fs.readFileSync('192.168.0.12+1-key.pem'), cert: fs.readFileSync('192.168.0.12+1.pem') },
  },
});
# Next.js
next dev -H 0.0.0.0 --experimental-https \
  --experimental-https-key ./192.168.0.12+1-key.pem \
  --experimental-https-cert ./192.168.0.12+1.pem

Now https://192.168.0.12:5173 is a secure context on the phone, with hot reload intact.

Install it on the iPhone

In Safari: Share › Add to Home Screen. There is no install prompt on iOS and no beforeinstallprompt event, so if your UI shows an install button, show iOS users the Share instructions instead.

Since iOS 16.4, Chrome, Edge and other browsers on iPhone can add to the home screen too. Since iOS 26, every site added to the home screen opens as a web app by default, even without a manifest; the user can switch Open as Web App off in the sheet.

Open the app from the icon and check:

  • Standalone mode. Your CSS and JS can detect it:
@media (display-mode: standalone) {
  .install-banner { display: none; }
}
const installed = matchMedia('(display-mode: standalone)').matches || navigator.standalone === true;
  • Safe areas. Without the Safari toolbar, your header sits under the status bar and your tab bar under the home indicator. With viewport-fit=cover in the viewport meta, pad them with env(safe-area-inset-top) and env(safe-area-inset-bottom). If the values stay at zero, see why safe-area-inset-bottom is not working.
  • Status bar. <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> lets your content run under the status bar (so the top inset matters); default keeps a solid bar.
  • Icon. iOS uses an apple-touch-icon of 180×180 if present, and Safari 26 also reads the manifest icons.
  • Splash. iOS doesn’t build a splash screen from the manifest the way Android does. Without apple-touch-startup-image tags you get a blank screen in your background color.
  • Offline. Turn on Airplane Mode, open the app from the icon, and confirm your cached shell loads instead of an error.

Debug the installed app

With a Mac: on the iPhone, enable Settings › Apps › Safari › Advanced › Web Inspector. On the Mac, enable Safari’s Develop menu (Safari › Settings › Advanced). Connect the iPhone by cable, open the home screen app, and it shows up under Develop › your iPhone, service worker included.

Without a Mac: inspect.dev inspects iOS pages from Windows or Linux (paid, with a small free tier). Otherwise, log to the page itself during testing.

Android: connect by USB, open chrome://inspect on the desktop and set up port forwarding for 5173. The phone then loads http://localhost:5173, which is a secure context, no certificate needed.

The iOS limits to plan around

  • No install prompt. Users must use the Share menu.
  • Web Push works only for installed apps (iOS 16.4+), and permission must be requested from a tap.
  • Storage is separate. The home screen app doesn’t share cookies or storage with Safari, so a user logged in in Safari is logged out in the app.
  • Storage can be evicted. Safari deletes script-written storage for sites not used in 7 days; WebKit says home screen apps have their own counter and it does not expect their first party data to be removed, but storage remains best effort. Call navigator.storage.persist() and keep the server as the source of truth.
  • To start clean, delete the icon, then clear the site in Settings › Apps › Safari › Advanced › Website Data.

A faster loop while you code

Reinstalling on the phone after every CSS change is slow. Breakpane shows your localhost in App mode, the way it looks installed, inside iPhones and Androids with each model’s safe areas and status bar, floating next to your editor on Windows or macOS. Its PWA diagnosis flags what’s missing in the manifest, and airplane mode tests your offline path. It renders with Chromium, not WebKit, so the push, storage and Safari specifics above still need the real iPhone pass before you ship.

Other guides

  1. iPhoneiPhone Air · 420 × 912
    iPhone Air running a web app in dark mode, with the width dimension line
    iPhone simulator for Windows? How to test a website on iPhone without a Mac
  2. SafariiPhone 17 Pro · 402 × 874
    iPhone 17 Pro in Safari, with dimension lines on the top and bottom bars
    Safari 100vh bug: the address bar, dvh, svh and lvh (iOS 26)
  3. Safe areaiPhone 17 Pro · 402 × 874
    iPhone 17 Pro running a full-screen web app, with safe area dimension lines
    env(safe-area-inset-bottom) not working? Every cause and fix