For a cross platform mobile app using Expo with the managed workflow, Expo Router for file based navigation, and EAS for builds and updates. It assumes TypeScript, React Query for server state, and native permissions handled through Expo modules. Use it when platform differences and build config are the main hazards.
# AGENTS.md
This is a cross platform mobile app built with Expo, Expo Router, and TypeScript.
## Project
Stack: Expo SDK 52, React Native, TypeScript (strict), Expo Router, React Query, Zustand for local state, EAS Build and EAS Update.
Layout:
- `app/` Expo Router routes, the directory tree is the navigation tree
- `app/(tabs)/` bottom tab navigator, `app/(auth)/` unauthenticated stack
- `app/_layout.tsx` root providers, `app/+not-found.tsx` fallback
- `src/components/` shared presentational components
- `src/features/<feature>/` screens, hooks, and API calls for one feature
- `src/api/` typed fetch clients and React Query hooks
- `src/native/` thin wrappers around Expo modules (camera, notifications, storage)
- `src/theme/` spacing, colors, typography tokens
- `app.config.ts` app identity, plugins, permissions
- `eas.json` build profiles
Entry points: `app/_layout.tsx`, `app.config.ts`, `eas.json`.
## Commands
```bash
pnpm install
pnpm start # metro bundler
pnpm ios # run on iOS simulator
pnpm android # run on Android emulator
pnpm lint
pnpm typecheck # tsc --noEmit
pnpm test # jest with jest-expo
npx expo install <package> # install a package at the SDK compatible version
npx expo prebuild --clean # regenerate native projects, ask first
npx expo-doctor # diagnose config and version drift
eas build --profile development --platform ios
eas build --profile production --platform all
eas update --branch preview # OTA update, JS only
```
Use `npx expo install` for any package with a native component, not `pnpm add`.
A change to `app.config.ts` plugins, permissions, or a new native dependency requires a new development build. JavaScript only changes reload in the existing build.
## Platform notes
- Test every screen on both an iOS simulator and an Android emulator before calling it done.
- Safe areas: use `useSafeAreaInsets` from react-native-safe-area-context. Do not hardcode status bar height.
- Android back button needs explicit handling on any screen with a modal or a multi step flow.
- Permissions are requested at the moment of use, never on app launch. Both platforms need a usage description in `app.config.ts`.
- Keyboard behavior differs: use `KeyboardAvoidingView` with `behavior="padding"` on iOS and `"height"` on Android.
- Shadows need `elevation` on Android and `shadow*` props on iOS. Use the helper in `src/theme/shadows.ts`.
- Fonts and icons load asynchronously, keep the splash screen up until `useFonts` resolves.
- OTA updates cannot ship native changes. If a change touches native code it needs a store build.
## Code style
- Styles go in `StyleSheet.create` at the bottom of the file. No inline style objects inside render.
- Use `FlashList` or `FlatList` for any list that can exceed ten items. Never map an array into a ScrollView.
- Every list item component is memoized and every list has a stable `keyExtractor`.
- Server state belongs to React Query. Local UI state belongs to component state or Zustand. Do not mirror server data in a store.
- Navigation uses typed routes from Expo Router. No string concatenation for hrefs.
- Native APIs are only called through `src/native/`, so permissions and fallbacks live in one place.
- Spacing and color come from `src/theme/`. No raw hex values or magic numbers in components.
- Images use `expo-image` with an explicit width and height.
## Boundaries
Do not touch without explicit instruction:
- `ios/` and `android/` directories. They are generated by prebuild.
- `app.config.ts` bundle identifier, package name, scheme, version, or build number.
- `eas.json` build profiles and any signing credential.
- `pnpm-lock.yaml`, `.env`, store metadata and screenshots.
Needs human review: new native dependencies, permission additions, changes to push notification handling, and anything affecting deep links.
Version numbers are set by the release process, not by a code change.
## Testing
- `pnpm test` runs Jest with the jest-expo preset.
- Required tests: everything in `src/api/` (request shaping, response parsing), pure helpers, and state reducers.
- Component tests use @testing-library/react-native and cover behavior, not layout.
- Native module wrappers are tested with the Expo module mocked.
- Interaction and layout are verified on device, not in the test suite.
## Git workflow
- Branch from `main`: `feat/short-description`, `fix/short-description`.
- Conventional commits: `fix(onboarding): keep keyboard clear of the submit button`.
- PR description includes screenshots or a screen recording from both iOS and Android.
- State in the PR whether the change is OTA safe or requires a new build.
- Never commit to `main`.
Expo Router puts navigation in the filesystem, so knowing which directory maps to which navigator is essential before touching any screen. Listing where hooks, native wrappers, and app config live keeps platform code from leaking into shared components. It also identifies app.config.ts as the file that controls the build.
Mobile has two kinds of run: the JavaScript reload and a native rebuild, and confusing them wastes a lot of time. Documenting when a prebuild or a development build is required tells the agent when its change cannot be tested in Expo Go. EAS commands are included because they are the only path to a real binary.
iOS and Android diverge on permissions, safe areas, keyboard behavior, and back navigation, and none of that is visible in a single simulator. Writing the known differences down stops an agent from shipping a screen that only works on one platform. It is also where you record which native modules require a rebuild.
React Native style rules are mostly about performance and layout primitives, since the wrong list component or an inline style in a render loop is felt immediately on device. Concrete rules about StyleSheet, list rendering, and image handling are checkable. Navigation typing belongs here too.
The native directories and app config determine whether a build succeeds at all, and regenerating them by hand is a common way to lose configuration. Signing credentials and store metadata should never be automated. Mark version and build numbers as release process, not code.
Mobile tests are limited, so it is worth being explicit that unit tests cover logic while devices cover interaction. Naming which pure modules must be tested keeps the suite meaningful. Manual verification steps belong in the PR rather than the test suite.
Mobile PRs need evidence from both platforms because a reviewer cannot rerun the app cheaply. Stating that a screenshot or recording is expected sets that norm. Branch and commit conventions match the rest of the team.
Claude Code looks for CLAUDE.md. Most other agents and editors read AGENTS.md. Rather than maintaining both and letting them drift, keep one real file and symlink the other:
ln -s AGENTS.md CLAUDE.md
git add AGENTS.md CLAUDE.md Git stores the symlink, so it survives cloning on macOS and Linux. On Windows it needs developer mode or a stub file that references the real one. The full comparison is in CLAUDE.md vs AGENTS.md.
Next.js SaaS
App Router SaaS with Supabase auth, Postgres, and Stripe billing.
Turborepo Monorepo
Multi-package workspace with shared UI, config, and typed contracts.
SvelteKit App
SvelteKit app with server load functions, form actions, and Vitest.
Open Source Library
Published npm package with a stable API, changesets, and docs.