# PawFeed Mobile Native Android/iOS client for PawFeed, built with **React Native + Expo (Expo Router)**. It talks to the exact same backend the web app uses — the self-hosted Docker deployment described in `../docker/SCALING-ROADMAP.md` — over the existing tRPC API. **No backend/Docker changes were needed to make this possible**, see "Why no backend changes" below. Decided over Capacitor (see the now-superseded `../mobil.md`) because a native RN app gives better performance/feel and can still share real logic with a future web-shared package, at the cost of more upfront work — see the architecture-decision thread in this session's handoff. **Status:** Phase 1 — real Feed/Explore/Notifications/Pets tabs against the live backend, not just a connectivity scaffold anymore. Post creation, comments, repost, pet profile detail (view/follow/edit/followers-following), stories (tray/creation/viewer), video playback, search, health tracking (consent gate + weight/vet/vaccine logs), milestones, and message sending/receiving are done and verified end-to-end against the live backend. Push notifications are wired end-to-end but not yet deliverable — see "Push notifications" below. Android is the priority platform; iOS builds need a Mac + Apple Developer account and come later (same codebase, no separate source tree — see "Android-first, then iOS" below). ## Architecture - **Routing:** Expo Router (file-based, `app/`). `(auth)` and `(app)` are route groups; `app/_layout.tsx` redirects between them based on Clerk's session state (`useProtectedRoute`), mirroring Clerk's official expo-router integration pattern. - **Auth:** `@clerk/expo` (the current SDK — `@clerk/clerk-expo` is deprecated, don't reinstall it). Session JWT persists in the OS keystore via `expo-secure-store` (`src/lib/tokenCache.ts`) instead of Clerk's in-memory default. - **API:** `src/api/trpc.ts` creates a `@trpc/client` pointed at `${EXPO_PUBLIC_API_URL}/api/trpc`, with the Clerk session token attached as `Authorization: Bearer ` on every request (`useAuth().getToken`, called per-request so a refreshed token is always used). - **Type safety:** `AppRouter` is imported as a **type-only** import directly from the web app's `../src/trpc/routers/_app.ts` — no code duplication, no publishing a package. This works because `mobile/` is nested inside the same repo: Node's (and TypeScript's) module resolution walks up to the root `node_modules` for `@trpc/server`, `zod`, etc., so the type import resolves without installing those packages a second time here. If a future refactor turns this repo into an npm workspace, revisit this — it wasn't necessary for the initial scaffold. `src/api/types.ts` centralizes the `inferRouterOutputs` types every screen uses (`Pet`, `FeedPost`, etc.) instead of repeating the long relative import. - **Design system:** `src/theme/tokens.ts` mirrors the web app's `DESIGN.md` ("Spotlight Porch" — marigold-orange accent, porch-white canvas, Inter font, hairline rings instead of shadows). `src/components/` has the shared building blocks (`Button`, `TextField`, `Card`) plus feed-specific ports of the web equivalents (`PostCard`, `PawButton`, `PetAvatar`, `PetIdentityBadges`). - **Active pet:** every pet-scoped procedure (`feed.getFeed`, `reactions.toggle`, `notifications.list`, ...) takes an explicit `petId` and asserts ownership server-side — there's no server-side "current pet". `src/context/ActivePetContext.tsx` is mobile's equivalent of web's `ActivePetContext` (MMKV-backed instead of `localStorage`), defaulting to the owner's first pet; the Pets tab doubles as the pet switcher. - **Fast reopen:** the TanStack Query cache is persisted to `react-native-mmkv` (`src/lib/storage.ts`, wired via `PersistQueryClientProvider` in `app/_layout.tsx`) so the last-known feed/pets/notifications render immediately on relaunch while a background refetch reconciles them — no blank screen while waiting on the network. Images go through `expo-image` for the same reason (disk-cached, unlike RN's built-in `Image`). - **Navigation:** `expo-router`'s built-in `` (`app/(app)/(tabs)/_layout.tsx`) — Feed/Explore/Notifications/Pets, mirroring the web app's actual mobile bottom nav (`src/components/layout/MobileNav.tsx`), not an invented IA. Messages is a header icon button pushing `app/(app)/messages/index.tsx`, matching web (there it's a floating corner button, not a tab). ### Why no backend changes were needed Two things had to be true for the app to reach the Docker-hosted backend as-is, and both already were: 1. **CORS doesn't apply.** CORS is a browser same-origin mechanism; a native app's `fetch` isn't a browser context, so the "just needs to reach it" requirement was already satisfied by the API being reachable over HTTPS at `pawfeed.org`. 2. **Clerk already accepts bearer tokens.** `src/proxy.ts` already marks `/api/trpc/(.*)` as a public route at the middleware level (tRPC does its own per-procedure auth — see the comment there), and `auth()` in `src/trpc/init.ts` recognizes both the browser's session cookie AND an `Authorization: Bearer` JWT out of the box. Native/Expo apps sending a bearer token is Clerk's documented mobile auth flow, not a special case that needed enabling server-side. ## Setup **Runs as a custom dev client, not Expo Go.** `@clerk/expo`'s Device Trust flow (see below) and `react-native-mmkv` both need native code Expo Go doesn't ship, so this app builds and installs its own native shell locally — no EAS account/login needed, everything runs on-device via the Android SDK already required for the emulator. ```bash cd mobile npm install cp .env.example .env # already done locally with the live Clerk publishable key # First time only (or after adding a native dependency / editing app.json): npx expo prebuild --platform android JAVA_HOME="" npx expo run:android ``` `JAVA_HOME` matters: RN 0.86's Gradle build needs **JDK 17**, and a JDK that's too new (e.g. a bundled Android Studio JBR on JDK 21+) fails native module CMake configuration with a cryptic "restricted method... System" error. Eclipse Temurin 17 works (`winget install EclipseAdoptium.Temurin.17.JDK` on Windows). Once the native app is installed, day-to-day iteration doesn't need another `expo run:android` — just `npm start` (→ `expo start --dev-client`) and reopen the already-installed app; that command only bundles/serves JS. Requires an Android emulator (Android Studio) or a physical device. See https://docs.expo.dev/versions/v57.0.0/ for current Expo APIs — SDK 57 changed things vs. older Expo docs/training data, per `AGENTS.md` in this folder. ### Clerk dashboard: Native Applications API Clerk's "Device Trust" feature (tied to the dashboard's Native Applications API) makes `signIn.status` come back as `needs_client_trust` on a sign-in from a new device — resolved via `signIn.mfa.sendEmailCode()` / `verifyEmailCode()` in `app/(auth)/sign-in.tsx`, a one-time email-code check per device, independent of any account-level MFA setting. If sign-in hangs at "Clerk lädt…" forever with no error, or `signIn.status` never reaches `complete`, check that the Native Applications API is enabled for this Clerk instance. ### `.env` | Var | Purpose | |---|---| | `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` | Same Clerk instance as the web app — copy from root `.env.local`'s `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`. Safe to embed client-side (that's what "publishable" means). | | `EXPO_PUBLIC_API_URL` | Backend origin. Defaults to `https://pawfeed.org` (the live deployment) — see the LAN-IP / Android-emulator alternatives in `.env.example` for local dev against `npm run dev` instead. | | `EXPO_PUBLIC_SUPABASE_URL` | Same Supabase project as the web app — copy from root `.env.local`'s `NEXT_PUBLIC_SUPABASE_URL`. Used to build public media URLs (`src/lib/media-url.ts`); safe client-side, it's the public storage origin, not a service key. | **Caution:** the default `.env` points at the **live production** backend and database — the same one real users are on ([[project_prod_launch_2026-08-10]]). Reading (public stats, browsing) is harmless. Anything that writes data while iterating on mobile features should point `EXPO_PUBLIC_API_URL` at a local `npm run dev` instead (`http://10.0.2.2:3000` from the Android emulator, or the dev machine's LAN IP from a physical device). ## Folder structure ``` mobile/ app/ # expo-router routes _layout.tsx # ClerkProvider + persisted QueryClient/tRPC + ActivePetProvider + auth redirect (auth)/ sign-in.tsx # email/password + Clerk Device Trust email-code step (app)/ _layout.tsx # Stack hosting the (tabs) group + the messages/search/story/comment screens (tabs)/ _layout.tsx # bottom Tabs: Feed/Explore/Notifications/Pets + header Search/Messages buttons index.tsx # Feed (StoryTray header + infinite post list) explore.tsx # pet discovery grid notifications.tsx # notifications list pets.tsx # owner's pets + active-pet switcher messages/index.tsx # conversations list messages/[conversationId].tsx # chat detail/compose, polls messages.list while focused (no realtime) search.tsx # pets/people/hashtags search, three tabs over one debounced query post-new.tsx # create a photo post (pick/upload → posts.create) story-new.tsx # create a photo story (pick/upload → stories.create) story-viewer.tsx # full-screen story playback (tap zones, progress bars, view tracking) milestone-new.tsx # create a milestone post (type picker + optional photo → milestones.create) pets/[petId]/health/index.tsx # weight/vet/vaccine logs, gated by HealthConsentGate pets/[petId]/milestones.tsx # a pet's milestone posts (milestones.listByPet) posts/[postId]/comments.tsx # read/write/delete comments for a post image-viewer.tsx # full-screen tapped-image lightbox src/ api/trpc.ts # tRPC client, AppRouter type import, query client factory api/types.ts # inferRouterOutputs shortcuts (Pet, FeedPost, ...) context/ActivePetContext.tsx # MMKV-backed active-pet state hooks/useDebounce.ts # generic debounce hook (search input) hooks/usePushNotifications.ts # permission + Expo push token registration (owner.registerPushToken) lib/env.ts # typed EXPO_PUBLIC_* access lib/tokenCache.ts # Clerk session persistence via expo-secure-store lib/storage.ts # MMKV instance + TanStack Query sync persister lib/media-url.ts # Supabase public storage URL builder lib/format.ts # pet display name + relative-time formatting lib/sponsor.ts # active-sponsor check lib/upload.ts # direct PUT of a picked image to a presigned Supabase URL lib/milestone-meta.ts # MilestoneType → German label/icon map theme/tokens.ts # design tokens mirroring the web app's DESIGN.md components/ # Button, TextField, Card, PostCard, PawButton, PetAvatar, # PetIdentityBadges, FollowButton, StoryTray, PawRing, VideoPlayer components/health/ # HealthConsentGate, WeightLogSection, VetVisitSection, VaccineSection ``` ## Android-first, then iOS Both platforms build from this one Expo codebase (`expo run:android` / `expo run:ios`) — there's no separate iOS source tree to scaffold separately. "Starting with Android" means: - `app.json`'s `android.package` (`org.pawfeed.app`) is set now; `ios.bundleIdentifier` is set to the same reverse-DNS id but untested. - Android needs a Google Play Console account (~25 € one-time, already budgeted in `../mobil.md`) before an internal test track can be created. - iOS needs a Mac (for `expo run:ios` / EAS builds with credentials) and an Apple Developer account (99 €/year) — deferred until Android is validated. - EAS Build (`eas.json`, not yet added) is the recommended path for producing signed Android/iOS binaries without a local Android Studio/Xcode toolchain; add it when a first real build is needed. ## What's not built yet Phase 1 (Feed/Explore/Notifications/Pets tabs, reactions, active-pet switching, persisted cache for fast reopen) plus a full pet profile (view/follow/edit/followers-following), post creation/upload, comments, repost ("Paw-Back") creation, stories (tray/creation/viewer via `expo-video`-adjacent `expo-image`, plus `PawRing` seen/unseen indicator), video playback (`expo-video` against Mux HLS, polling while `PROCESSING`), search (pets/people/hashtags, `search.pets`/`byOwner`/`byHashtag`), health tracking (consent gate + weight/vet visit/vaccine logs — feeding info, emergency vet, and the shareable Health Card are web-only for now), milestones (view + create, restricted to the 5 user-initiated types), and messaging (conversation list + chat detail/compose, polling while the screen is focused since messages.ts has no realtime mechanism) are done and verified end-to-end against the live backend on a real Android emulator. Still missing, roughly in the order they'd unblock the most: - No i18n — the whole app is hardcoded German text, matching the sign-in screen ### Push notifications — wired but not yet deliverable `usePushNotifications` (in `app/_layout.tsx`) requests notification permission and registers the resulting Expo push token with the backend (`owner.registerPushToken`); `src/lib/notify.ts` on the web side sends a best-effort push through Expo's push API alongside every in-app Notification it creates (follow, reaction, comment, message, ...). Minting a real token needs an **EAS project id** (`app.json`'s `extra.eas.projectId`, set once via `eas init` — free, no EAS Build required, but does need an Expo account login). Until that's done, the permission prompt shows and is granted, but `getExpoPushTokenAsync` has nothing to attach to and the hook no-ops with a console warning (`[push] No EAS project id configured...`) — confirmed via `adb logcat` on 2026-09-06. No Owner row has an `expoPushToken` set until this is run.