Native iOS & Android app that edits any @webhouse/cms server. Capacitor shell, React 19, JWT auth, QR pairing, push notifications.
What it is
webhouse.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.
It'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.
Stack
| Layer | Choice |
|---|---|
| Shell | Capacitor 8 (native iOS + Android) |
| UI | React 19 + Vite 5 + Tailwind v3 |
| Routing | wouter (1.5 KB) |
| State | TanStack Query |
| Animation | framer-motion 11 |
| Forms | React Hook Form + Zod |
| QR scanning | jsQR (via getUserMedia, no native plugin) |
| Biometric | @capgo/capacitor-native-biometric |
| Push | @capacitor/push-notifications + Firebase Cloud Messaging |
Not React Native, not Expo. Native-feeling via Capacitor, native performance on each platform, one codebase to maintain.
What's shipped
- Onboarding — user enters their own CMS server URL (BYO-server)
- QR pairing login — live camera scanner reads a 5-minute pairing token from the desktop admin, exchanges for JWT
- Email/password fallback —
/api/mobile/loginfor non-TOTP accounts - Biometric unlock — Face ID / Touch ID on 2nd launch, silent re-auth with stored JWT
- Home screen — avatar, org dropdown, site list
- Live preview — phone-safe preview for localhost dev servers via signed proxy (
/api/mobile/preview-proxy) - Content editing — pages, posts, collections. Richtext, image upload with AI analysis, relation picker, array/object editors
- Media browser — Photos-style thumbnail grid with AI analysis
- AI chat — conversational CMS access as a floating action button
- Push notifications — 6 topics, per-user topic preferences (build_failed, build_succeeded, agent_completed, curation_pending, link_check_failed, scheduled_publish)
- Swipe-back navigation — left-edge swipe to go back (native iOS feel)
- Settings — topic toggles, permission status, device management, sign out
Authentication
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).
Two login paths:
- 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. - Email/password —
POST /api/mobile/login, returns JWT + user profile.
Both routes validate on the same session path as web — no parallel auth system to maintain.
API surface
Every mobile endpoint lives under /api/mobile/* and validates Bearer JWT via getMobileSession(req).
| Route | Purpose | |
|---|---|---|
GET /api/mobile/ping | Server identity check (no auth — onboarding) | |
POST /api/mobile/login | Email/password → JWT | |
POST /api/mobile/pair | Issue 5-min QR pairing token (desktop session) | |
POST /api/mobile/pair/exchange | Exchange pairing token → JWT | |
GET /api/mobile/quick-pair | One-URL phone-safari pairing (auto-detects LAN IP) | |
GET /api/mobile/me | Authenticated user + sites + permissions | |
GET /api/mobile/preview-proxy?upstream=…&tok=… | Signed proxy for localhost preview | |
POST /api/mobile/push/register | Register FCM/APNs device token | |
POST /api/mobile/push/preferences | Topic opt-in/opt-out | |
POST /api/mobile/push/test | Test push delivery | |
| `GET\ | POST /api/mobile/content/*` | Fetch/edit docs, resolve collections |
POST /api/mobile/uploads | Media upload with auto-analysis | |
POST /api/mobile/chat/* | Chat history, memory, streaming |
All responses are JSON. No HTML redirects, no cookies, CORS-permissive for capacitor://localhost, https://localhost, ionic://localhost.
LAN IP pairing (dev)
A 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).
/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.
Push notifications
Six topics configured out of the box, each with per-user opt-in/opt-out:
| Topic | Default | Fires when |
|---|---|---|
build_failed | ON | Deploy fails |
build_succeeded | OFF | Deploy succeeds |
agent_completed | ON | Agent run finishes |
curation_pending | ON | New item in curation queue |
link_check_failed | ON | Broken link detected |
scheduled_publish | ON | Content auto-published |
Delivery 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.
Build & run
From the repo root:
pnpm install
pnpm webhouse.app:ios # boots iOS sim, builds Vite, cap syncs, opens app
pnpm webhouse.app:android # same for Android emulatorFallback names if pnpm rejects dots in aliases:
pnpm wha:ios
pnpm wha:androidInside packages/cms-mobile/:
pnpm build # Vite → dist/
pnpm cap:sync # sync native changes
pnpm dev # local Vite dev server (web debugging)
pnpm typecheck # TS validation
pnpm sim:login # auto-login helper for iOS simCurrent status
Phase 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.
A blocker before App Store submission: NSAllowsArbitraryLoads in Info.plist must be removed (HTTPS-only). The preflight-release.sh script gates on this.