<!-- skill: Expo Router | version: 1.0.0 | description: File-based routes, stacks, tabs and EAS updates for Expo apps | category: Mobile -->
## Instructions
- Use file-based routing in `app/` dir: `app/index.tsx` = `/`, `app/details/[id].tsx` = `/details/:id`, `_layout.tsx` per folder for stack/tabs config.
- Define root layout with `<Stack>` or `<Tabs>`: `app/_layout.tsx` exports Stack with `screenOptions={{ headerShown: true }}`; nested `app/(tabs)/_layout.tsx` for tab group.
- Navigate with `useRouter` / `Link`: `router.push('/details/42')`, `router.replace('/login')`, `router.back()`; `<Link href={{ pathname: '/details/[id]', params: { id: '42' }}}>` for typed links.
- Dynamic + catch-all routes: `[id].tsx` via `useLocalSearchParams()`, `[...rest].tsx` for docs/not-found, `+not-found.tsx` for 404 screen.
- Modals: file `app/modal.tsx` + `<Stack.Screen options={{ presentation: 'modal' }} />`; dismiss via `router.dismiss()`; use `presentation: 'formSheet'` on iOS.
- Deep links: set `scheme` in `app.json` (`"scheme": "myapp"`), add `intentFilters` on Android + `associatedDomains` on iOS; test with `npx uri-scheme open myapp://details/42 --android`.
- Route groups don't affect URL: `(tabs)`, `(auth)` folders organize stacks; use `unstable_settings={{ initialRouteName: '(tabs)' }}` in root layout.
- Auth guard: `useSegments` + `useRootNavigationState` in `_layout.tsx`; redirect unauthed to `/(auth)/login` with `router.replace`, avoid flicker with splash (`expo-splash-screen`).
- EAS Update channels: `eas.json` with `development/preview/production` profiles + `channel` field; `eas update --branch production --message "fix"`; runtimeVersion policy `"appVersion"` or SDK pinned.
- Fonts/splash: `expo-font` + `expo-splash-screen` (`SplashScreen.preventAutoHideAsync()` then hide on `useFonts` loaded); `app.json` splash + icon + adaptiveIcon.
- Typed routes: enable `experiments: { typedRoutes: true }` for `href` type-safety; run `npx expo customize` only when ejecting metro config.
- Web support: `app/+html.tsx` for meta, `Link` works on web; test `npx expo start --web` for responsive check.
## Examples
- Tabs app: `(tabs)/home|search|profile` + `details/[id]` pushed on top of stack, modal for `new-post`, deep link `myapp://details/42`.
- Auth flow: `(auth)/login|register` group, `(tabs)` guarded by token check, `+not-found` redirects to `/`.
- OTA fix: push `eas update --branch preview` for copy bug, promote to production channel without store resubmit.
## Workflows
1. Scaffold: `npx create-expo-app -t tabs` → verify `app/_layout.tsx` + `(tabs)` group.
2. Add routes: new file per screen → register in Stack/Tabs options → typed `Link` wiring.
3. Auth guard + modal + not-found → test push/replace/back on both platforms.
4. Deep-link config (scheme/intents/domains) → `uri-scheme open` test + `eas build --profile development`.
5. EAS channels: dev/preview/prod branches → `eas update` → verify OTA in Expo DevTools.
6. Production: `eas build -p all` → submit → monitor update adoption.
## Templates
- Root + tabs layout:
```tsx
// app/_layout.tsx
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { useFonts } from 'expo-font';
import { useEffect } from 'react';
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
  const [loaded] = useFonts({ Inter: require('../assets/fonts/Inter.ttf') });
  useEffect(() => { if (loaded) SplashScreen.hideAsync(); }, [loaded]);
  if (!loaded) return null;
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'New Post' }} />
    </Stack>
  );
}
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabsLayout() {
  return (
    <Tabs screenOptions={{ headerShown: false }}>
      <Tabs.Screen name="home" options={{ title: 'Home', tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} /> }} />
      <Tabs.Screen name="profile" options={{ title: 'Profile' }} />
    </Tabs>
  );
}
```
- Auth guard + deep link test:
```tsx
// app/_layout.tsx (guard snippet)
import { useSegments, useRouter, useRootNavigationState } from 'expo-router';
function useAuthGuard(token: string | null) {
  const segments = useSegments();
  const navState = useRootNavigationState();
  const router = useRouter();
  useEffect(() => {
    if (!navState?.key) return;
    const inAuth = segments[0] === '(auth)';
    if (!token && !inAuth) router.replace('/(auth)/login');
    if (token && inAuth) router.replace('/(tabs)/home');
  }, [segments, navState?.key, token]);
}
// app.json + eas.json
// app.json: { "expo": { "scheme": "myapp", "ios": { "associatedDomains": ["applinks:myapp.example.com"] } } }
// eas.json: { "build": { "preview": { "channel": "preview" }, "production": { "channel": "production" } } }
```
## Tools
- SARAH shell_run for `npx expo start/clean`, `eas build/update/submit`, `uri-scheme open` deep-link tests.
- SARAH file_write for `app/` routes/layouts; web_fetch for expo-router.dev + EAS docs.
## Advanced
- Performance: `FlashList` for long feeds, `expo-image` with `contentFit`, lazy `useLocalSearchParams` parsing; Hermes enabled by default on SDK 50+.
- Universal links: `applinks:` + `assetlinks.json` (`/.well-known/`) with SHA-256 fingerprints; handle cold-start params via `getInitialURL`.
- OTA safety: gate breaking native changes behind `runtimeVersion.appVersion` bump + store build; never OTA native-module additions.
## Documentation
- https://docs.expo.dev/router/introduction/
- https://docs.expo.dev/eas-update/introduction/
- https://docs.expo.dev/guides/deep-linking/
- https://docs.expo.dev/build/introduction/
