Android app (APK)
The Android app is a thin shell around the live site: it opens https://alhudatravels.in/m — the phone layout of the ERP — full-screen in the phone's WebView. Every release of the web app is a release of the Android app — nothing to rebuild or reinstall. The service worker, the tour leader's outbox and the manifest cache (Tour leader app) work inside it as they do in Chrome. Rule PLT-060.
Package: in.alhudatravels.app. Built with Capacitor (capacitor.config.ts, android/).
Getting an APK
.github/workflows/android-apk.yml builds it. It is not part of CI (paid minutes):
- By hand: Actions → Android APK → Run workflow (optionally type a version name).
- By tag:
git tag android-v1 && git push origin android-v1.
The run leaves two artifacts (Actions → the run → Artifacts):
| Artifact | What it is | For |
|---|---|---|
alhuda-travels-debug-<version> |
Debug APK, signed with a throwaway debug key | Testing on a phone. Android asks to allow installs from unknown sources |
alhuda-travels-release-<version> |
Release APK signed with the company keystore | Staff phones; later the Play Store (as an AAB) |
The release APK is built only when the signing secrets exist. Without them the run says so in its summary and stops at the debug APK. A release is never signed with a throwaway key: the phone would refuse the next update as "from a different developer".
versionCode is the workflow's run number, so it always rises; versionName is the
package.json version unless one is typed.
Signing key — one-time setup (owner or IT)
The keystore is the app's identity. Lose it and no future build can update the installed app; leak it and anyone can publish as us. Keep it in the password manager, never in the repo or in chat.
keytool -genkeypair -v -keystore alhuda-android.keystore -alias alhuda \
-keyalg RSA -keysize 2048 -validity 10000
base64 -i alhuda-android.keystore | pbcopy # macOS: the base64 is now on the clipboard
Then GitHub → Settings → Secrets and variables → Actions:
| Secret | Value |
|---|---|
ANDROID_KEYSTORE_BASE64 |
the base64 of the keystore file |
ANDROID_KEYSTORE_PASSWORD |
the store password |
ANDROID_KEY_ALIAS |
alhuda |
ANDROID_KEY_PASSWORD |
the key password |
Installing on a phone
- Download the APK on the phone (from the artifact zip, or a link staff share).
- Open it. Allow Install unknown apps for the browser or file manager if asked.
- Open Alhuda Travels. It loads the live site; sign in as usual.
The tour leader's location prompt (when they tap Attach my location) is the phone's
normal permission dialog; the app declares ACCESS_FINE_LOCATION for it.
Notifications (Firebase) — one-time setup
The WebView has no Web Push, so the app uses Firebase Cloud Messaging. Without this set-up the app works; only alerts are off and the Notifications screen says so.
- Firebase console → create a project (any name) → Add app → Android, package name
in.alhudatravels.app. Downloadgoogle-services.json. - Project settings → Service accounts → Generate new private key. Download the JSON.
- Secrets:
- GitHub → Actions secret
GOOGLE_SERVICES_JSON=base64 -i google-services.json. The workflow writes the file before building. For a laptop build put the file atandroid/app/google-services.json(git-ignored). - Supabase →
supabase secrets set FCM_SERVICE_ACCOUNT_JSON="$(cat service-account.json)", thennpx supabase functions deploy push-send. - Rebuild the APK. On first open the app asks for notification permission (Android 13+)
and stores its device token; the same
push-sendthat reaches browsers now reaches the app.
Building on a laptop
Needs the Android SDK (Android Studio installs it) and Java 17+.
npx cap sync android # copies capacitor-web/ (a placeholder; the app opens the live site)
cd android && ./gradlew assembleDebug # → app/build/outputs/apk/debug/app-debug.apk
npx cap open android opens the project in Android Studio.
Changing the app
- Which page it opens:
server.urlincapacitor.config.ts(/m, the phone ERP). Sign-in, Supabase and the captcha are onallowNavigation; any other link opens in the browser. - Name, icon, colours:
android/app/src/main/res/(icons generated frompublic/pwa-512x512.pngwith@capacitor/assets; re-runnpx @capacitor/assets generate --androidafter changingassets/icon.png). - Permissions:
android/app/src/main/AndroidManifest.xml.
After any of these, run npx cap sync android and rebuild.
The native app
The native app (apps/mobile, Expo) is a different build from
the Capacitor shell above: its own screens, its own package (in.alhudatravels.app too —
do not install both on one phone with the same package name; the native app replaces the
shell), built from .github/workflows/mobile-app.yml.
Checks run on every pull request that touches apps/mobile: npm ci, tsc --noEmit,
expo lint, vitest. The debug APK is built only by hand (Actions → Mobile app → Run
workflow) or on a push to main, to save runner minutes: expo prebuild + Gradle takes ten
minutes or more. The artifact is alhuda-travels-app-debug-<run number>.
On a laptop (Android SDK, JDK 21):
cd apps/mobile && npm ci
npx expo prebuild --platform android --clean
cd android && ./gradlew assembleDebug # → app/build/outputs/apk/debug/app-debug.apk
The APK for staff and travellers' phones is the release build — it carries its own JavaScript and takes over-the-air updates (below); the debug APK needs a developer's computer running Metro:
cd apps/mobile/android
ANDROID_HOME=$HOME/Library/Android/sdk JAVA_HOME=/opt/homebrew/opt/openjdk@21 \
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a
# → app/build/outputs/apk/release/app-release.apk (about 58 MB)
The android/ folder is generated; do not edit it by hand — change app.json and rebuild.
Environment (apps/mobile/.env, all EXPO_PUBLIC_*, baked into the build):
| Variable | Value |
|---|---|
EXPO_PUBLIC_SUPABASE_URL |
the project URL |
EXPO_PUBLIC_SUPABASE_ANON_KEY |
the anon key |
EXPO_PUBLIC_MOBILE_APP_KEY |
the app's key, the same value as the MOBILE_APP_KEY secret on auth-login |
Secrets the owner sets (one-time):
| Where | Secret | For |
|---|---|---|
Supabase → auth-login |
MOBILE_APP_KEY |
the app identifies itself when signing in (supabase secrets set MOBILE_APP_KEY=…, then deploy auth-login) |
| Firebase → the Android app | google-services.json |
push notifications on the phone. In CI: GitHub secret GOOGLE_SERVICES_JSON = base64 -i google-services.json, written to apps/mobile/google-services.json before prebuild. On a laptop: put the file there (git-ignored). Without it the workflow removes android.googleServicesFile from app.json before prebuild — prebuild fails on a missing file — and the app builds with notifications off |
Supabase → push-send |
FCM_SERVICE_ACCOUNT_JSON |
delivering to FCM tokens (same secret as the Capacitor shell, above) |
Supabase → extract-passport |
ANTHROPIC_API_KEY |
reading a photographed passport; without it the app still reads the machine-readable lines typed on the phone |
Updates without a new APK
A fix that changes only JavaScript (screens, wording, logic in apps/mobile/src) reaches
installed phones through EAS Update, without a reinstall
(PLT-061; what the phone does:
native app §10).
| Setting | Value | Where |
|---|---|---|
| Expo account / project | organization alhudatravels (company-owned since 01/10/2026; owner login alhuda-admin, admins ceo-alhuda and syedhamidali) / alhuda-travels, id fd4a2fa8-b795-4c3b-92d5-df73f4269e4c |
app.json → owner, extra.eas.projectId |
| Update URL | https://u.expo.dev/<project id> |
app.json → updates.url |
| Runtime version | policy appVersion: the version in app.json |
app.json → runtimeVersion |
| Channel an APK listens to | production; APP_UPDATE_CHANNEL=preview at prebuild for a test phone |
app.config.js (a Gradle build has no EAS build profile, so the channel is baked in here) |
| When a phone checks | on every cold start, without waiting; the update runs on the next cold start | app.json → checkAutomatically: ON_LOAD, fallbackToCacheTimeout: 0 |
The project id is not a secret. The Expo login is: the Mac is signed in with
npx expo login; npx expo whoami shows who.
Once: the APK that can take updates. Phones installed before expo-updates was added
(app version 0.1.0) cannot receive updates. Build the release APK (above) from main and
install it on every phone one last time. From then on, JavaScript fixes go over the air.
Publishing a fix (from the office Mac, on an up-to-date main, from apps/mobile):
git pull && npm ci
npm run update:preview -- "Fix: receipts list shows the payer" # optional: a test phone on preview
npm run update:production -- "Fix: receipts list shows the payer"
npm run update:list # what production is serving
- The bundle is built on the Mac and the
EXPO_PUBLIC_*values inapps/mobile/.envare baked into it.npm run update:check(run first by both scripts) refuses to publish when any of the three is missing — a bundle without them would sign every phone out for good. Never publish from a checkout that lacks.env. - An update is served to APKs with the same
versiononly. Publishing does not changeversion. - Phones get it on their next cold start and run it on the one after. Staff check More → About.
- Merged to
mainfirst, published second: an update is a release to every phone, and follows the same rules as a web release (PLT-014).
A bad update. Serve the last good one again: npm run update:list shows each update's
group id; npx eas-cli@latest update:republish --group <good group id> (or, on expo.dev,
the project → Updates → the earlier update → Republish). Phones pick it up the same way. An
update that crashes on start is rolled back on that phone to the bundle inside the APK by
expo-updates itself.
Needs a new APK, not an update: a new native package or an Expo SDK upgrade, a new
Android permission or prompt text, anything in app.json / app.config.js (name, icon,
splash, plugins, Firebase). Raise version in app.json (for example 0.2.0 → 0.3.0) in the
same change, build the release APK, install it on every phone. Publishing a JS update built
against the new native code without raising version would send it to old APKs that lack
that code, and they would crash.
Not set up: code signing of updates (expo-updates can verify a signature; the key would
be one more secret to keep); iOS updates (no iPhone build yet); updates to the workflow's debug
APK (a debug build never takes them). Changing to the company keystore later means one more
reinstall — Android refuses an APK signed with a different key.
Release signing is not set up for the native app yet. Two ways, later: EAS Build
(npx eas-cli build --platform android, credentials kept by Expo), or the same
ANDROID_KEYSTORE_* secrets android-apk.yml uses, with a signingConfigs block added to
the generated android/app/build.gradle through an Expo config plugin. The debug APK is for
testing on a phone only.
Not done
- Play Store listing. Needs the signed AAB (
./gradlew bundleRelease), a developer account, the privacy policy URL (https://alhudatravels.in/privacy) and store assets. The Data safety form's account-deletion answer is in App Store (iPhone) §8: an in-app Request account deletion that the office completes within 30 days, and the web linkhttps://alhudatravels.in/privacy#delete-account. - iOS. Capacitor can add it (
npx cap add ios), but it needs a Mac with Xcode and an Apple developer account. The same is true of the native app (npx expo prebuild --platform ios). - Release signing for the native app (above).
- App-links (opening alhudatravels.in links in the app) need
assetlinks.jsonon the site; not set up.
The iOS app
The same Expo project (apps/mobile) builds the iPhone app: bundle id in.alhudatravels.app,
the camera, photo-library, location and notification usage texts are in app.json, and
eas.json carries three build profiles (development, preview, production).
What it needs, none of which the repository can supply:
| Need | Why | Who |
|---|---|---|
| Apple Developer Program membership (₹8,000–9,000 a year) | Signing, TestFlight, the App Store | Owner |
An Expo account signed in to EAS (npx eas-cli login) or a Mac with Xcode installed |
EAS builds in the cloud; Xcode builds on the Mac (the release Mac has only the command-line tools, not Xcode) | Owner / IT |
| Push certificate (APNs key) uploaded to Firebase | Notifications on iPhone go APNs → FCM; eas credentials creates the key once the Apple account exists |
IT |
Build and distribute:
cd apps/mobile
npx eas-cli login # once
npx eas-cli build --platform ios --profile preview # TestFlight-ready .ipa, signed by EAS
npx eas-cli submit --platform ios # to TestFlight; testers install from the TestFlight app
A simulator build for a Mac that has Xcode: npx expo prebuild --platform ios && npx expo run:ios.
The App Store listing, enrolment, API key, review notes and privacy answers: App Store (iPhone).
Not done: no iOS build has been produced yet (no Apple account, no Xcode on the release Mac); the code is the same as the Android app and nothing in it is Android-only except the foreground-service notification for location sharing, which iOS replaces with its own "Always" location permission.