{
  "slug": "mobile-app",
  "title": "Mobile app (F07)",
  "description": "Native iOS & Android app that edits any @webhouse/cms server. Capacitor shell, React 19, JWT auth, QR pairing, push notifications.",
  "category": "guides",
  "order": 18,
  "locale": "en",
  "translationGroup": "d574cead-8047-452f-aa38-72a3940935d8",
  "helpCardId": null,
  "content": "## What it is\n\nwebhouse.app is a native mobile companion for the CMS. It edits content, browses media, runs chat, and receives push notifications — against any @webhouse/cms server you point it at. One TypeScript codebase compiles to iOS IPA and Android AAB via Capacitor 8.\n\nIt's a **server-agnostic** client: the app talks to `/api/mobile/*` endpoints via Bearer JWT and makes no assumptions about a specific bundle id, server version, or brand. You (or a whitelabel reseller) can repackage the same shell under a different name — cms-admin won't notice.\n\n## Stack\n\n| Layer | Choice |\n|---|---|\n| Shell | Capacitor 8 (native iOS + Android) |\n| UI | React 19 + Vite 5 + Tailwind v3 |\n| Routing | wouter (1.5 KB) |\n| State | TanStack Query |\n| Animation | framer-motion 11 |\n| Forms | React Hook Form + Zod |\n| QR scanning | jsQR (via getUserMedia, no native plugin) |\n| Biometric | @capgo/capacitor-native-biometric |\n| Push | @capacitor/push-notifications + Firebase Cloud Messaging |\n\nNot React Native, not Expo. Native-feeling via Capacitor, native performance on each platform, one codebase to maintain.\n\n## What's shipped\n\n- **Onboarding** — user enters their own CMS server URL (BYO-server)\n- **QR pairing login** — live camera scanner reads a 5-minute pairing token from the desktop admin, exchanges for JWT\n- **Email/password fallback** — `/api/mobile/login` for non-TOTP accounts\n- **Biometric unlock** — Face ID / Touch ID on 2nd launch, silent re-auth with stored JWT\n- **Home screen** — avatar, org dropdown, site list\n- **Live preview** — phone-safe preview for localhost dev servers via signed proxy (`/api/mobile/preview-proxy`)\n- **Content editing** — pages, posts, collections. Richtext, image upload with AI analysis, relation picker, array/object editors\n- **Media browser** — Photos-style thumbnail grid with AI analysis\n- **AI chat** — conversational CMS access as a floating action button\n- **Push notifications** — 6 topics, per-user topic preferences (build_failed, build_succeeded, agent_completed, curation_pending, link_check_failed, scheduled_publish)\n- **Swipe-back navigation** — left-edge swipe to go back (native iOS feel)\n- **Settings** — topic toggles, permission status, device management, sign out\n\n## Authentication\n\n**Bearer JWT in the `Authorization` header — never cookies.** The token is minted by the same `createToken(user)` helper as the web admin, so the audit trail is unified. Tokens are stored in Capacitor Preferences (iOS Keychain / Android EncryptedSharedPreferences).\n\nTwo login paths:\n\n- **QR pairing** — scan a QR from the desktop admin, app calls `POST /api/mobile/pair/exchange`, gets a JWT back in one step. Works with TOTP-protected accounts because the desktop has already done the full 2FA flow.\n- **Email/password** — `POST /api/mobile/login`, returns JWT + user profile.\n\nBoth routes validate on the same session path as web — no parallel auth system to maintain.\n\n## API surface\n\nEvery mobile endpoint lives under `/api/mobile/*` and validates Bearer JWT via `getMobileSession(req)`.\n\n| Route | Purpose |\n|---|---|\n| `GET  /api/mobile/ping` | Server identity check (no auth — onboarding) |\n| `POST /api/mobile/login` | Email/password → JWT |\n| `POST /api/mobile/pair` | Issue 5-min QR pairing token (desktop session) |\n| `POST /api/mobile/pair/exchange` | Exchange pairing token → JWT |\n| `GET  /api/mobile/quick-pair` | One-URL phone-safari pairing (auto-detects LAN IP) |\n| `GET  /api/mobile/me` | Authenticated user + sites + permissions |\n| `GET  /api/mobile/preview-proxy?upstream=…&tok=…` | Signed proxy for localhost preview |\n| `POST /api/mobile/push/register` | Register FCM/APNs device token |\n| `POST /api/mobile/push/preferences` | Topic opt-in/opt-out |\n| `POST /api/mobile/push/test` | Test push delivery |\n| `GET\\|POST /api/mobile/content/*` | Fetch/edit docs, resolve collections |\n| `POST /api/mobile/uploads` | Media upload with auto-analysis |\n| `POST /api/mobile/chat/*` | Chat history, memory, streaming |\n\nAll responses are JSON. No HTML redirects, no cookies, CORS-permissive for `capacitor://localhost`, `https://localhost`, `ionic://localhost`.\n\n## LAN IP pairing (dev)\n\nA subtle detail that saves a lot of time. During local development the desktop runs on `https://localhost:3010`. A phone on the same wifi can't reach localhost — it needs your Mac's LAN IP (e.g. `192.168.1.42`).\n\n`/api/mobile/pair` and `/api/mobile/me` auto-detect the Mac's first non-loopback IPv4 and rewrite server URLs in the response. The phone gets `https://192.168.1.42:3010` without any manual configuration. `/api/mobile/quick-pair` bakes this into the emitted deep link so a single tap on the phone pairs both the LAN IP and the JWT.\n\n## Push notifications\n\nSix topics configured out of the box, each with per-user opt-in/opt-out:\n\n| Topic | Default | Fires when |\n|---|---|---|\n| `build_failed` | ON | Deploy fails |\n| `build_succeeded` | OFF | Deploy succeeds |\n| `agent_completed` | ON | Agent run finishes |\n| `curation_pending` | ON | New item in curation queue |\n| `link_check_failed` | ON | Broken link detected |\n| `scheduled_publish` | ON | Content auto-published |\n\nDelivery via Firebase Cloud Messaging (iOS + Android). The device calls `POST /api/mobile/push/register` after the OS grants a token. Tokens are stored per-user in `_data/device_tokens.json`.\n\n## Build & run\n\nFrom the repo root:\n\n```bash\npnpm install\npnpm webhouse.app:ios       # boots iOS sim, builds Vite, cap syncs, opens app\npnpm webhouse.app:android   # same for Android emulator\n```\n\nFallback names if pnpm rejects dots in aliases:\n\n```bash\npnpm wha:ios\npnpm wha:android\n```\n\nInside `packages/cms-mobile/`:\n\n```bash\npnpm build            # Vite → dist/\npnpm cap:sync         # sync native changes\npnpm dev              # local Vite dev server (web debugging)\npnpm typecheck        # TS validation\npnpm sim:login        # auto-login helper for iOS sim\n```\n\n## Current status\n\nPhase 3+ is shipped — content editing, media, chat, push, QR login, LAN pairing all live in the dev channel. **Not yet in the App Store or Play Store.** Phase 8 is the TestFlight beta + public release.\n\nA blocker before App Store submission: `NSAllowsArbitraryLoads` in `Info.plist` must be removed (HTTPS-only). The `preflight-release.sh` script gates on this.",
  "excerpt": "What it is\n\nwebhouse.app is a native mobile companion for the CMS. It edits content, browses media, runs chat, and receives push notifications — against any @webhouse/cms server you point it at. One TypeScript codebase compiles to iOS IPA and Android AAB via Capacitor 8.\n\nIt's a server-agnostic clie",
  "seo": {
    "metaTitle": "Mobile app (F07) — webhouse.app Docs",
    "metaDescription": "Native iOS & Android CMS client built with Capacitor. QR pairing, push notifications, chat, content editing against any @webhouse/cms server.",
    "keywords": [
      "webhouse",
      "cms",
      "mobile",
      "ios",
      "android",
      "capacitor",
      "jwt",
      "push-notifications",
      "qr-login",
      "f07"
    ]
  },
  "createdAt": "2026-04-15T20:45:00.000Z",
  "updatedAt": "2026-04-15T20:45:00.000Z"
}