Mobile Builds
PizzaPi ships a thin Capacitor wrapper around the existing React/Vite PWA. The web app remains the product; native projects provide packaging, app-store paths, and future native integration points.
The mobile app keeps the user inside the bundled UI after setup. On first launch the bootstrap page captures the server URL (manually or via QR), completes mobile-link approval in the web UI, then loads the bundled PWA locally and authenticates it with a short-lived API key minted during approval. The bundled UI talks to the relay through native HTTP (CapacitorHttp) and Socket.IO using absolute URLs and the injected key, avoiding cross-origin WebView cookie issues.
What Exists
Section titled “What Exists”capacitor.config.tspoints Capacitor atmobile/, which contains both the bootstrap page (index.html) and the bundled PWA (app/).mobile/index.htmlhandles first-run server URL entry, QR scanning, and mobile-link redemption.scripts/copy-mobile-ui.tscopies the builtpackages/ui/distassets intomobile/app/duringbun run build:mobile.android/contains the committed Android project.ios/contains the committed iOS project.bun run build:mobile*always builds the UI withVITE_MOBILE=1 VITE_BASE_URL=./, which disables the Vite PWA plugin (enablePWAinpackages/ui/vite.config.ts) — a service worker registered inside the Capacitor WebView precaches by absolute scope and can serve stale HTML after a rebuild (black screen). The bundled app should therefore never containsw.js,registerSW.js,workbox-*.js, ormanifest.webmanifest.android/app/src/main/assets/public/andios/App/App/public/(the assetscap synccopies into each native project) are build output, not tracked in git — both are ignored wholesale byandroid/.gitignoreandios/.gitignore. Nothing under either path should ever be committed; runbun run build:mobile,build:mobile:android, orbuild:mobile:iosto (re)generate them locally.- CI builds the Android debug APK on pull requests and pushes, and guards both bundle trees against stale service-worker artifacts (see CI coverage below).
CapacitorHttpis enabled sofetch/XHRrun through native networking (the API key is only injected for relay-origin requests).
Commands
Section titled “Commands”# Build the PWA and sync both native projectsbun run build:mobile
# Build the Android debug APKbun run build:mobile:android
# Build a SIGNED Android release APK + AAB (requires the PIZZAPI_KEYSTORE_* vars below)bun run build:mobile:android:release
# Build the PWA and sync iOS project filesbun run build:mobile:iosCI coverage
Section titled “CI coverage”.github/workflows/ci.yml includes a mobile-android job that runs on every pull request and push. It installs Java, Android SDK tooling, and Bun dependencies, then runs:
bun run build:mobile:androidThat intentionally builds a debug APK only — signed with Android’s throwaway debug key, fine for smoke-testing but not distributable.
The same job then runs bunx cap sync ios (a plain Node/Bun file-copy + Swift
Package manifest step — no CocoaPods, no Xcode, so it runs fine on the
ubuntu-latest runner) to populate ios/App/App/public/ from the same
VITE_MOBILE bundle, and finishes with a guard step that fails the build if
sw.js, registerSW.js, workbox-*, or manifest.webmanifest shows up
anywhere under android/app/src/main/assets/public/ or ios/App/App/public/.
That’s the regression this whole page exists to prevent: a stale PWA build
(pre-VITE_MOBILE, PWA plugin still enabled) sitting in the native shell and
black-screening the app on launch.
Release signing
Section titled “Release signing”android/app/build.gradle defines a release signing config whose keystore path
and credentials come from env vars (or gradle properties) — never hardcoded. If
no keystore is configured the release build is left unsigned, so debug/local
builds keep working without secrets.
-
Generate a keystore once, and guard it. This file is the identity of your app on the Play Store — back it up securely; if you lose it you can never ship an update to the same listing.
Terminal window keytool -genkeypair -v \-keystore pizzapi-release.jks \-alias pizzapi \-keyalg RSA -keysize 2048 -validity 10000 -
Build a signed release by providing the four vars at build time:
Terminal window PIZZAPI_KEYSTORE_FILE=/abs/path/pizzapi-release.jks \PIZZAPI_KEYSTORE_PASSWORD=… \PIZZAPI_KEY_ALIAS=pizzapi \PIZZAPI_KEY_PASSWORD=… \bun run build:mobile:android:releaseThis runs
assembleRelease bundleRelease, thenscripts/verify-apk-signed.ts, which fails the build if the APK came out unsigned or ifapksigneris not found under$ANDROID_HOME/build-tools. Outputs land inandroid/app/build/outputs/{apk,bundle}/release/. Bump the build number with-PversionCode=<n> -PversionName=<x.y.z>. -
Verify a signature manually any time with the SDK’s apksigner:
Terminal window $ANDROID_HOME/build-tools/<ver>/apksigner verify --print-certs app-release.apk
Signing in CI
Section titled “Signing in CI”The repo ships two Android workflows:
.github/workflows/android-debug-apk.yml— unsigned debug APK, no secrets; runs onworkflow_dispatchandv*tags, uploadsapp-debug.apk..github/workflows/android-release.yml— signed release (APK + AAB), runs onworkflow_dispatchandv*tags. It never runs on pull requests, so secrets are never exposed to forks, and it fails fast with a clear message if any secret is missing.
To set up the signed workflow, add these under Settings → Secrets and
variables → Actions (never commit the .jks):
| Secret | Value |
|---|---|
PIZZAPI_KEYSTORE_BASE64 | base64 -i pizzapi-release.jks (the whole keystore, base64-encoded) |
PIZZAPI_KEYSTORE_PASSWORD | keystore password |
PIZZAPI_KEY_ALIAS | key alias (e.g. pizzapi) |
PIZZAPI_KEY_PASSWORD | key password |
# macOS: copy the base64 keystore straight to the clipboardbase64 -i pizzapi-release.jks | pbcopyThe workflow decodes the keystore into $RUNNER_TEMP, sets the four
PIZZAPI_KEYSTORE_* vars, and runs bun run build:mobile:android:release
(which verifies the APK is signed before uploading). Trigger it from the Actions
tab (optionally passing versionName / versionCode) or by pushing a v* tag —
on a tag the versionName defaults to the tag minus its leading v.
iOS is committed and syncable, and CI now sanity-checks its generated bundle (see CI coverage above), but nothing ever compiles the Xcode project in CI. Add macOS/Xcode CI only when iOS distribution is active; it needs signing/provisioning setup and is slower than the Android smoke check.
ios/App/App/public/ (the web assets cap sync copies in) is generated and
gitignored, exactly like Android’s android/app/src/main/assets/public/ — run
bun run build:mobile:ios (or bun run build:mobile) to regenerate it
locally; never hand-edit or commit it.
Server URL and Auth
Section titled “Server URL and Auth”The mobile bootstrap page stores the relay URL in localStorage and hands the API key to the bundled UI via a URL fragment, which the UI persists to native secure storage (iOS Keychain / Android Keystore). The bundled UI reads those values and:
- Rewrites relative
fetchURLs to the configured relay server. - Adds the
x-api-keyheader to REST calls and passes the API key in the Socket.IOauthpayload. - Connects to
/hub,/viewer,/runners, and/terminalnamespaces using an absolute server URL and the API key.
Because the app uses API-key auth, the server trusts the Capacitor origins (capacitor://localhost and https://localhost) by default. Disable this with PIZZAPI_TRUST_MOBILE_ORIGINS=false if your deployment requires explicit origin allowlisting.
QR-based setup is the recommended flow. Manual URL entry still redirects to the server’s web sign-in page for now; full in-app sign-in for manual entry is planned follow-up work.
Future Native Integrations
Section titled “Future Native Integrations”Keep integrations service-driven:
- Push notifications should map native device tokens into the existing push subscription model.
- Deep links should route to sessions or service panels, not native-only screens.
- Share/camera/secure-storage plugins should be added only when the web API is insufficient.
- Runner services should stay server-side; mobile can expose small typed bridge capabilities when needed.