PWA
A progressive web app, or PWA, is a website that uses a service worker and a web app manifest so it can be installed, work offline and behave like an app. There is no PWA SDK: what an installed web app can do depends on the browser and the platform, and the gap between Chrome on Android and Safari on an iPhone is what most product decisions turn on. The page opens with sixteen capabilities and where each one works in October 2026, then covers the service worker and the update that never arrives, caching strategies and storage that can be evicted, installation on four platforms, what Apple permits and how that changed, the store question, five documented cases, and where it degrades.
- What it is
- A service worker, a manifest and HTTPS
- Biggest gap
- iPhone: push only once installed; no background sync, Bluetooth or install prompt
- Distribution
- A URL; a fix ships with the next deploy
What a web app can reach in 2026 — sixteen capabilities, platform by platform
A PWA is not one technology with one level of support. It is a set of browser features, and each browser implements a different subset. So the question a team actually has — “can we build this as a web app?” — has no single answer. It has an answer per feature and per platform. On an iPhone the answer also changes once the user adds the site to the Home Screen, because some features exist only for installed web apps. Every browser on iOS uses Apple’s WebKitSafari’s browser engineThe open-source browser engine behind Safari. A page that works in Chrome can still break in WebKit, which is why test suites run their browser journeys in it too — Playwright ships its own WebKit build for exactly that.All terms engine, so an iPhone user cannot get around this by installing Chrome. Here are sixteen things an app commonly needs, where each one works as of October 2026, and what to do where it does not. Later sections cite the rows by number.
| # | What the app needs | Android · Chrome | iPhone · Safari, added to Home Screen | Desktop | If it is missing |
|---|---|---|---|---|---|
| 1 | Work offline (a service worker and the Cache API) | Yes | Yes, since iOS 11.3 (2018) | Every current browser | This is the baseline — design the offline state (section 3) |
| 2 | Keep data on the device (IndexedDB, files in OPFS) | Yes, up to 60% of the disk | Yes — but in a Safari tab it is deleted after seven days of use without a visit; installed apps are exempt | Yes; Firefox gives a site at most 10 GiB unless its storage is made persistent | Keep the source of truth on a server |
| 3 | Open in its own window, without browser UI | Yes, once installed | Yes; since iOS 26 every site added to the Home Screen opens as a web app | Chrome and Edge install it; Safari 17 adds it to the Dock; Firefox 143 pins it to the Windows taskbar | A bookmark still works |
| 4 | Camera, microphone, location | Yes, after a permission prompt | Yes, after a permission prompt | Yes | — |
| 5 | Take payments | Payment Request, Google Pay | Apple Pay and Payment Request | Yes | An ordinary checkout form |
| 6 | The system share sheet (navigator.share) | Yes | Yes | Chrome, Edge and Safari; Firefox only on Android | A “copy link” button |
| 7 | Keep the screen awake (Wake Lock) | Yes | Yes, since iOS 18.4 | Yes; Firefox since 126 | — |
| 8 | Push notifications | Yes, in the browser or installed | Only once added to the Home Screen (iOS 16.4, March 2023), and the prompt must follow a tap | Yes; Safari on the Mac since 16 | Email or SMS for anything critical |
| 9 | A badge on the app icon (setAppBadge) | No — Android shows notification dots instead | Yes, once installed (iOS 16.4) | Chrome and Edge once installed; Safari 17 | A count inside the app |
| 10 | Offer installation from the page (beforeinstallprompt) | Yes | No — the user must find Share → Add to Home Screen | Chrome and Edge; not Safari or Firefox | An instructions screen for each platform |
| 11 | Storage that is never evicted (navigator.storage.persist()) | Granted silently, by how much the user engages | Granted silently, for example to installed apps | Firefox asks the user | Sync to a server (row 2) |
| 12 | Send queued writes when the connection returns, even with the app closed (Background Sync) | Yes | No | Chrome and Edge only | Queue writes in IndexedDB and replay them when the app is next open |
| 13 | Refresh content in the background on a schedule (Periodic Background Sync) | Installed apps only; the browser picks the interval | No | Chrome and Edge only | Refresh on open; a silent push cannot replace it, because every push must show a notification |
| 14 | Large downloads that survive closing the app (Background Fetch) | Yes | No | Chrome and Edge only | Download while the app is open, in resumable parts |
| 15 | Bluetooth, USB, NFC and serial devices | Bluetooth, USB, NFC and, since Chrome 148, serial | None of them | Bluetooth, USB, serial and HID in Chrome and Edge; serial in Firefox 151 behind an add-on | A native or companion app |
| 16 | Receive shared items and open files from the system (Share Target, File Handling) | Share Target, yes | No | Share Target and File Handling in Chrome and Edge | An upload button |
The service worker: a proxy the page cannot see around
Everything a PWA can do that an ordinary site cannot starts with one mechanism. A service worker is a script the browser runs separately from the page, with no access to the page itself. Once registered, it receives a fetch event for every request the pages in its scope make — the HTML, the scripts, the images, the APIApplication Programming Interface — contract between programsThe rules one program follows to call another: which requests it may send, in what shape, and what comes back. To the caller it is a contract — everything behind it stays a black box, which is exactly what lets the other side be rewritten without breaking you.Full article →All terms calls. For each one it decides what to answer: something from a cache, something from the network, or something it builds itself. The page cannot tell which. The scope is the folder the worker’s file sits in, so a worker at /sw.js controls the whole site and one at /app/sw.js controls only /app/. Service workers run only over HTTPS, apart from localhost during development.
The worker has a lifecycle, and most production problems come from it. The browser checks for a new version on every navigation to a page in scope, and on events such as a push if it has not checked in the last 24 hours. Since Chrome 68, and in other current browsers, that check ignores HTTPHyperText Transfer Protocol — the protocol of the webThe protocol browsers and servers talk in: the client sends a method and an address, the server answers with a status code and a body. It keeps no memory between requests — every request stands alone, which is why anything that must be remembered is carried explicitly.All terms caching headers for the worker file itself, though not for the scripts it imports. If even one byte differs, the browser downloads and installs the new worker. Then the new worker waits. By default it takes over only when no tab is controlled by the old one any more. Until then the old worker keeps answering, from the old caches.
Plan the way out before shipping the first worker. If a broken worker reaches users, the fix is to deploy a worker that does nothing — no fetch handler, skipWaiting() on install — at exactly the same URL. Requests then pass through as if no worker existed. Rename the file and the broken worker stays registered next to the new one, still answering. The Clear-Site-Data: "storage" response header unregisters workers too, but it wipes all of the site’s storage and is not supported everywhere, so it is a backup, not a plan. Finally, write the worker with a library rather than by hand. Workbox (7.4), Serwist, vite-plugin-pwa and Angular’s service worker generate the list of files to precache from the build output, so the list and its version hashes stay correct as the bundle changes.
Caching and storage: how stale may an answer be, and how long will it stay?
A caching strategy is chosen per kind of request, not once for the site, and the choice comes down to one question: how stale may this answer be? A JavaScript bundle whose file name contains a hash of its content is never stale — a new version gets a new name — so it can come from the cache forever. An account balance must never be stale, so it must come from the network or not at all. Most requests sit in between. Three strategies cover most of them, and they differ in who answers first.
| Request | Strategy | What the user sees offline | The trap |
|---|---|---|---|
| Hashed scripts, styles, fonts | Precache at install; cache first | The app shell, instantly | Caching an unhashed index.html cache-first, which pins every user to one release |
| Page navigations (HTML) | Network first, with a timeout of a few seconds, then the cached page or an offline page | The last version of the page, or a page that says it is offline | No timeout: on a weak signal the user stares at a blank screen instead of the cached page |
| API reads | Network first; stale-while-revalidate where yesterday’s data is fine | The last data, marked with when it was fetched | Old data shown as current, with no timestamp |
| API writes | Network only; when offline, queue in IndexedDB and replay | The change, marked as pending | A replay that runs twice and charges twice — send an idempotency key with every write |
| Images and fonts from another origin without CORS | Stale-while-revalidate, with a size limit — or not at all | The cached copy | An opaque response hides its status, so an error can be cached forever, and Chrome counts each one as at least 7 MB of quota |
| Sign-in, payments, personal data | Network only | A clear error | Personal responses left in the cache on a shared device |
Writes need the most care, because an offline write is a promise. The common pattern is to save the change locally, show it as pending, and send it when the connection returns. On Chrome and Edge, Background Sync can send it even after the app is closed (row 12). Everywhere else the replay happens the next time the app is open. Either way the server may receive the same write twice — once from the attempt that timed out, once from the replay — so every write should carry a key that lets the server treat a repeat as the same request. An operation that gives the same result however many times it runs is called idempotentsafe to repeatAn operation that has the same effect whether it runs once or five times. It is what makes a retry safe — and since any network call may be retried without you knowing, the alternative is charging a card twice.All terms, and offline writes are only safe when they are.
What the browser may delete
| Browser | How much one site may store | When it is deleted | `persist()` |
|---|---|---|---|
| Chrome, Edge | Up to 60% of the disk | Least recently used sites first, when the disk runs low | Granted or refused silently, by engagement |
| Firefox | 10% of the disk or 10 GiB per site group; 50% once persistent | Least recently used first, unless persistent | Asks the user |
| Safari, in a tab | About 60% of the disk | Everything a script wrote, after seven days of Safari use without a visit; and under storage pressure | Granted or refused silently |
| Safari, Home Screen app | About 60% of the disk | Not by the seven-day rule; still under storage pressure | More likely to be granted |
Offline is a state the interface has to have, not a technical detail. Show that the app is offline. Mark cached data with the time it was fetched. List changes that are waiting to be sent, and say what happened when one failed. Decide in advance what happens when the same record was changed on two devices while both were offline — the last write wins, the server merges, or the user chooses. An app that silently shows week-old data as if it were current is worse than one that admits it is offline.
Installation: what “install” means on four platforms
The second half of a PWA is the web app manifest — a small JSONJavaScript Object Notation — text format for dataA text format for passing data around: key–value pairs, lists, numbers and strings. It came out of JavaScript, but every language reads it now, and that is why most APIs answer in it rather than in anything more capable.All terms file, linked from every page, that gives the app a name, icons, a start URL and a display mode. Set display to standalone and the installed app opens without an address bar or tabs, which is most of what makes it feel like an app rather than a bookmark. Chrome offers installation when a site is served over HTTPS and the manifest has a name or short_name, icons of 192 and 512 pixels, a start_url and a display value, once the user has clicked on the page and spent about 30 seconds on it. Since Chrome 108 on Android and 112 on desktop, installing from the browser menu no longer needs a service worker at all; the install prompt still looks for one.
“Install” produces four different things. On Android, Chrome mints a WebAPK — a real Android package, generated from the manifest — so the app appears in the launcher and in system settings and opens links within its scope instead of the browser. On an iPhone the user adds the site from the Share menu. Since iOS 26, in September 2025, every site added that way opens as a web app by default, with or without a manifest. An iPhone web app keeps its own and storage, separate from Safari: a user signed in on the website opens the new icon signed out, so the first screen after installing should be a sign-in that does not feel like a bug. Chrome and Edge on the desktop install the site into its own window. Safari on the Mac, since version 17, adds it to the Dock. Firefox 143 started pinning sites to the Windows taskbar in September 2025.
Only Chromium browsers let the page itself offer installation (row 10). They fire a beforeinstallprompt event, which a page can hold and replay from its own “Install” button at a sensible moment. Everywhere else, installation is a menu item the user has to find, so a product that needs installed users writes short instructions per platform and shows them when the user is most likely to say yes — after a second visit, not on the first screen. A standard way for a page to ask is on the way: on 21 September 2026 Chrome announced its intent to ship the Web Install API, navigator.install() and an <install> element, in Chrome 156, desktop first. WebKit has said it is opposed, on the grounds that installation should start in the browser’s own interface, so do not plan an iPhone feature around it.
| Manifest member | What it does | Who reads it |
|---|---|---|
| name, short_name | The app’s name, and a shorter one for the icon label | Every browser that installs |
| icons | Icons in several sizes; "purpose": "maskable" for icons the system may crop into its own shape | Chromium; Safari only when there is no apple-touch-icon |
| start_url, scope | Where the app opens, and which URLs count as inside it | Every browser that installs |
| display | standalone for an app window; minimal-ui keeps a few browser controls | Every browser that installs |
| id | A stable identity, so changing start_url later does not create a second app | Chromium; Safari since 16.4 on iOS |
| shortcuts | Actions on a long press of the icon | Chromium; Safari 17.4 on the Mac |
| share_target | Appear in the system share sheet as a target (row 16) | Chromium |
| file_handlers, protocol_handlers | Open files of a type, or links of a scheme, in the app (row 16) | Chromium on the desktop |
The iPhone: what Apple permits, and how it got here
Rows 8–11 and the empty iPhone cells in rows 12–16 all have one cause. On iOS every browser — Safari, Chrome, Firefox, Edge — has been required to use Apple’s WebKit engine, so what WebKit implements is what the web can do on an iPhone. The rule has started to loosen: since iOS 17.4 browsers in the EU may use their own engines, and since iOS 26.2 in December 2025 so may browsers in Japan. But by Apple’s own statement in 2024, Home Screen web apps are built directly on WebKit, and as of this writing no major browser ships its own engine on the iPhone. For planning, assume WebKit. The history explains why teams keep being surprised by it:
| When | iOS | What changed for web apps |
|---|---|---|
| June 2007 | iPhone OS 1 | Apple presented web apps in Safari as the way to build for the iPhone; a native SDK followed in March 2008 |
| March 2018 | 11.3 | Service workers and the manifest — offline web apps on the iPhone for the first time (row 1) |
| March 2020 | 13.4 | Safari deletes all script-written storage after seven days of use without a visit; Home Screen apps exempt (row 2) |
| March 2023 | 16.4 | Web Push and badges for Home Screen apps; other browsers may add sites to the Home Screen (rows 8–9) |
| Feb–Mar 2024 | 17.4 | Betas turned Home Screen apps in the EU into Safari shortcuts; reversed on 1 March |
| March 2025 | 18.4 | Declarative Web Push, which shows a notification without running any JavaScript; Wake Lock in Home Screen apps (row 7) |
| September 2025 | 26 | Every site added to the Home Screen opens as a web app by default; no manifest required (row 3) |
| December 2025 | 26.2 | Other browser engines allowed in Japan, as in the EU since 17.4 |
Push on an iPhone has three rules beyond “installed only” (row 8). The permission prompt may appear only in response to a direct action, such as a tap on a “Turn on notifications” button — never on page load. Every push must show a notification the user can see: WebKit revokes the subscription of a site whose worker receives a push and shows nothing, so the web has no silent push for background refreshes (row 13). And since iOS 18.4, a declarative push lets the server describe the notification itself, so it appears even if the worker never runs. Badges follow the same pattern (row 9): an installed app can set a count on its icon, which the system shows with the app’s other notifications.
PWA, wrapper or app: the store question
The advantage of a PWA that survives every comparison is distribution. There is no review queue, no store account, no certificate to renew and no build to sign. A fix ships with the next deploy, and a link is the whole installation flow. For a product people find through search or receive as a link, that is the difference between trying it now and being asked to install something first. What a URL does not give you is a listing in the store where people browse, the store’s payment system, and the trust some users place only in an app from a store. There are four ways to get some of that back, and each has a catch:
| Path | What ships | Updates | The catch |
|---|---|---|---|
| PWA only | The website | Every deploy | No store listing; on the iPhone, users must be taught to install (row 10) |
| Google Play, as a Trusted Web Activity | A thin Android app that opens your site full screen in Chrome (Bubblewrap builds it) | Web changes with every deploy; the shell rarely | Digital Asset Links must prove you own the site, or a browser address bar appears; Play’s policies apply, including its billing rules for digital goods |
| Microsoft Store | The PWA, packaged with PWABuilder | Every deploy | Reaches Windows only |
| Web code in a native shell (Capacitor, Cordova) | Your web code bundled into an app that renders it in a WebView, with plugins for native features | Through store review; web code can change over the air within the stores’ rules | Apple rejects an app that is only “a repackaged website” (guideline 4.2); you now maintain an app as well |
Apple’s guideline 4.2 is the line that decides the last row: an app should include features, content and interface that “elevate it beyond a repackaged website”, and one that is not useful, unique or app-like does not belong in the store. A shell around your website with nothing native in it is exactly what that rule rejects. On Android the opposite is true: a Trusted Web Activity is a supported way to put your website in Google Play, as long as Digital Asset Links on your site prove that the app and the site belong to the same developer. The decision itself follows from the capability table more than from taste:
| If the product… | Build |
|---|---|
| Needs only rows 1–7 and is found through search or links — content, commerce, booking, dashboards, internal tools | A PWA |
| Needs rows 8–11 on iPhones — push, badges | A PWA with a designed install step, or an app if users will not install |
| Needs a Google Play listing but nothing beyond the web | A PWA plus a Trusted Web Activity |
| Needs rows 12–16 on iPhones, or work while the app is closed | A native or cross-platform app — see the React Native, Flutter, iOS and Android pages |
| Is a game, a camera or media app, or anything where every frame counts | An app |
Five cases, and what each one was missing
Three of the five cases are about the platform under the web app — a browser vendor or an operating system changing a rule — and two are about the team’s own choices. Each is documented by the people involved.
| Case | What happened | What was missing |
|---|---|---|
| Create React App, 2017–2018 | The most popular React starter made every new app offline-first by default; a year and a half later it made the service worker opt-in | An update flow — a service worker is a deployment decision, not a template default |
| Safari’s seven-day cap, March 2020 | Safari began deleting everything a site’s scripts stored after seven days of use without a visit | A server holding the real copy; drafts and offline data kept only in the browser were lost |
| Pinterest, 2017–2018 | After going all-in on native apps, it rebuilt its mobile website as a PWA in three months; signups rose 843% in a year | Before: a fast mobile web for people arriving from a link |
| Apple and the EU, Feb–Mar 2024 | iOS 17.4 betas turned EU Home Screen apps into Safari shortcuts; Apple reversed it on 1 March | A plan for the platform switching off the install mode, and with it push and badges |
| Notification prompts, 2020–2025 | Chrome quietened prompts for sites with low acceptance, then began removing unused permissions — installed web apps exempt | Asking at the moment the user wants something, not on the first visit |
The first case shows how the update problem spreads. Create React App 1.0, released on 18 May 2017, made every newly generated project a progressive web app by default: a service worker cached the whole build and served it cache-first. Ten days later an issue titled “Why was service worker merged into create react app?” opened, and over 154 comments developers described the same thing. They deployed, and their users kept seeing the old site. Version 2.0, on 1 October 2018, made the service worker opt-in. Its documentation gave the reason plainly: offline-first apps can make debugging deployments harder, and users see older content until they close every open tab — “reloading is not enough”. Nothing in the worker was broken. What was missing was the update flow from the section above, and a template cannot know when your users should get a new version.
The other three cases are about rules the team does not own. In March 2020 Safari began deleting all of a site’s script-written storage — IndexedDB, localStorage, service worker registrations and caches — after seven days of Safari use without a visit. It was aimed at trackers, and Home Screen apps were exempt, but any web app that kept a user’s only copy of something in the browser lost it. In February 2024 the iOS 17.4 betas turned Home Screen web apps in the EU into shortcuts that opened in a Safari tab, without their own window, push or badges; Apple cited the EU’s Digital Markets Act. After an outcry from developers, and with the European Commission taking notice, Apple reversed the change on 1 March. And browsers have been closing the door on careless push: Chrome 80 in 2020 quietened the prompt for sites whose prompts users usually reject, and from October 2025 Chrome removes notification permission from sites the user no longer visits, noting that less than 1% of notifications get any interaction. Installed web apps are exempt from the second rule. All three say the same thing: a web app runs on rules set by someone else, so it keeps its data on its own server, keeps a second channel for anything critical, and earns each permission at the moment it is needed.
Where it degrades
A PWA fails in ways that are invisible from the developer’s machine, where the network is fast, the browser is Chrome and the tab is always fresh. Almost every failure is one of these:
- No update flow, so a shipped fix waits behind an old worker for as long as a tab or Home Screen app stays open.
- A renamed service worker file, which leaves the broken one registered and answering.
- An unhashed
index.htmlserved cache-first, pinning users to one release. - API data shown from the cache as if it were current, with no time on it.
- Offline writes replayed without an idempotency keya request id that makes a retry safeA unique value a client sends with a request that changes something — a payment, an order — so the server can recognise a repeat and return the first result instead of acting twice. The server has to store the keys and decide how long to keep them; a retry that arrives after its key has expired is treated as new.Full article →All terms, so one order becomes two.
- The only copy of user data kept in browser storage, which the browser is entitled to delete (row 2).
- Cached opaque responses, which hide errors and eat quota seven megabytes at a time.
- A notification prompt on the first visit, rejected once and quietened for good.
- A feature built on rows 8–16 and discovered missing on the iPhone after the design was approved.
- A store wrapper with nothing native in it, rejected under guideline 4.2.
The honest limit is that a PWA lives inside a browser, and the browser belongs to someone else. On Android and the desktop, Chromium gives a web app nearly everything an app gets, and the gap keeps closing. On an iPhone the web gets what Apple decides it gets, the decision has moved in both directions within a single beta cycle, and the engine underneath is not yours to choose. That is a reason to build a PWA with its eyes open, not a reason to avoid one. For most products that live on being found and shared, a URL that works at once beats an app that has to be installed first — as long as nothing essential depends on a capability one platform does not grant.
The practical check takes an hour. List every feature the product needs and find its row in the table at the top. Open the site on an iPhone, add it to the Home Screen, and check that sign-in, push and offline work there. Deploy a visible change with a tab left open and see when it arrives. Find the file name of the service worker and confirm that a do-nothing replacement could be deployed at that URL today. Search the caching rules for anything personal or anything opaque. Turn the network off, make a change, turn it back on twice, and count how many times the server received it.
When to use it
Use it when
- Products that are fundamentally a website and benefit from working offline and living on the home screen.
- Anything found through search or shared as a link, where an install step costs you users.
- Internal tools and field apps on devices you control, where a store review is pure overhead.
- One codebase across desktop and mobile, built by a team that cannot staff two native platforms.
Avoid it when
- Anything that needs Bluetooth, NFC, USB or serial devices on iPhones (row 15).
- Products that depend on work happening while the app is closed (rows 12–14).
- Products whose users find apps by browsing a store, where a URL reaches nobody.
- Games, camera and media apps, and anything else where the browser is the wrong runtime for every frame.
Sources and method
What this page stands on
- 17 sources
- 1 documented failure
- Checked October 13, 2026
Practices and where the terms come from
the capability table, storage, the service worker and installing- Documentationmdn/browser-compat-data (opens in a new tab)GitHub · MDN
- DocumentationStorage quotas and eviction criteria (opens in a new tab)MDN Web Docs
- GuideThe service worker lifecycle (opens in a new tab)web.dev
- GuideRemoving buggy service workers (opens in a new tab)Chrome for Developers
- GuideWhat does it take to be installable? (opens in a new tab)web.dev
- ArticleWeb Push for Web Apps on iOS and iPadOS (opens in a new tab)WebKit · Feb 16, 2023
- ArticleMeet Declarative Web Push (opens in a new tab)WebKit · Mar 27, 2025
- ArticleWebKit Features in Safari 26.0 (opens in a new tab)WebKit · Sep 15, 2025
- DocumentationApp Review Guidelines (opens in a new tab)Apple Developer
The failures this page learns from
Create React App’s default, in “Five cases, and what each one was missing”Safari, Pinterest, the EU and notification prompts
the other four cases- ArticleFull Third-Party Cookie Blocking and More (opens in a new tab)WebKit · Mar 24, 2020
- ArticleZack Argyle — A one year PWA retrospective (opens in a new tab)Pinterest Engineering · Jul 20, 2018
- ArticleiOS 17.4 won’t remove Home Screen web apps in the EU after all (opens in a new tab)9to5Mac · Mar 1, 2024
- ArticleIntroducing quieter permission UI for notifications (opens in a new tab)Chromium Blog · Jan 7, 2020
- ArticleGoogle Chrome silences those pesky notifications (opens in a new tab)TechCrunch · Oct 10, 2025
Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.
Found this useful?
Share it with someone who is working on the same problem.