Metricis Mobile Deployment Guide¶
This guide covers the current Capacitor 8 mobile build, debug, signing, and release workflow for the Metricis assessment client.
For the automated GitHub Actions + Fastlane release path, see Release Automation. This page focuses on local validation, native platform setup, app-store readiness, and deployment-specific checks.
Current Mobile Baseline¶
| Area | Current Baseline |
|---|---|
| Capacitor | @capacitor/core, @capacitor/android, @capacitor/ios, and CLI 8.3.x |
| Node | 24.16.0 from .nvmrc |
| Java | 21 for Android builds |
| Android | SDK 36, Android Gradle Plugin 8.13.0, Gradle wrapper 8.14.3 |
| iOS | iOS deployment target 15.0; current simulator validation uses Xcode 26.x / iOS 26.5 |
| Bundle ID / package | app.metricis.app |
| Native project root | client/android and client/ios/App |
| Release automation | .github/workflows/mobile-release.yml plus fastlane/Fastfile |
Mobile app submission is separate from the M7 web cutover. The native projects now build and sync on CI, but store submission still depends on Firebase/APNs configuration, signing assets, store metadata, and sponsor/regulatory release approval.
The native app strategy is single-app for now: the client assessment app is the only Capacitor app. The patient-portal workspace remains a web-only frontend deployed through the normal web path; it should not receive native projects, signing identities, or store listings unless a future roadmap milestone explicitly reopens a two-app strategy.
Prerequisites¶
Shared¶
- Node
24.16.0; usenvm useor install the version from.nvmrc. - npm from the selected Node toolchain.
- Repository dependencies installed with
npm installfrom the monorepo root. - No global Capacitor CLI is required; use the workspace scripts below.
Android¶
- Android Studio with Android SDK 36 installed.
ANDROID_HOMEpointing at the SDK, orclient/android/local.propertiescontainingsdk.dir=/absolute/path/to/Android/sdk.- Java 21 available to Gradle.
- A release upload keystore for Play Store builds.
iOS¶
- macOS with Xcode installed.
- Xcode command line tools selected with
xcode-select. - iOS simulator/runtime matching the local Xcode installation.
- CocoaPods available for local iOS native builds.
- Apple Developer account, distribution certificate, provisioning profile, and App Store Connect API key for signed releases.
Fastlane¶
Fastlane dependencies are intentionally isolated under fastlane/Gemfile so normal cap sync ios does not try to resolve release gems.
Monorepo Capacitor Commands¶
Run these from the repository root.
# Sync both native projects after building client assets
npm run cap:sync
# Sync one platform
npm run cap:sync:android
npm run cap:sync:ios
# List or run Android targets
npm run cap:run:android -- --list
npm run cap:run:android -- --target <device-or-emulator-id>
# List or run iOS targets
npm run cap:run:ios -- --list
npm run cap:run:ios -- --target <simulator-name-or-udid>
# Open native IDEs
npm run cap:open:android
npm run cap:open:ios
If you are already inside client/, the equivalent direct command is npx cap run android or npx cap run ios. From the monorepo root, prefer the npm scripts so Capacitor targets the client workspace instead of the repository root.
Local Debug Builds¶
Android Debug¶
Build and sync the Android project:
Assemble a debug APK:
Run on a connected emulator or device:
Typical emulator target names look like Medium_Phone_API_36.1; ADB IDs look like emulator-5554.
iOS Debug¶
Build and sync the iOS project:
Open in Xcode:
Or build from the command line without signing:
cd client/ios/App
pod install
xcodebuild -workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO \
build
Run on a simulator:
Version Management¶
Use the repository version helper so npm package versions and native metadata stay aligned:
npm run release:bump -- --version=1.2.3 --build=123 --dry-run
npm run release:bump -- --version=1.2.3 --build=123
The helper updates:
package.jsonclient/package.jsonclient/android/app/build.gradleclient/ios/App/App.xcodeproj/project.pbxproj
Rules:
--versionmust be numeric SemVer:major.minor.patch.--buildmust be a positive integer.- Store uploads require monotonically increasing build numbers.
- Apply the bump on a branch, validate, open a PR, and release from
main.
Release Workflow¶
Preferred release path:
- Land a version-bump PR on
main. - Open GitHub Actions.
- Run Mobile and web release.
- Choose
platform,upload_to_stores,android_track,ios_destination, anddeploy_landing. - Approve any protected GitHub Environments.
- Download retained artifacts or finish store promotion in the store consoles.
See Release Automation for required secrets, dispatch presets, and promotion policy.
Local Release Commands¶
Android App Bundle¶
Build a signed Android App Bundle through Fastlane:
Manual Gradle fallback after exporting signing variables:
NODE_ENV=production npm run build --workspace=client
npm run cap:sync:android
cd client/android
./gradlew bundleRelease --stacktrace
The release AAB is written to:
Upload to Google Play internal testing through Fastlane:
SUPPLY_JSON_KEY_DATA="$(cat google-play-service-account.json)" \
BUNDLE_GEMFILE=fastlane/Gemfile bundle exec fastlane android play track:internal release_status:draft
Google Play usually requires first-time app/listing setup and at least one manual initial binary before API uploads can fully manage tracks.
iOS IPA¶
Build a signed IPA through Fastlane after exporting signing variables:
Upload to TestFlight:
Upload to App Store Connect without automatic submission:
The App Store lane uploads the binary but does not submit for review and does not automatically release.
Signing Assets and Secrets¶
The GitHub Actions release workflow expects these secret groups:
- Android Firebase config, keystore base64, passwords, alias, and Google Play service account JSON.
- Apple team ID, distribution certificate, provisioning profile, CI keychain password, and App Store Connect API key.
- Cloudflare secrets only when deploying the landing site.
Use Release Automation as the authoritative secret list.
Never commit keystores, signing certificates, provisioning profiles, API keys, or store service-account JSON.
Push Notifications¶
iOS APNs¶
The iOS project includes push-notification entitlements and forwards APNs registration callbacks to Capacitor. Before release:
- Create an APNs key or certificate in Apple Developer.
- Confirm the App ID has Push Notifications enabled.
- Configure backend APNs credentials.
- Smoke test device token registration through
POST /api/notifications/register.
Android FCM¶
Android still requires a project-specific Firebase configuration file:
Generate it from Firebase Console for package app.metricis.app. Use client/android/app/google-services.json.example as a shape reference only; do not copy the placeholder file into a release build.
For local release validation, place the real file at client/android/app/google-services.json. The file is ignored by Git so project-specific Firebase values do not get committed accidentally.
For GitHub Actions release builds, base64-encode the real file and save it as ANDROID_GOOGLE_SERVICES_JSON_BASE64:
Release builds fail fast when the Firebase config is missing. Debug builds may omit it, but Android push notifications will not work without it.
Troubleshooting¶
android platform has not been added yet¶
Run Capacitor from the client workspace or use the root workspace script:
Running npx cap run android from the monorepo root looks for android/ at the wrong level.
Android SDK location not found¶
Set ANDROID_HOME:
Or create client/android/local.properties:
invalid source release: 21¶
Gradle is using a Java runtime older than Java 21. Point Gradle at Java 21:
Or add to client/android/gradle.properties locally:
Do not raise the Android Java target to a newer local JDK just because it is installed. Capacitor 8 and the Android toolchain are validated here on Java 21.
ADB install fails after Gradle succeeds¶
The APK built, but deployment to the emulator/device failed. Check:
adb devices
adb -s <device-id> install -r client/android/app/build/outputs/apk/debug/app-debug.apk
adb logcat
Common causes are a stale emulator session, a mismatched installed package signature, insufficient device storage, or an emulator that has not fully booted.
CocoaPods or Fastlane gems interfere with cap sync ios¶
There should not be a root Gemfile. Fastlane dependencies live under fastlane/Gemfile.
Use:
Normal iOS sync remains:
WebView cannot reach local API from a device¶
Use your workstation LAN IP for native builds:
Android debug builds include a debug-only cleartext network security config. Release builds must use HTTPS.
Security and Store Readiness¶
- WebView debugging is disabled in source for production posture.
- Android backup is disabled so local clinical data is not copied to Google backup.
- Android release builds use ProGuard keep rules for Capacitor bridge/plugin classes.
- Release signing assets must be stored in GitHub Secrets or local secure storage only.
- Production store uploads should require GitHub Environment approval and sponsor/regulatory sign-off.
Universal Links (iOS) Deployment Checklist¶
The repo ships client/public/.well-known/apple-app-site-association (AASA), which Vite copies into client/dist/.well-known/apple-app-site-association. Apple's CDN fetches it once at app install / first launch and caches it.
1. Replace the TEAMID placeholder¶
The committed AASA contains the placeholder appID TEAMID.app.metricis.app. Before the file is served from production, replace TEAMID with the 10-character Apple Developer Team ID from App Store Connect > Membership. The deployed file must look like:
The placeholder fails verification by design. Apple will refuse to validate the domain until the real Team ID is in place.
2. nginx serving rules¶
Apple requires the AASA file to be served:
- With
Content-Type: application/json. - Without a
.jsonextension. - Without any redirect.
- Over HTTPS with a valid certificate.
Example nginx snippet:
location /.well-known/apple-app-site-association {
default_type application/json;
try_files $uri =404;
add_header Cache-Control "no-cache" always;
}
3. Validation¶
curl -sI https://app.metricis.app/.well-known/apple-app-site-association
# Expect: 200 OK, Content-Type: application/json, no Location header
Also run Apple's parser through a trusted AASA validator before release.
4. Path components¶
The AASA currently claims:
/study/*/start/participant/*/session/resume/*/battery/*/?link=*/?study=*/?participant=*
These mirror client/src/services/mobile/deepLinking.ts. If a new deep-link route is added there, add a matching AASA component.
App Links (Android) Deployment Checklist¶
The repo ships client/public/.well-known/assetlinks.json, which Vite copies into client/dist/.well-known/assetlinks.json. Google's verifier fetches it from https://app.metricis.app/.well-known/assetlinks.json when the app is installed and on signed APK/AAB updates.
1. Replace the fingerprint placeholder¶
The committed file uses a placeholder in sha256_cert_fingerprints. Before production, replace it with the SHA-256 fingerprint of the release signing keystore:
For debug App Links testing, append the debug keystore fingerprint too:
keytool -list -v \
-keystore ~/.android/debug.keystore \
-alias androiddebugkey \
-storepass android | grep "SHA256:"
The placeholder fails verification by design so a wrong fingerprint is not silently accepted.
2. nginx serving rules¶
Google requires assetlinks.json to be served:
- With
Content-Type: application/json. - Over HTTPS with a valid certificate.
- With no redirect.
- With the literal filename
assetlinks.json.
Example nginx snippet:
location = /.well-known/assetlinks.json {
default_type application/json;
try_files $uri =404;
add_header Cache-Control "no-cache" always;
}
3. Validation¶
curl -sI https://app.metricis.app/.well-known/assetlinks.json
# Expect: 200 OK, Content-Type: application/json, no Location header
curl -s "https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=https://app.metricis.app&relation=delegate_permission/common.handle_all_urls"
On a signed installed build:
4. Path patterns¶
The Android intent filter claims:
/study/.*/start/participant/.*/session/resume/.*/battery/.*/
Magic-link URLs use query strings such as /?link=...; Android pathPattern does not match query strings, so the root path entry is what dispatches those links to the app.
5. autoVerify failure mode¶
If assetlinks.json is missing, malformed, served with the wrong content type, or contains a mismatched fingerprint, autoVerify fails silently. Android keeps the intent filter active but may show a chooser instead of opening Metricis automatically. Re-check adb shell pm get-app-links app.metricis.app after every release-signing change.