Web App (PWA)
The same PraisonAI Mobile bundle, as an installable, offline-capable PWA in the browser
The same mobile bundle boots in a browser tab as an installable PWA, deployed to GitHub Pages, with the remote engine by default and the whole shell cached for an offline reload.
PraisonAI Mobile ships to three targets from one bundle: iOS and Android through the Tauri shell, and the web as a Progressive Web App. The web build is the same dist/ the Tauri shell serves โ the manifest, icons, and service worker are inert inside the shell, so one build covers all three.
Quick Start
- Build the bundle
bash git clone https://github.com/MervinPraison/PraisonAI cd PraisonAI/src/praisonai-mobile npm install npm run buildThe build writes
dist/โ the page, stylesheet, manifest, icons, the entryapp.jsplus its eager chunks, and asw.jswhose cache name is a hash of exactly those bytes. - Serve dist/
bash npx serve distOpen the printed URL. The composer and Send button render, and the app runs against the remote engine by default.
- Install it
The browser shows an Install app prompt because the manifest and service worker are present. Installed, it opens standalone in portrait โ no browser chrome โ using the icons from
manifest.webmanifest. - Reload offline
Cut the network and reload. The shell still renders: the service worker serves the last
index.htmland the precached assets from its versioned cache.
The Manifest
app/manifest.webmanifest is what turns the tab into an installable app. Every URL is relative (./) so a project site served from a subpath resolves correctly.
| Field | Value | Why |
|---|---|---|
id | ai.praison.mobile | Stable identity across installs. |
name | PraisonAI | Full install name. |
short_name | PraisonAI | Home-screen label. |
start_url | ./ | Relative, so a subpath install opens the right page. |
scope | ./ | Confines the app to its own subpath. |
display | standalone | Opens without browser chrome. |
orientation | portrait | Locks to portrait, matching the phone shell. |
background_color | #ffffff | Splash background before first paint. |
theme_color | #1f7a63 | Toolbar / status-bar tint. |
Icons are read from the Tauri icon set at build time, so the manifest cannot name an icon the shell does not have โ a missing one fails the build, not a phone’s home screen.
How Offline Works
The service worker picks a strategy from the request, never touching cross-origin traffic.
| Request | Strategy |
|---|---|
| Navigation | Network-first; on failure, the cached index.html. A user online gets the current page; one offline gets the last one that loaded. |
| Precached asset | Cache-first. These bytes are the ones the cache name was derived from. |
| Other same-origin (lazy chunk) | Fetch, then cache on use into the same versioned cache. |
Cross-origin (engine at 127.0.0.1:*, an https: engine) | Never touched โ passthrough. |
How Redeploys Work
A new build is a new cache, and the old one is deleted on activate โ so a redeploy never pins a user to a stale app.
- The cache name is
praisonai-mobile-<buildId>, wherebuildIdis the first 16 hex of a sha256 over the precached bytes. - On
activate, everypraisonai-mobile-*cache that is not the current one is deleted, thenclients.claim()runs. - Navigation is always network-first, so a new
index.htmlsurfaces on the first online visit rather than waiting for a cache to expire.
Which Engine the Web Picks
A browser tab has a server to talk to, so the web defaults to the remote engine.
import { defaultEngineIdFor } from "praisonai-mobile/app/main";
defaultEngineIdFor("web"); // "remote-http" โ a browser tab has a server
defaultEngineIdFor("tauri"); // "praisonai-ts" โ a device runs in-processdefaultEngineIdFor only decides the very first launch; a persisted engineId always wins. The web picks remote-http because a browser has no reason to fetch ~1.4 MB of engine before first paint. See Engines โ First-Launch Default.
Content Security Policy
The browser sees only the <meta> CSP in index.html; Tauri appends its own, so inside the shell both apply and the effective policy is their intersection.
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:* http://localhost:* https:; manifest-src 'self'; worker-src 'self'; base-uri 'self'; form-action 'self'"
/>| Directive | What it allows | Why |
|---|---|---|
script-src 'self' | External scripts only โ no inline, no eval. | Every script, including the SW registration, is an external file, so there is nothing to whitelist and no hash to keep in step. |
connect-src local | http://127.0.0.1:, http://localhost: | Reach a local engine. |
connect-src remote | https: | Reach a remote HTTPS engine. |
connect-src IPC | ipc:, http://ipc.localhost | Inert in a browser, but keeps Android’s IPC channel unblocked once the Tauri shell appends its own tag. |
Every URL Is Relative
GitHub Pages serves project sites from a subpath (e.g. /PraisonAI/), so every URL in the app is relative.
An absolute /app.js or /manifest.webmanifest would resolve to the wrong site. The boot proof (tools/web-boot.test.mjs) serves dist/ under /PraisonAI/ to pin exactly this โ the manifest is served as application/manifest+json, icons return 200 as image/png, and the service worker activates scoped to the subpath.
Deploying to GitHub Pages
The mobile-web.yml workflow builds, boots the app in a real browser, and deploys โ but only main deploys, and only after one manual repo setting.
| Step | What it does |
|---|---|
Build praisonai-ts | It is a file: link, so its dist/ must exist before the mobile package installs. |
npm ci, install Chromium, npm run build | Produce dist/. |
npm run test:web | Serve dist/ under /PraisonAI/ and drive first paint, install, an offline reload, and a blocked inline script. |
Upload mobile-web-dist | The built site is always available as an artifact, even when the deploy is skipped. |
| Deploy (main only) | actions/deploy-pages, gated on the Pages source. |
Choosing the Web Build vs. Tauri
Both ship from the same bundle; pick by what the target needs.
| Choose | When |
|---|---|
| Web PWA | No install or store review needed; instant access on any device; a server is available for the remote engine. |
| Tauri shell | An on-device engine, OS integration (safe-area, keyboard insets, back gesture), or offline-first before the app has ever been online. |
Best Practices
Never use absolute URLs
The app is designed to live under a subpath on GitHub Pages. An absolute /app.js resolves to the wrong site. Keep start_url, scope, links, icons, and scripts ./-relative.
Never precache lazy chunks
Splitting exists so a browser does not pay ~1.4 MB it may never use. Precaching lazy chunks undoes that. The service worker caches each one on first use into the same versioned cache instead.
Never register the service worker inside Tauri
register-sw.js skips registration when __TAURI_INTERNALS__ is present, on non-http(s) origins, and where serviceWorker is absent. Inside the shell the app IS the offline copy; a worker caching custom-protocol responses would put a second copy between the shell and its assets.
Let a failed registration degrade, not break
If registration fails, the app still boots from the network. register-sw.js logs a warning rather than throwing โ a degraded page, not a broken one.
Trust the content-hash cache name
The cache name is a sha256 of the precached bytes, so two builds of one version differ the moment a source file does. activate deletes any older praisonai-mobile-* cache, so a redeploy never serves a mismatched pair of old and new chunks.