docs(mobile): roadmap + phase-0 deployment foundation design

Mobile shipping is decomposed into 5 phases:
  0. Deployment Foundation — Netralax brand on a runnable dev build.
  1. Auth + Chat MVP — magic-link login, conversation list, text send.
  2. Messaging Features — attachments, voice messages, reactions.
  3. Voice/Video Calls — LiveKit RN + CallKit/ConnectionService.
  4. Polish + Store Submission — TestFlight, Play, signing.

Phase 0 spec lays out the concrete file changes:
  * app.json rename to Netralax + cloud.netralax.app bundle/package.
  * New eas.json with development/preview/production profiles.
  * SafeAreaProvider + GestureHandlerRootView + ErrorBoundary in the
    root layout, Netralax landing screen with runtime app version.
  * sharedSmoke.ts runtime import to verify Metro can resolve
    @chat-app/shared (which already has CryptoBackend/SecretStore
    interfaces designed for mobile adapters).
  * README quickstart for `eas init` + first dev client build.

No code changes here — just the planning surface. Implementation plan
follows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-05-13 23:27:52 +02:00
parent a560f24c45
commit 5f753412a6
2 changed files with 278 additions and 0 deletions
@@ -0,0 +1,91 @@
# Mobile Deployment Roadmap
**Date:** 2026-05-13
**Scope:** `apps/mobile` end-to-end ship to iOS App Store + Google Play
**Status:** Decomposition — each phase ships independently with its own spec.
---
## Where we are today
- `apps/mobile` is an Expo + React Native scaffold from the very first commit (`f7cfd2a initial`). It has the right deps (`expo-router`, `react-native-libsodium`, `expo-secure-store`, `expo-sqlite`, `expo-notifications`) but no real screens — just two placeholders ("ChatApp" landing, "Chats — placeholder").
- Earlier mobile work (image attachments, voice messages, voice calls, reactions, profile editing, push wiring) lives on the archived `byGalax/chat-app` GitHub repo. It targets a pre-Tauri→Electron `packages/shared` API and is not directly portable; we treat it as reference material, not a merge source.
- `packages/shared` is platform-agnostic by design (`CryptoBackend` and `SecretStore` interfaces wait for mobile adapters), so the desktop's business logic is reusable verbatim once we plug in RN-flavoured implementations.
- The desktop reached `v0.17.5` with a Netralax rebrand, Discord-style call/chat UX, and an EAS-free electron-builder/electron-updater release pipeline pointed at `update.netralax.cloud`.
## Goal
Ship the Netralax mobile app to iOS App Store + Google Play with the core Discord-style chat + voice/video calls of the desktop, working over the same self-hosted Supabase + LiveKit backend.
## Decomposition
Five phases, each with its own design spec, plan, and ship gate. We brainstorm and execute one at a time; later phases only get fleshed out once their prerequisites land.
### Phase 0 — Deployment Foundation
**Spec:** `2026-05-13-mobile-phase-0-foundation-design.md`
**Goal:** prove the pipeline. After Phase 0, `eas build` (or `expo run`) produces a runnable Netralax-branded shell on a real iPhone + Android device. No features yet — but every later phase ships through this same build pipeline.
### Phase 1 — Auth + Chat MVP
**Why next:** smallest end-to-end vertical slice that's actually useful. Users can log in, see conversations, send/receive text.
- Mobile `CryptoBackend` adapter wrapping `react-native-libsodium`.
- Mobile `SecretStore` adapter wrapping `expo-secure-store`.
- Magic-link auth + device-key registration (reuse `packages/shared/auth`).
- Conversation list (DM + group) with last-message preview.
- Conversation view: decrypt + render text messages, send text.
- Push notifications via `expo-notifications` + the existing `notify-push` edge function.
- Minimal profile + logout.
### Phase 2 — Messaging Features
**Why next:** mobile users expect parity with desktop on day-to-day messaging.
- Image attachments (camera roll + camera capture).
- File attachments (document picker).
- Voice messages (record + play).
- Reactions, edit, delete, reply, forward.
- Read receipts + delivery state.
- Typing indicator.
- Polls (optional / stretch).
### Phase 3 — Voice/Video Calls
**Why now and not Phase 1:** RN LiveKit + native call UX (CallKit / ConnectionService) is the heaviest single feature. Hard to scope without the basic chat working first, and not blocking for an early TestFlight.
- LiveKit React Native SDK (`@livekit/react-native` + `@livekit/react-native-webrtc`).
- Incoming call notifications wake the app via CallKit (iOS) + ConnectionService (Android).
- Outgoing call flow.
- Voice + video tracks, mute/hangup, speaker/earpiece switch, headset routing.
- Screen sharing is explicitly out of scope on mobile.
### Phase 4 — Polish + Store Submission
**Why last:** can't submit until the features are in.
- Privacy policy + Terms surfaced inside the app.
- Store-listing assets: screenshots, descriptions, age rating.
- Code signing: Apple Developer Program enrolment + provisioning, Android upload keystore.
- TestFlight internal + external testing.
- Google Play internal track → closed test → production.
## Cross-cutting decisions (locked in here, no per-phase relitigation)
| Topic | Decision | Rationale |
|---|---|---|
| Framework | Expo + React Native (existing scaffold) | Already set up; New Architecture enabled; managed-workflow gives EAS Build out of the box |
| Branding | Netralax everywhere | Matches the desktop rebrand released as v0.17.x |
| Bundle / package id | `cloud.netralax.app` (both iOS + Android) | Matches the desktop's AppUserModelId `cloud.netralax.desktop`; `com.meinname.chatapp` is dev-placeholder |
| Shared business logic | Consume `@chat-app/shared` verbatim | Designed for this; the only mobile-specific pieces are the `CryptoBackend` + `SecretStore` adapters |
| Old chat-app mobile sprints | Reference, do not merge | API drift since pre-Electron; cleaner to rewrite against current shared |
| Backend | Self-hosted Supabase at `update.netralax.cloud` + LiveKit | Unchanged from desktop |
| Push delivery | Expo Push Service for development; native APNS + FCM after EAS submit | Avoids managing certs in Phase 1 |
| State management | React local state + tiny context, same as desktop | Avoid Redux/Zustand bloat for a chat app |
| Persistent local store | `expo-sqlite` (already in deps) | Mirror desktop's better-sqlite3 schema |
## Ship gates between phases
A phase only ends — and the next one begins — when:
1. Manual verification of every spec'd flow passes on **both** an iOS device and an Android device.
2. The previous phase's release build still runs (no regression).
3. The phase's spec is reflected in the codebase (no straggling TODOs that were in scope).
## Out of scope for the entire roadmap
- Web build of the mobile app (Expo can technically emit one — not worth the bundle-cost double-duty since the Electron app already covers desktop).
- macOS / Windows React Native targets.
- Watch / TV / wearable apps.
- iPad-specific UI tuning beyond `"supportsTablet": true` (we ship the phone UI on tablet for now).
- Resurrecting any code from the archived `chat-app` mobile sprints.
@@ -0,0 +1,187 @@
# Mobile Phase 0 — Deployment Foundation
**Date:** 2026-05-13
**Scope:** `apps/mobile`
**Roadmap context:** `2026-05-13-mobile-deployment-roadmap.md`
---
## Problem
We can't even smoke-test a Netralax-branded build on a real device today. The Expo scaffold says "ChatApp" everywhere, the EAS project ID is a literal string `REPLACE_WITH_EAS_PROJECT_ID`, and the iOS bundle id / Android package id is the placeholder `com.meinname.chatapp`. Every later phase ships through this build pipeline, so it needs to work end-to-end before we touch features.
## Goal
After Phase 0, a developer (or the user) can run a single command from the repo root and get an installable `.ipa` or `.apk` carrying the Netralax brand. No real features — just a launch screen that renders, plus the harness underneath: SafeArea, navigation root, error boundary, the `@chat-app/shared` package compiling and importable from the mobile app, version readout, EAS config.
## Non-goals
- Auth, chat, calls, push wiring (Phases 13).
- App Store / Play Store submission (Phase 4).
- Resurrecting the legacy mobile sprints from the archived `chat-app` repo.
- Code-signing certificates / provisioning profiles. Phase 0 produces development-signed builds via EAS managed credentials; production signing is Phase 4.
## Design
### 1. Branding rename in `apps/mobile/app.json`
Replace every `ChatApp`/`com.meinname.chatapp` reference with the Netralax brand. Final values:
```json
{
"expo": {
"name": "Netralax",
"slug": "netralax",
"scheme": "netralax",
"version": "0.1.0",
"ios": {
"bundleIdentifier": "cloud.netralax.app",
"supportsTablet": true,
"infoPlist": { "ITSAppUsesNonExemptEncryption": false }
},
"android": {
"package": "cloud.netralax.app",
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#0b0b0f"
}
},
"extra": { "eas": { "projectId": "REPLACE_WITH_EAS_PROJECT_ID" } }
}
}
```
`extra.eas.projectId` stays as the placeholder string in source — the user runs `eas init` once locally, which fills it in and commits. Documented in the mobile README.
### 2. App icon + splash
Replace `assets/icon.png`, `assets/adaptive-icon.png`, `assets/splash.png` with placeholder Netralax variants (purple-on-dark `N` mark matching the sidebar avatar in the desktop). A single 1024×1024 master plus the 1024-Android-foreground and 1284×2778 splash is enough for Phase 0. Final art lives in `apps/mobile/assets/`.
### 3. EAS configuration
Create `apps/mobile/eas.json`:
```json
{
"cli": { "version": ">= 13.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": true },
"android": { "buildType": "apk" }
},
"preview": {
"distribution": "internal",
"ios": { "simulator": false },
"android": { "buildType": "apk" }
},
"production": {
"ios": { "simulator": false },
"android": { "buildType": "app-bundle" }
}
},
"submit": {
"production": {
"ios": { "appleId": "REPLACE_WITH_APPLE_ID", "ascAppId": "REPLACE_WITH_ASC_APP_ID" },
"android": { "serviceAccountKeyPath": "./play-service-account.json" }
}
}
}
```
`production`-submit credentials stay as placeholders — Phase 4 fills them in. The `development` profile produces a custom dev client + APK that can be installed and used with `expo start --dev-client`.
### 4. Minimal app shell that doesn't crash
Four changes to verify the runtime is healthy:
- `app/_layout.tsx`: wrap the `<Stack>` in `<SafeAreaProvider>` from `react-native-safe-area-context`. Add `<GestureHandlerRootView style={{ flex: 1 }}>` outside it so future gesture-based UI is unblocked. Wrap everything in a top-level `<ErrorBoundary>` (new component, §4a) so an unhandled render error shows a readable fallback instead of a white-screen crash on a TestFlight build.
- `app/index.tsx`: render "Netralax" (not "ChatApp"), display the app version read from `expo-application` (`Application.nativeApplicationVersion`), and link to `/(app)/chats` via a placeholder button so navigation is proved.
- `app/(app)/chats.tsx`: same — render "Netralax — Chats coming soon" so the protected-stack mounts cleanly.
#### 4a. Error boundary
`apps/mobile/components/ErrorBoundary.tsx` is a small React class component that catches render errors and shows the error message + a "Reload" button. Same shape as the desktop's crash-recovery surface. Around 40 lines.
### 5. Shared package smoke import
Phase 0's gate is that `import { CryptoBackend } from '@chat-app/shared'` resolves and the Metro bundler doesn't choke on the shared package's exports. We don't *use* the import — just type it so a compile failure surfaces here, not later.
Concretely: add `apps/mobile/lib/sharedSmoke.ts` that pulls a **runtime** value (not a type-only import — type imports are erased by the TS compiler and never reach Metro). `@chat-app/shared` re-exports its modules as namespaces (`crypto`, `auth`, `chat`, `friends`, `rtc`, `supabase`, `i18n`, `admin`), so we go through one of those:
```ts
// Force-imports a runtime symbol from @chat-app/shared so Metro
// actually resolves the package on app boot. A type-only import would
// be erased and never surface a packaging problem. This file goes away
// once Phase 1's real crypto adapter lands and registers the backend
// for real.
import { crypto } from '@chat-app/shared';
// Pull the function into a runtime reference so the bundle keeps the
// import. Calling it without a backend would throw, so we just retain
// the reference.
export const sharedSmoke = { register: crypto.setCryptoBackend };
```
Imported from `_layout.tsx` once, value unused. Phase 1 removes it.
### 6. README + scripts
`apps/mobile/README.md` gets a "Phase 0 quickstart" section that walks through `eas init` + the first development build + installing the dev client on a device.
Root `package.json` gets convenience scripts only if they're missing: `mobile:dev`, `mobile:ios`, `mobile:android`, `mobile:typecheck` — all `pnpm --filter @chat-app/mobile <cmd>` wrappers.
### 7. Typecheck + lint plumbing
Verify `pnpm --filter @chat-app/mobile typecheck` already runs (the mobile `package.json` already declares it). If `eslint` is wired similarly, add it to a CI workflow stub — but actual CI integration is out of scope here.
## File structure (after Phase 0)
```
apps/mobile/
├── app/
│ ├── _layout.tsx ← MODIFIED: SafeArea, GestureHandler, providers shell
│ ├── index.tsx ← MODIFIED: Netralax landing with version + nav link
│ └── (app)/
│ ├── _layout.tsx ← unchanged
│ └── chats.tsx ← MODIFIED: "coming soon" placeholder text
├── assets/
│ ├── icon.png ← REPLACED: Netralax 1024×1024
│ ├── adaptive-icon.png ← REPLACED: foreground for Android adaptive
│ └── splash.png ← REPLACED: Netralax splash
├── lib/
│ └── sharedSmoke.ts ← NEW: temporary import-canary
├── app.json ← MODIFIED: Netralax branding, ids
├── eas.json ← NEW: build profiles
├── package.json ← unchanged (deps stay)
└── README.md ← MODIFIED: Phase 0 quickstart
```
## Risks
- **`react-native-libsodium` + New Architecture (Fabric / TurboModules).** `newArchEnabled: true` is in `app.json`. The library claims support, but past RN libsodium ports broke around the JSI migration. If the dev client crashes on require, fall back to disabling new arch in Phase 0 and re-enable in Phase 1 once the adapter is wired and tested.
- **`@chat-app/shared` Node-only paths.** The package was authored against Node + browser; the supabase client uses `fetch` and `WebSocket` which RN polyfills. If any export pulls `node:crypto` directly the Metro resolver may fail. The smoke import (§5) is the canary for this.
- **EAS account access.** `eas init` requires an Expo account. The user owns it; we treat the project ID slot as an external input.
- **Apple Developer enrolment.** Phase 0 builds for iOS need an Apple ID; EAS can generate dev certs automatically the first time `eas build --platform ios` runs against the user's Apple ID. No paid enrolment needed for the development profile (free signing).
## Verification (run before declaring Phase 0 done)
1. `pnpm --filter @chat-app/mobile typecheck` → exit 0.
2. `pnpm --filter @chat-app/mobile dev` (`expo start --dev-client` once a dev client is installed) → app boots on a real iPhone *and* a real Android device.
3. Landing screen shows "Netralax" + a non-empty version string read at runtime.
4. Tapping the placeholder nav link routes into `/(app)/chats` without crash.
5. `eas build --platform android --profile development` produces an APK that installs and launches on a physical Android device.
6. `eas build --platform ios --profile development` produces a `.tar.gz` (simulator build) **OR** an `.ipa` (device build, if free signing succeeded).
7. Optional: visual check that the icon + splash render as Netralax (not Expo's default) on both platforms.
If any of those fail, Phase 0 isn't done — fix in this phase rather than rolling it into Phase 1.
## Out of scope
- Push notification token registration (Phase 1).
- Linking to the Supabase URL / env-var injection (Phase 1).
- Local SQLite schema (Phase 1).
- Any screen beyond Landing + Chats-coming-soon.
- Reanimated / gesture-based animations.
- Dark / light mode polish — system default is fine.