🟡 Live, but narrowly scoped. A Client's live site can be installed to a phone's home screen as a standalone, offline-tolerant app with an auto-branded icon — but only when that site is an imported full-page mock served as a folder-site. Themed website-builder sites are not installable today, even though the admin share card advertises "Install" for every live site. Web push is not part of this feature at all.
Audience: operator / marketer. This is the surface you hand a Client so their customers can "add the site to their home screen." If you build sites in the visual builder, read the scope flags below carefully — they tell you which sites this actually works on. For the crew-facing installable app, see Field app — that's a different PWA.
There is no separate app-store listing. The PWA is the site. Installing it just pins the site's URL to the home screen with a branded icon and a standalone (chrome-less) window. The "app URL" is the site URL — the {label}.gobuild.ca subdomain now, the custom domain once forwarded.
When PWA is on for an org, its live folder-site serves three extra per-host endpoints and injects the wiring that makes browsers offer "Install":
| Endpoint | Purpose | Code |
|---|---|---|
GET /manifest.webmanifest |
Web app manifest — name, display: standalone, theme/background colour, icon set |
app/routers/landing_portal.py:281 |
GET /sw.js |
Service worker — offline shell, scope: / |
app/routers/landing_portal.py:296 |
GET /appicon-192.png · GET /appicon-512.png |
Auto-branded icon, Pillow-generated on the fly | app/routers/landing_portal.py:304 |
All three are resolved by Host header, not by path: _pwa_ctx() maps the incoming host to a CustomPageFolder and reads the PWA config off org.settings["pwa"] (app/routers/landing_portal.py:273). If the org doesn't have pwa.on, every endpoint returns 404 — the site behaves as if the feature doesn't exist.
site_manifest() (app/routers/landing_portal.py:281) returns a standard application/manifest+json document:
name = the configured app name, falling back to the folder's site name / name / "Site"short_name = first 12 chars of the namestart_url / scope = /display = standalonetheme_color / background_color = the configured theme hex (default #111827)192, 512) declared "purpose": "any maskable"The worker is a single inlined constant, _SITE_SW (app/routers/landing_portal.py:262). It is deliberately tiny:
skipWaiting + clients.claim so it activates immediatelyfetch (GET only): network-first, and on a navigation request it caches the fetched / into gbsite-v1; on a network failure it serves the cached / (or the cached request) as an offline fallbackThat is the entire offline story: an offline shell for the home document. There is no push handler, no notificationclick, no background sync in this worker — see Web push below.
site_appicon() (app/routers/landing_portal.py:304) renders the icon at request time with Pillow:
#111827 on a bad hex)load_default font fallback if the TTF is missingCache-Control: public, max-age=86400So the icon is "auto-branded" in the sense of brand colour + monogram — not the Client's actual logo image.
The manifest link, theme-color, apple-touch-icon, and the SW registration are only added inside _render_custom(), and only on a very specific path (app/routers/landing_portal.py:376):
folder is not None), and<!doctype or <html> (an imported, signed-off mock served shell-free), andorg.settings["pwa"]["on"] must be true.Only then does the code splice the manifest/theme/icon into <head> and the SW-registration + Install-button script into <body> (app/routers/landing_portal.py:379-395).
Because the app URL is just the site URL, "distribution" means handing the Client a link and a QR code, plus surfacing an in-page Install prompt.
On a prospect's live card in the Showcase admin, an Install / share row shows (showcase/templates/admin_prospect.html, install row):
domain_status is live/secured, otherwise the {subdomain}.gobuild.ca URL — with a Copy buttonGET /admin/qr?data=… (showcase/main.py:697), which uses the qrcode lib (added to requirements.txt) and is admin-gatedThe team hands that QR/URL to the Client for flyers, cards, and email signatures.
⚠️ Honest scope — the card over-advertises. The Install/share row renders whenever a prospect is
live, unconditionally. It does not check whether the PWA plugin is on, and it does not check whether the live site is actually an installable folder-mock vs. a themed builder site. On a non-installable site the QR still resolves and "opens," but tapping Install/Add-to-Home-Screen produces a plain bookmark with a generic icon, not the branded standalone app the copy promises.
The injected <body> script (app/routers/landing_portal.py:386) listens for beforeinstallprompt, suppresses the default mini-infobar, and drops a floating "📲 Install app" pill at the bottom of the page. Tapping it calls the saved prompt() and removes the button; an appinstalled listener also removes it. This is Android/Chromium only — iOS Safari has no beforeinstallprompt, so iPhone users install manually via Share → Add to Home Screen (which is exactly what the hints say).
PWA is a Showcase plugin (showcase/main.py, PLUGINS), now marked live: True. Config and defaults:
showcase/templates/viewer.html, savePWA) — app name, theme colour, icon letter, all optional.admin_golive() (showcase/main.py:659) fills them: theme from _brand_color(variant) and monogram from the business name's first letter.
_brand_color() (showcase/main.py:52) scans the mock's first three pages' CSS for hex colours and picks the most-used saturated, mid-dark one (skips near-white/near-black/greys), defaulting to #1f2937._widget_script() injects a non-functional "Installable app · Add to Home Screen" badge with the monogram tile onto the served mock (showcase/main.py:213), the same way chat/estimator previews are injected. It's a preview, not a real install.controlplane/main.py:190 passes pwa_on/pwa_name/pwa_theme/pwa_monogram) → hub import_site() (app/routers/admin_api.py:601), which writes org.settings["pwa"].The manifest/SW/icon endpoints only fire for a host that resolves to a CustomPageFolder via resolve_folder_host() (app/routers/landing_portal.py:273), and the head/body injection only happens for a full-document mock rendered through _render_custom() with a folder context (app/routers/landing_portal.py:376).
A themed website-builder site renders through _render() → _render_core() into landing_public.html (app/routers/landing_portal.py:219, :133) and never touches the PWA injection. So even if org.settings["pwa"]["on"] is true, a builder-substrate site:
<head>/<body>, and{subdomain}.gobuild.ca host resolves via resolve_site_host (a LandingPage), which _pwa_ctx ignores — so /manifest.webmanifest, /sw.js, and /appicon-*.png all 404.Net: installability today is real only for imported full-page mock / folder-sites. The org.settings["pwa"] flag is org-wide, but the plumbing that consumes it is folder-mock-only.
The client-site service worker (_SITE_SW) does offline caching only; it has no push code. So installed Client sites cannot receive web push, full stop.
Separately, there is web-push plumbing elsewhere in the codebase — app/routers/push.py (subscribe/unsubscribe + VAPID key) targeting the staff/client portal worker at /portal/sw.js, a different surface. That module gates on push_configured (app/config.py:178), and VAPID keys do appear set in .env. But none of that is wired to the installable Client sites described on this page. Treat "push on a Client's installed site" as not built.
The icon is a generated monogram-on-colour tile, not an upload of the Client's real logo/wordmark. Good enough for a launcher glyph; not a substitute for a designed app icon.
| Piece | Status | Note |
|---|---|---|
| Per-host manifest / SW / icon endpoints | 🟢 | Host-resolved, 404 when PWA off |
Offline shell (cached /) |
🟢 | Navigation fallback only |
| Auto-branded monogram icon | 🟢 | Brand colour + letter, not the real logo ⚪ |
| Admin QR + copyable share link | 🟢 | /admin/qr, admin-gated |
| On-site Install button (Android/Chrome) | 🟢 | iOS is manual Add-to-Home-Screen |
| Install works on themed builder sites | 🟡 | No — folder-mock sites only |
| Share card advertises Install for any live site | 🟡 | Over-promises on non-installable sites |
| Web push on installed Client sites | ⚪ | Not built; SW has no push handler |