Files
petfeed/mobile
adminandClaude Sonnet 5 fcb5ae3eef chore(mobile): add eas.json build profile config
Minimal EAS Build config (development/preview/production profiles) —
needed by `eas credentials` to manage the FCM push key, no EAS Build
runs yet. Push notifications are now fully verified end-to-end: real
device token minted, registered with the backend, and a live push
delivered to the Android emulator's notification tray.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 14:27:13 +02:00
..

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 <token> 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<AppRouter> 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 <Tabs> (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.

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="<path to a JDK 17 install>" 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<AppRouter> 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.