# Mobile Phase 1 — Auth + Chat MVP

**Date:** 2026-05-13
**Scope:** `apps/mobile`
**Roadmap context:** `2026-05-13-mobile-deployment-roadmap.md`

---

## Problem

Phase 0 delivered a Netralax-branded shell. It boots, renders a placeholder, and is configured for `eas build`, but the app has no actual functionality. To call mobile "deployment-ready with working features" the user must be able to log in, see their chats, and send a text message — the smallest end-to-end vertical slice that's also useful.

## Goal

After Phase 1, a Netralax user on iOS or Android can:

1. Open the app, enter their account email, request a magic link.
2. Tap the link in their email (deep link returns to `netralax://auth/callback`), complete the session, and land on the chat list.
3. See their existing conversations (DMs + groups) with last-message previews, decrypted on-device.
4. Open a conversation, see the message history (decrypted), and send a new text message that the desktop client receives correctly.
5. Log out from a settings entry, returning to the login screen.

## Non-goals

- Signup with invite code (login-only MVP — accounts are provisioned via desktop or admin scripts).
- Attachments, voice messages, reactions, edits (Phase 2).
- Calls (Phase 3).
- Push notifications (deferred — chat works without push, the user pulls fresh data on screen focus / pull-to-refresh).
- Realtime subscription on conversations (deferred). Phase 1 polls / refetches on screen focus.
- Read receipts, typing indicator, presence (Phase 2).
- Friend list, profile edit, settings beyond logout.

## Design

### 1. Theme constants

`apps/mobile/theme/colors.ts` exports a flat `colors` object with the hex codes Phase 0 inlined across files (`#0b0b0f`, `#9ca3af`, `#fff`, `#6b7280`, `#5865f2`) plus a couple of new ones we need for Phase 1 (input border, danger). Every Phase 1 screen imports from here; no new hex literals appear in components.

### 2. Crypto adapter (`apps/mobile/lib/cryptoBackend.ts`)

A near-mirror of `apps/desktop/src/lib/cryptoBackend.ts` but wrapping `react-native-libsodium` instead of `libsodium-wrappers-sumo`. The two libraries expose the same primitive API (the RN port re-exports the libsodium-wrappers types) — only the import + the absence of `_sodium.ready` differs. Synchronous (no WASM warm-up needed on a native lib). Registered at boot via `crypto.setCryptoBackend(createLibsodiumBackend())` from `_layout.tsx`.

### 3. Secret store adapter (`apps/mobile/lib/secretStore.ts`)

Implements `SecretStore` (from `@chat-app/shared/auth/secure-storage`) backed by `expo-secure-store`. Per the contract, values are `Uint8Array`; we base64-encode at the boundary because `SecureStore` only accepts strings. `buffer` polyfill is part of RN's base set, no extra install.

### 4. Supabase session storage (`apps/mobile/lib/sessionStorage.ts`)

Supabase's JS client needs an async-storage object for session tokens. Use `@react-native-async-storage/async-storage` (new dep) — supabase-js v2 accepts its `getItem` / `setItem` / `removeItem` API directly.

### 5. Env config (`apps/mobile/lib/env.ts`)

Expo exposes vars prefixed `EXPO_PUBLIC_*` to the bundle via `process.env`. Read three:

- `EXPO_PUBLIC_SUPABASE_URL`
- `EXPO_PUBLIC_SUPABASE_ANON_KEY`
- `EXPO_PUBLIC_AUTH_REDIRECT_URL` — defaults to `netralax://auth/callback`.

`apps/mobile/.env.example` is added with placeholder values + a README pointer.

### 6. Supabase client (`apps/mobile/lib/supabase.ts`)

Mirrors desktop's `apps/desktop/src/lib/supabase.ts`. Calls `createClient` from `@chat-app/shared/supabase` with the mobile `sessionStorage` adapter and `detectSessionInUrl: false` (the callback screen parses the URL manually).

### 7. Auth context (`apps/mobile/lib/authContext.tsx`)

React Context provider that wraps the authenticated tree. Exposes:

- `session: Session | null`
- `user: User | null`
- `device: DeviceRecord | null` — the registered device for this install
- `loading: boolean` — true while hydrating from AsyncStorage on boot
- `signOut(): Promise<void>`

On mount:

1. `await supabase.auth.getSession()` — pulls from AsyncStorage.
2. If a session exists, derive the device by `listOwnDevices(supabase)` and matching on a locally-stored device id (in `secretStore` under key `device.id`).
3. If session exists but no device record yet (fresh install, account exists on other devices), call `registerDevice` with a fresh key pair, store the new device id + private key in `secretStore`.

The provider subscribes to `supabase.auth.onAuthStateChange` so signing out from anywhere updates the tree.

### 8. Route gating

- `app/index.tsx`: if `session` is set, redirect to `/(app)/chats`. Otherwise show the login form.
- `app/(app)/_layout.tsx`: if no `session`, redirect to `/`. (Existing file: change from "always render Stack" to "guard on session".)

### 9. Login screen (`app/index.tsx`)

Replaces the Phase-0 placeholder. Renders:

- Netralax wordmark.
- Email `TextInput`.
- "Magic Link senden" button → `auth.loginWithMagicLink(supabase, email, env.authRedirectUrl)`.
- After tap: show "Check deine Mails" state until the deep link fires.
- Error banner on failure.

### 10. Auth callback (`app/auth/callback.tsx`)

A new screen reachable via the `netralax://auth/callback` deep link. Reads URL params (Supabase magic-link callback puts `access_token` + `refresh_token` in the URL hash or query), calls `supabase.auth.setSession({...})`, routes to `/(app)/chats`. On failure, routes back to `/` with an error message.

Expo Router auto-handles the deep link → screen mapping when the scheme in `app.json` matches and the path matches the route file.

### 11. Conversation list (`app/(app)/chats.tsx`)

Replaces the Phase-0 placeholder. Uses `chat.listConversations(supabase)` from `@chat-app/shared/chat`. Renders a `FlatList` of `ConversationRow` (new component):

- DM: peer's display name + Avatar (initial-letter circle).
- Group: group name + "N Mitglieder".
- Last message preview — decrypted via `decryptMessages` if encrypted, else "…".
- Timestamp (relative — "Vor 5 Min").

Pull-to-refresh refetches.

Tap on row → navigates to `/(app)/conversations/[id]`.

A small icon-button in the header opens a logout modal calling `signOut()`.

### 12. Conversation detail (`app/(app)/conversations/[id].tsx`)

Loads:

- `fetchConversationMessages(supabase, { conversationId, limit: 50 })`.
- `listConversationDeviceKeys(supabase, conversationId)` for decryption.
- `decryptMessages({ messages, deviceKeys, myDeviceId, myPrivateKey })` to get plaintext.

Renders an inverted `FlatList` (newest at the bottom, like Discord). Each row shows sender name + body + timestamp. Attachments rendered as "📎 [Anhang]" placeholders — Phase 2 adds real rendering.

Bottom: a `TextInput` + send button. On send: `sendEncryptedMessage`. Optimistically appends to local state, then refetches.

### 13. Cleanup

- Delete `apps/mobile/lib/sharedSmoke.ts` and the `void sharedSmoke` import in `_layout.tsx`.

## File structure (after Phase 1)

```
apps/mobile/
├── app/
│   ├── _layout.tsx           ← MODIFIED: register crypto backend, mount AuthProvider
│   ├── index.tsx             ← MODIFIED: Login screen (magic link form)
│   ├── auth/
│   │   └── callback.tsx      ← NEW: deep-link landing
│   └── (app)/
│       ├── _layout.tsx       ← MODIFIED: session-gated stack
│       ├── chats.tsx         ← MODIFIED: real conversation list + logout
│       └── conversations/
│           └── [id].tsx      ← NEW: conversation view + send
├── components/
│   ├── ErrorBoundary.tsx     ← unchanged
│   ├── Avatar.tsx            ← NEW: initial-letter avatar circle
│   └── ConversationRow.tsx   ← NEW: list row
├── lib/
│   ├── authContext.tsx       ← NEW
│   ├── cryptoBackend.ts      ← NEW
│   ├── env.ts                ← NEW
│   ├── secretStore.ts        ← NEW
│   ├── sessionStorage.ts     ← NEW
│   ├── supabase.ts           ← NEW
│   ├── timeFormat.ts         ← NEW
│   └── sharedSmoke.ts        ← DELETED
├── theme/
│   └── colors.ts             ← NEW
├── .env.example              ← NEW
└── package.json              ← MODIFIED: + @react-native-async-storage/async-storage
```

## Risks

- **`react-native-libsodium` New Architecture compatibility.** v1.7 claims Fabric/TurboModule support. If `setCryptoBackend(createLibsodiumBackend())` crashes on app boot under New Arch, fall back to `"newArchEnabled": false` and re-evaluate.
- **`@chat-app/shared` ESM resolution under Metro.** Shared emits ESM with `.js` import specifiers. Metro's default resolver handles this; if it doesn't, set `metro.config.js`'s `resolver.unstable_enablePackageExports: true`.
- **Magic-link deep-link return.** Expo Router auto-maps `netralax://auth/callback` to `app/auth/callback.tsx` since the scheme in `app.json` matches (`netralax`) and `expo-linking` is in the prebuild.
- **Device key persistence.** The private key blob lives in `expo-secure-store` (Keychain on iOS, Keystore on Android). Uninstalling the app wipes the key — same as desktop. Backup/restore is post-Phase-4.

## Verification (manual on real hardware after typecheck passes)

1. `pnpm --filter @chat-app/mobile typecheck` → exit 0.
2. `.env.local` populated with the same `SUPABASE_URL` / `SUPABASE_ANON_KEY` the desktop uses → app boots, shows Login.
3. Enter an existing-user email → "Magic Link senden" succeeds → "Check deine Mails" state appears.
4. Open the email link on the device → callback URL fires Expo Router → app navigates to `/(app)/chats`.
5. Chat list loads; existing DMs and groups from the desktop are visible with decrypted previews.
6. Tap a conversation → detail screen opens, history loads, decrypted message bodies render newest-at-bottom.
7. Type "test from mobile" → send → desktop client of the same account sees the new message via Supabase realtime.
8. Pull-to-refresh on chat list re-fetches.
9. Tap settings → "Abmelden" → returns to Login screen, session cleared from AsyncStorage.

## Out of scope

- Push notifications (`expo-notifications` + `notify-push` edge function).
- Realtime subscription on the mobile side (relies on focus-refetch).
- Conversation creation (new DM / new group) from mobile.
- Attachments, voice messages, reactions, edits, replies, forwards (Phase 2).
- Read receipts, typing, delivery state (Phase 2).
- Voice/video calls (Phase 3).
- Backup/restore device keys (post-Phase-4).
- Profile editing, friends, admin tools (post-Phase-4).
