Files
petfeed/mobile
admin 085d2abb53 feat(mobile): add post creation and comments
Post creation: gallery/camera picker (expo-image-picker) uploads each
image straight to Supabase Storage via a presigned PUT
(posts.getPresignedUrl + expo-file-system), then posts.create with the
resulting storage keys — reachable via a new "+" header button on the
Feed tab. No client-side blurhash (browser Canvas API, no RN
equivalent) or AI-disclosure/mention support yet, matching the rest of
mobile's plain-text captions.

Comments: a dedicated screen (posts/[postId]/comments) for reading,
posting, and deleting one's own comments, reachable by tapping the
comment icon on a PostCard. Ported from web's PostDetailDialog comment
panel, scoped down to just that panel since mobile's image full-view
already has its own route.
2026-09-05 15:35:32 +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, stories, pet profile detail, video playback, search, health, and message sending are still not built — see "What's not built yet" 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 screen
      (tabs)/
        _layout.tsx         # bottom Tabs: Feed/Explore/Notifications/Pets + header Messages button
        index.tsx           # Feed
        explore.tsx         # pet discovery grid
        notifications.tsx   # notifications list
        pets.tsx             # owner's pets + active-pet switcher
      messages/index.tsx    # conversations list
  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
    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
    theme/tokens.ts            # design tokens mirroring the web app's DESIGN.md
    components/                 # Button, TextField, Card, PostCard, PawButton, PetAvatar, PetIdentityBadges

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) 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:

  • Post creation/upload (camera/gallery → Supabase direct upload, per posts.getPresignedUrl/posts.create)
  • Comments (reading + posting — PostDetailDialog equivalent)
  • Repost creation (the count already renders, creating one doesn't)
  • Stories (story ring, viewer, creation)
  • Video playback (currently a static placeholder for type === "VIDEO" posts)
  • Search (search.pets/byHashtag/byOwner) — separate from the Explore tab
  • Health tracking, milestones
  • Sending messages (conversations list works; no chat detail/compose screen)
  • Push notifications
  • No i18n — the whole app is hardcoded German text, matching the sign-in screen