Files
petfeed/mobile
..

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: scaffold only. 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.

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

cd mobile
npm install
cp .env.example .env   # already done locally with the live Clerk publishable key
npm run android         # or: npx expo start, then press 'a'

Requires an Android emulator (Android Studio) or a physical device with Expo Go / a dev build. 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.

.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.

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 + tRPC/QueryClient providers + auth redirect
    (auth)/
      sign-in.tsx           # minimal email/password sign-in
    (app)/
      index.tsx             # placeholder home screen — proves the backend connection
  src/
    api/trpc.ts             # tRPC client, AppRouter type import, query client factory
    lib/env.ts               # typed EXPO_PUBLIC_* access
    lib/tokenCache.ts        # Clerk session persistence via expo-secure-store
    components/              # shared UI components (empty so far)

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

This is a connectivity + auth scaffold, not a feature-complete app. No pet feed, no navigation beyond the placeholder home screen, no push notifications, no camera/media upload flow. Next steps belong in a planning session once the scaffold itself is confirmed working end-to-end on a real Android device/emulator.