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.tsxredirects 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-expois deprecated, don't reinstall it). Session JWT persists in the OS keystore viaexpo-secure-store(src/lib/tokenCache.ts) instead of Clerk's in-memory default. - API:
src/api/trpc.tscreates a@trpc/clientpointed at${EXPO_PUBLIC_API_URL}/api/trpc, with the Clerk session token attached asAuthorization: Bearer <token>on every request (useAuth().getToken, called per-request so a refreshed token is always used). - Type safety:
AppRouteris 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 becausemobile/is nested inside the same repo: Node's (and TypeScript's) module resolution walks up to the rootnode_modulesfor@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:
- CORS doesn't apply. CORS is a browser same-origin mechanism; a native
app's
fetchisn't a browser context, so the "just needs to reach it" requirement was already satisfied by the API being reachable over HTTPS atpawfeed.org. - Clerk already accepts bearer tokens.
src/proxy.tsalready marks/api/trpc/(.*)as a public route at the middleware level (tRPC does its own per-procedure auth — see the comment there), andauth()insrc/trpc/init.tsrecognizes both the browser's session cookie AND anAuthorization: BearerJWT 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'sandroid.package(org.pawfeed.app) is set now;ios.bundleIdentifieris 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.