DevelopmentAll levels

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 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 needsAndroid · ChromeiPhone · Safari, added to Home ScreenDesktopIf it is missing
1Work offline (a service worker and the Cache API)YesYes, since iOS 11.3 (2018)Every current browserThis is the baseline — design the offline state (section 3)
2Keep data on the device (IndexedDB, files in OPFS)Yes, up to 60% of the diskYes — but in a Safari tab it is deleted after seven days of use without a visit; installed apps are exemptYes; Firefox gives a site at most 10 GiB unless its storage is made persistentKeep the source of truth on a server
3Open in its own window, without browser UIYes, once installedYes; since iOS 26 every site added to the Home Screen opens as a web appChrome and Edge install it; Safari 17 adds it to the Dock; Firefox 143 pins it to the Windows taskbarA bookmark still works
4Camera, microphone, locationYes, after a permission promptYes, after a permission promptYes—
5Take paymentsPayment Request, Google PayApple Pay and Payment RequestYesAn ordinary checkout form
6The system share sheet (navigator.share)YesYesChrome, Edge and Safari; Firefox only on AndroidA “copy link” button
7Keep the screen awake (Wake Lock)YesYes, since iOS 18.4Yes; Firefox since 126—
8Push notificationsYes, in the browser or installedOnly once added to the Home Screen (iOS 16.4, March 2023), and the prompt must follow a tapYes; Safari on the Mac since 16Email or SMS for anything critical
9A badge on the app icon (setAppBadge)No — Android shows notification dots insteadYes, once installed (iOS 16.4)Chrome and Edge once installed; Safari 17A count inside the app
10Offer installation from the page (beforeinstallprompt)YesNo — the user must find Share → Add to Home ScreenChrome and Edge; not Safari or FirefoxAn instructions screen for each platform
11Storage that is never evicted (navigator.storage.persist())Granted silently, by how much the user engagesGranted silently, for example to installed appsFirefox asks the userSync to a server (row 2)
12Send queued writes when the connection returns, even with the app closed (Background Sync)YesNoChrome and Edge onlyQueue writes in IndexedDB and replay them when the app is next open
13Refresh content in the background on a schedule (Periodic Background Sync)Installed apps only; the browser picks the intervalNoChrome and Edge onlyRefresh on open; a silent push cannot replace it, because every push must show a notification
14Large downloads that survive closing the app (Background Fetch)YesNoChrome and Edge onlyDownload while the app is open, in resumable parts
15Bluetooth, USB, NFC and serial devicesBluetooth, USB, NFC and, since Chrome 148, serialNone of themBluetooth, USB, serial and HID in Chrome and Edge; serial in Firefox 151 behind an add-onA native or companion app
16Receive shared items and open files from the system (Share Target, File Handling)Share Target, yesNoShare Target and File Handling in Chrome and EdgeAn 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 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.

A service worker is a script the browser keeps outside the page, and every request the page makes passes through it. Per request, it decides whether to answer from a cache or go to the network, which is what makes offline use, instant repeat loads and background sync possible at all. The same design creates the update problem: a new worker installs but then waits until every tab using the old one is closed, so a shipped fix does not reach users on the next load unless the page handles the update deliberately.

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

A browser installs a new service worker as soon as it notices the change, but by default the new worker waits until no tab is controlled by the old one. Reloading does not end that wait: during a navigation the new page is created before the old one goes away, so the old worker never loses its last client, and a user who keeps one tab open — or a home-screen app that is never fully closed — can stay on the old version for days. The fix is to handle the update deliberately: detect the waiting worker, tell the user an update is ready, and on their tap call skipWaiting and reload, so the new version takes over at a moment the user chose.

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.

A service worker answers every request with a strategy chosen for that kind of request, and the choice comes down to how stale the answer is allowed to be. Cache first answers instantly and offline, and is correct for files whose name changes whenever their content does, such as hashed bundles. Network first shows current data whenever there is a connection and falls back to the last copy when there is not, which suits API data and pages. Stale-while-revalidate answers from the cache at once and refreshes it in the background, so the user sees yesterday’s copy now and today’s next time — right for avatars and feeds, wrong for a balance or a price.
RequestStrategyWhat the user sees offlineThe trap
Hashed scripts, styles, fontsPrecache at install; cache firstThe app shell, instantlyCaching 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 pageThe last version of the page, or a page that says it is offlineNo timeout: on a weak signal the user stares at a blank screen instead of the cached page
API readsNetwork first; stale-while-revalidate where yesterday’s data is fineThe last data, marked with when it was fetchedOld data shown as current, with no timestamp
API writesNetwork only; when offline, queue in IndexedDB and replayThe change, marked as pendingA replay that runs twice and charges twice — send an idempotency key with every write
Images and fonts from another origin without CORSStale-while-revalidate, with a size limit — or not at allThe cached copyAn 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 dataNetwork onlyA clear errorPersonal 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 , and offline writes are only safe when they are.

What the browser may delete

BrowserHow much one site may storeWhen it is deleted`persist()`
Chrome, EdgeUp to 60% of the diskLeast recently used sites first, when the disk runs lowGranted or refused silently, by engagement
Firefox10% of the disk or 10 GiB per site group; 50% once persistentLeast recently used first, unless persistentAsks the user
Safari, in a tabAbout 60% of the diskEverything a script wrote, after seven days of Safari use without a visit; and under storage pressureGranted or refused silently
Safari, Home Screen appAbout 60% of the diskNot by the seven-day rule; still under storage pressureMore 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 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” names four different results. On Android, Chrome mints a WebAPK — a real Android package that appears in the launcher and in app settings and can open links in its scope. On an iPhone the user adds the site from the Share menu, and since iOS 26 every site added this way opens as a web app; it keeps its own cookies and storage, separate from Safari, so a user signed in on the website opens the icon signed out. Desktop Chrome and Edge install a site into its own window and can register it for files and links. Safari on a Mac adds it to the Dock. Only Chromium browsers let a page offer installation itself, so every other platform needs written instructions.

“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 memberWhat it doesWho reads it
name, short_nameThe app’s name, and a shorter one for the icon labelEvery browser that installs
iconsIcons in several sizes; "purpose": "maskable" for icons the system may crop into its own shapeChromium; Safari only when there is no apple-touch-icon
start_url, scopeWhere the app opens, and which URLs count as inside itEvery browser that installs
displaystandalone for an app window; minimal-ui keeps a few browser controlsEvery browser that installs
idA stable identity, so changing start_url later does not create a second appChromium; Safari since 16.4 on iOS
shortcutsActions on a long press of the iconChromium; Safari 17.4 on the Mac
share_targetAppear in the system share sheet as a target (row 16)Chromium
file_handlers, protocol_handlersOpen 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:

WheniOSWhat changed for web apps
June 2007iPhone OS 1Apple presented web apps in Safari as the way to build for the iPhone; a native SDK followed in March 2008
March 201811.3Service workers and the manifest — offline web apps on the iPhone for the first time (row 1)
March 202013.4Safari deletes all script-written storage after seven days of use without a visit; Home Screen apps exempt (row 2)
March 202316.4Web Push and badges for Home Screen apps; other browsers may add sites to the Home Screen (rows 8–9)
Feb–Mar 202417.4Betas turned Home Screen apps in the EU into Safari shortcuts; reversed on 1 March
March 202518.4Declarative Web Push, which shows a notification without running any JavaScript; Wake Lock in Home Screen apps (row 7)
September 202526Every site added to the Home Screen opens as a web app by default; no manifest required (row 3)
December 202526.2Other 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:

PathWhat shipsUpdatesThe catch
PWA onlyThe websiteEvery deployNo store listing; on the iPhone, users must be taught to install (row 10)
Google Play, as a Trusted Web ActivityA thin Android app that opens your site full screen in Chrome (Bubblewrap builds it)Web changes with every deploy; the shell rarelyDigital 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 StoreThe PWA, packaged with PWABuilderEvery deployReaches 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 featuresThrough store review; web code can change over the air within the stores’ rulesApple 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 toolsA PWA
Needs rows 8–11 on iPhones — push, badgesA PWA with a designed install step, or an app if users will not install
Needs a Google Play listing but nothing beyond the webA PWA plus a Trusted Web Activity
Needs rows 12–16 on iPhones, or work while the app is closedA 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 countsAn 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.

CaseWhat happenedWhat was missing
Create React App, 2017–2018The most popular React starter made every new app offline-first by default; a year and a half later it made the service worker opt-inAn update flow — a service worker is a deployment decision, not a template default
Safari’s seven-day cap, March 2020Safari began deleting everything a site’s scripts stored after seven days of use without a visitA server holding the real copy; drafts and offline data kept only in the browser were lost
Pinterest, 2017–2018After going all-in on native apps, it rebuilt its mobile website as a PWA in three months; signups rose 843% in a yearBefore: a fast mobile web for people arriving from a link
Apple and the EU, Feb–Mar 2024iOS 17.4 betas turned EU Home Screen apps into Safari shortcuts; Apple reversed it on 1 MarchA plan for the platform switching off the install mode, and with it push and badges
Notification prompts, 2020–2025Chrome quietened prompts for sites with low acceptance, then began removing unused permissions — installed web apps exemptAsking 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.html served 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 , 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

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.