Make it installable

Your app can be installed from the browser to a phone's home screen or a computer, and open like an app. It takes one switch, a name and one icon.

What people see, the update bar and the install numbers are in Installable app (PWA). This page is the setup.

Turn it on

  1. Switch on the feature in src/custom/features.ts. The PWA is the one feature that is off until you ask for it:

    export const CUSTOM_FEATURES: FeatureChoice = {
        on: ['pwa'],
    };
  2. Name it in src/custom/pwa.ts. Anything you leave out keeps Arc CMS's default; every key and default is in Installable app (PWA):

    export const CUSTOM_PWA: Partial<PwaConfig> = {
        name: 'Sanskrit for Kids',      // install dialog and app switcher
        shortName: 'Sanskrit',          // under the home screen icon, about 12 characters
        themeColor: '#e8590c',          // browser bar and phone status bar
        startUrl: '/learn',             // the page the home screen icon opens
    };
  3. Add one icon: a square image of at least 512 by 512 pixels, saved as src/custom/pwa-icon.svg or src/custom/pwa-icon.png. Keep the important part inside the middle 80%, since Android crops it into a circle or rounded square.

    An app built for several brands keeps one icon per brand and names the one to use in pwa.ts, with its path from the project folder:

    export const CUSTOM_PWA: Partial<PwaConfig> = {
        name: 'Acme Lessons',
        icon: 'src/custom/brands/acme/icon.svg',
    };
  4. Build and deploy the website. These settings are read at build time, so every change needs a new build and a hosting deploy. See Deploy.

Putting enabled in src/custom/pwa.ts stops the build with a message. The switch lives only in src/custom/features.ts.

What the build makes

FileFrom
The web app manifestYour name, short name, description, colors and start page, shown full screen (standalone) with the whole site as its scope
Every icon sizeYour one image: the standard app icon sizes, the maskable icon, and the 180 by 180 pixel iPhone icon
The service worker, sw.jsStores the app's main code, styles and fonts; pages always come from the network first, and the stored app opens when there is no connection
iPhone tagsThe home screen icon link and the short name as the iPhone app title

With the feature off, none of these are built, and a visitor who still has an old service worker gets it removed on their next visit.

Show the install button

Arc CMS shows the install card at the top of the member dashboard. To show it on a page of your own, such as your app's start page, add <arc-install-prompt /> (see Installable app (PWA)). It shows what fits the browser, and nothing once the app is installed.

On your published home page, give any button data-arc-install hidden: it appears when the browser offers installing and asks on a click (see Build your home page).

Build your own install card

<arc-install-prompt /> carries its own styles and icons, so it also works inside Shadow DOM. To draw your own sheet instead, in your app's look, use PwaService (src/app/core/pwa/pwa.service.ts). Its install side is supported and stable:

MemberWhat it does
showInstall()A signal: true when the card should show (the browser can install, the app is not installed, the PWA is on, and the person has not chosen Not now in the last 30 days).
installMode()A signal: how this browser installs, or null. 'prompt' (Android, desktop Chrome and Edge: call install()), 'ios-safari' (Safari on iPhone and iPad: Share, then Add to Home Screen), 'ios-browser' (Chrome, Edge, Firefox and others on iPhone and iPad, iOS 16.4 and later: the same steps from that browser's Share button, often in its address bar), 'ios-other' (a web view inside another app, or an older iOS: the page must be opened in Safari).
install()Opens the browser's own install dialog, for 'prompt'. Does nothing otherwise.
dismiss()Not now: showInstall() turns false on this device for 30 days, and the decline is counted.
shown()Call it when your card comes into view, so the install numbers count it (once per device).
installed()A signal: the app runs from the home screen or as an installed desktop app.
readonly pwa = inject(PwaService);
constructor() {
    effect(() => { if (this.pwa.showInstall()) this.pwa.shown(); });
}
// template: @if (pwa.showInstall()) { ... @switch (pwa.installMode()) { ... } }

Test that it installs

  1. On your computer: build, serve the build, and check Chrome's DevTools, Application, as described in Installable app (PWA). npm run dev has no service worker, so it cannot be used for this.

  2. On an Android phone: deploy to your Firebase Hosting site, open it in Chrome, and install from the install card or the browser menu. The icon, name and splash colors come from your settings.

  3. On an iPhone: open the same address in Safari and add it to the home screen (below).

  4. After a change: build and deploy again, then reopen the installed app. It offers the new version with an Update button instead of reloading by itself.

Show the update yourself

By default Arc CMS shows its own "A new version is ready" bar, and holds it back on full-screen pages until a normal page opens. An app that lives entirely on full-screen pages (a kiosk, a check-in screen) would never see it. Such an app shows the update where it suits, such as a lock screen, and Arc CMS stays out of the way.

  1. Tell Arc CMS the page owns the update. Add pwaUpdate: 'app' to the route's data. Every page below that route inherits it, and Arc CMS's own bar is not shown there at all. 'app' is the only value; any other is ignored.

    { path: 'lock', loadComponent: () => import('./lock.component').then((m) => m.default),
      data: { fullScreen: true, pwaUpdate: 'app' } }
  2. Use PwaService (src/app/core/pwa/pwa.service.ts) in that page:

    MemberWhat it does
    updateReady()A read-only signal, true while a new version is waiting. Always false when the pwa feature is off.
    checkForUpdate()Looks for a new version now, instead of waiting for the hourly check. It resolves once the browser has looked, and does nothing when the feature is off, offline, or before the service worker has registered.
    applyUpdate()Switches to the waiting version and reloads the page. It does nothing when no update is waiting.
import { Component, inject } from '@angular/core';
import { PwaService } from '../app/core/pwa/pwa.service'; // from src/custom/lock.component.ts

@Component({
    selector: 'app-lock',
    template: `
        @if (pwa.updateReady()) {
            <button type="button" (click)="pwa.applyUpdate()">Update now</button>
        }
    `,
})
export default class LockComponent {
    readonly pwa = inject(PwaService);

    constructor() {
        void this.pwa.checkForUpdate(); // the lock screen is a quiet moment to look
    }
}

Arc CMS never calls applyUpdate() itself, on any page, so a page is never reloaded in the middle of a task. Call it only when the person is at a safe point. On a page without the flag, nothing changes: the bar shows as before.

When another tab updates

The app may be open in more than one tab or window. When someone updates in one of them, the new version takes over every open tab. A normal page reloads into it, as before. A page with pwaUpdate: 'app' does not reload: updateReady() stays true (or turns true), and your applyUpdate() reloads it into the new version at the next safe point. Until then the page keeps running the old version and may not open a screen it has not opened yet (stale(), below), so offer the update soon.

Do not leave an update waiting for long. A deploy replaces the old version's code files on the server, so a page of the old version that has not opened a screen yet cannot load it until the update is applied, unless its code was stored ahead (below). Offer the update at the next quiet moment, such as the lock screen.

Open screens offline and after a deploy

The service worker stores the code every page needs when the app installs. The code of each screen is stored the first time it opens. So a screen never opened on this device cannot open offline, nor after a deploy has removed the old version's files from the server. An app that must open every screen, such as a kiosk used through the day, has them stored ahead in src/custom/pwa.ts:

export const CUSTOM_PWA: Partial<PwaConfig> = {
    routeCode: 'app',
};
routeCodeStored when the app installs or updatesSize, Arc CMS on its own
visited (the default)What every page needs; each screen's code when it first opensAbout 1.8 MB
appAlso every screen outside the admin area, with everything it uses. Chosen from the build's own code graph, so your pages are found by themselves; the admin's editors and settings are left out, except code public pages shareAbout 3.8 MB
allEvery screen, the admin's too, for an app whose admins also work offlineAbout 6.6 MB

The build prints what it stores ("PWA: 110 code files, 3.8 MB stored ...") and warns above 15 MB. Your own pages add to the sizes above. Data is a separate matter: to show data offline too, turn on the offline cache (see Keep working offline).

Start offline after an update

The service worker keeps a copy of each page opened, for offline starts. After an update, the copies made by the old version point at code files the update removed, so they would open blank. Arc CMS checks a stored page before it opens it: if a code file it starts with is no longer on the device, the new version's app opens instead, at the same address. Published pages, which use no app code, open from their stored copy as before. A screen whose code was never stored is the case below.

A page waits 4 seconds for the network before it uses a stored copy. On a network without internet, such as a kiosk on a closed Wi-Fi, every start waits that long. Lower it in src/custom/pwa.ts, from 1 to 30 seconds:

export const CUSTOM_PWA: Partial<PwaConfig> = {
    navigationTimeoutSeconds: 2,
};

When a screen's code is missing anyway

Without the PWA, or with a screen that was not stored, a tab left open across a deploy can still meet a missing code file. Arc CMS then loads the address the person asked for afresh, so the tap opens that screen in the new version. If that fails as well within 10 seconds, a short message with a Reload button shows instead of trying again. On a page with data: { pwaUpdate: 'app' }, Arc CMS never reloads: StaleCodeService (src/app/core/version/stale-code.service.ts) sets stale(), and your page decides when to call reload(), as it does for updates.

iPhone and iPad

Wrap it for the app stores

To publish in the App Store or Google Play, wrap the installable app with Capacitor: add Capacitor to the project, point its web directory at the browser files that npm run build writes, add the plugins your app needs (push notifications, camera and so on, chosen per app), and build the iOS and Android apps with Xcode and Android Studio. Follow Capacitor's own documentation for each step.