Files
HiveGrid-Wireframes/hivegrid-cloud/docs/operations/capacitor.md
T

250 lines
8.6 KiB
Markdown

# Capacitor mobile wrapper — operations
Last updated: 2026-05-25 (mobile optimization sweep)
The toolctrl mobile app is a Capacitor 7 native wrapper around the
React Router v7 web app at `apps/web/`. There is no separate UI
codebase. This document covers local development, building for iOS /
Android, signing, and store submission.
## Quick reference
| Concern | Where |
|---|---|
| App workspace | `apps/mobile/` |
| Bundle id | `de.toolctrl.app` |
| Capacitor config | `apps/mobile/capacitor.config.ts` |
| Push backend worker | `apps/worker/src/jobs/push-notification.ts` |
| Device-token API | `apps/api/src/routes/devices-routes.ts` (`POST /v1/devices/register`) |
| Prisma model | `prisma/schema/notifications.prisma` → `model DeviceToken` |
| Sentry init | `apps/mobile/src/sentry.ts` |
## Status: code-complete, native builds and store submission deferred
The Phase 08 #65 scope ships:
- The `apps/mobile/` workspace skeleton + Capacitor 7 config.
- Hand-scaffolded iOS + Android native projects under
`apps/mobile/{ios,android}/`. An operator with Xcode / Android Studio
installed should re-run `npx cap add ios && npx cap add android` once
to overwrite the skeletons with the canonical CLI output.
- DeviceToken Prisma model + `POST /v1/devices/register` upsert endpoint.
- `dispatchPushNotification` worker orchestrator with APNs + FCM seams
(real SDK adapters wire in operator-side at deployment).
- `@sentry/capacitor` init mirroring the web client config.
**Explicitly deferred** (operator-owned):
- App-Store and Play-Store submission flows (screenshots, review notes,
release marketing).
- iOS signing certificate + provisioning profile management.
- Android keystore generation + native encrypted storage for long-lived tokens.
- `google-services.json` + APNs `.p8` provisioning.
- CI pipelines (Codemagic / GitHub Actions macOS-runner / Play upload).
## Local development
### Prerequisites
- macOS for iOS (Xcode 15+, CocoaPods, an Apple Developer team)
- Any OS for Android (Android Studio Hedgehog+, Android SDK 34, JDK 17)
- pnpm 10+, Node 22+
### Initial setup
```bash
# 1. Install workspace deps (from repo root)
pnpm install
# 2. Build the web bundle that the wrapper loads
pnpm --filter @toolctrl/mobile build:web
# 3. Sync web assets + plugins into the native projects
pnpm --filter @toolctrl/mobile sync
# 4. Open in the platform IDE
pnpm --filter @toolctrl/mobile open:ios # Xcode
pnpm --filter @toolctrl/mobile open:android # Android Studio
```
### Live-reload dev loop
For tight iteration cycles, point the wrapper at a running web dev
origin instead of the bundled web assets:
```bash
# iOS simulator can reach the host dev server through localhost.
export MOBILE_LIVE_RELOAD_URL=http://localhost:3000
# Android emulator maps 10.0.2.2 to the host loopback instead.
# export MOBILE_LIVE_RELOAD_URL=http://10.0.2.2:3000
pnpm --filter @toolctrl/mobile sync
pnpm --filter @toolctrl/mobile open:ios # or open:android
```
The Capacitor config emits `server.url` only when `MOBILE_LIVE_RELOAD_URL`
is set. It must be an absolute `http://` or `https://` URL. Production
builds run with the variable unset so the bundle loads the embedded
`webDir`.
Use `http://localhost:3000` for the iOS simulator, `http://10.0.2.2:3000`
for the Android emulator, and HTTPS staging/tunnel URLs for physical
devices. The committed Android skeleton only permits cleartext live reload
for `localhost` and `10.0.2.2`; do not broaden cleartext allowances in
release builds.
### Resetting the native projects
When the canonical Capacitor CLI is needed (signing config, new plugin,
platform upgrade):
```bash
cd apps/mobile
rm -rf ios android
npx cap add ios
npx cap add android
npx cap sync
```
Bundle id, app name and webDir come from `capacitor.config.ts`, so
re-creation is deterministic.
## Building iOS
```bash
cd apps/mobile/ios/App
pod install
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-archivePath build/App.xcarchive \
archive
xcodebuild \
-exportArchive \
-archivePath build/App.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath build/ipa
```
`ExportOptions.plist` is operator-provided (not committed). Upload to
TestFlight via Transporter.app or `xcrun altool --upload-app`.
## Building Android
```bash
cd apps/mobile/android
./gradlew bundleRelease
# AAB lands at app/build/outputs/bundle/release/app-release.aab
```
Signing requires `signingConfigs.release` in `app/build.gradle` and a
keystore. The committed skeleton does not declare a release signing
config — operator must add one before the first release build.
## Push notifications
### Backend
The push job is implemented as a pure orchestrator
(`apps/worker/src/jobs/push-notification.ts`) with three injected
provider adapters:
- `sendApns(token, payload)` → `node-apn` / `@parse/node-apn` against the
PEM key configured in `APNS_KEY` / `APNS_KEY_ID` / `APNS_TEAM_ID`.
- `sendFcm(token, payload)` → `firebase-admin` using credentials from
`FCM_SERVICE_ACCOUNT_JSON`.
- `sendWebPush(token, payload)` → optional `web-push` adapter for the
PWA fallback path.
The adapters live in `apps/worker/src/index.ts` (operator-side
implementation, kept out of the orchestrator so unit tests stay
SDK-free). When a provider returns one of the canonical revocation
codes (`Unregistered`, `InvalidRegistration`, `BadDeviceToken`,
`messaging/registration-token-not-registered`,
`messaging/invalid-registration-token`), the corresponding DeviceToken
row is soft-deleted via `revokedAt`.
### Provider credentials
| Env var | Source | Notes |
|---|---|---|
| `FCM_SERVICE_ACCOUNT_JSON` | Firebase console → Project settings → Service accounts → Generate new private key | Either raw JSON or base64-encoded JSON |
| `APNS_KEY` | Apple Developer → Certificates, Identifiers & Profiles → Keys → Create `.p8` | PKCS#8 PEM block from the downloaded `.p8` file (the standard `BEGIN PRIVATE KEY` framing). |
| `APNS_KEY_ID` | Apple Developer (10 chars) | Shown alongside the `.p8` |
| `APNS_TEAM_ID` | Apple Developer → Account → Membership (10 chars) | |
| `APNS_BUNDLE_ID` | Defaults to `de.toolctrl.app` | Must match `capacitor.config.ts` |
**Rotate APNs keys every 12 months.** Apple does not auto-expire `.p8`
files but rotation contains compromise blast-radius.
### Device-token API
The Capacitor shell calls `POST /v1/devices/register` after the native
push plugin yields a token:
```json
POST /v1/devices/register
Authorization: Bearer <service-token>
Content-Type: application/json
{
"userId": "u-1",
"tenantId": "t-1",
"platform": "ios",
"token": "<APNs / FCM / WebPush token>",
"pushProvider": "apns",
"appVersion": "1.0.0"
}
```
The endpoint upserts by `token` (globally unique) so cold-start
re-registration produces zero duplicates. Logout calls
`POST /v1/devices/revoke` with `{ "token": "..." }`.
## App-Store / Play-Store submission
Both flows are **operator-owned** and deferred from this scaffold.
High-level checklists:
### iOS
1. Apple Developer account active (USD 99/year).
2. App Store Connect record created with `de.toolctrl.app` bundle id.
3. Push certificate / Auth Key (`.p8`) generated.
4. Privacy Policy URL published at `/legal/privacy`.
5. App Privacy questionnaire answered (we collect: contact info,
identifiers, usage data; we do NOT collect: precise location,
financial info, health data).
6. Screenshots (6.7" iPhone 15 Pro Max, 5.5" iPhone 8 Plus, optionally
12.9" iPad Pro).
7. Review notes with demo credentials.
8. Submit via TestFlight → "Submit for Review".
### Android
1. Google Play Developer account active (USD 25 one-time).
2. Play Console app created with `de.toolctrl.app` application id.
3. Service account JSON for Play API uploads.
4. Privacy Policy URL.
5. Data safety form (same answers as iOS).
6. Screenshots (phone 9:16 + tablet 16:10).
7. Internal testing → Production rollout (staged 5% → 25% → 100%).
## Rollback
- Revert the `08/capacitor:` commit range to drop the workspace.
- Disable push fan-out by flipping `mobile.push.enabled` feature flag
off (the dispatcher short-circuits without touching DeviceToken rows).
- Roll back the Prisma migration `20260517200000_08_capacitor_device_tokens`
with `prisma migrate resolve --rolled-back` + a manual `DROP TABLE`
if the schema needs to revert.
## See also
- [`docs/improvement-plan/08-mobile-cli-storybook-sdk/capacitor.md`](../improvement-plan/08-mobile-cli-storybook-sdk/capacitor.md)
- [`apps/mobile/README.md`](../../apps/mobile/README.md)
- [`apps/mobile/ios/README.md`](../../apps/mobile/ios/README.md)
- [`apps/mobile/android/README.md`](../../apps/mobile/android/README.md)