Skip to content

Project structure

├── index.ts # Custom entry: configures styles, then starts the router
├── app.config.ts # Dynamic Expo config (name, bundle id, scheme, plugins)
├── env.ts # Zod schema: validates env vars, builds typed config
├── .env.example # The only committed env file; seeds the other three
├── eas.json # EAS build profiles
├── .eas/workflows/ # EAS workflows: build, build-fast, release
├── scripts/ # setup-env.mjs, repack.mjs, eas-pre-install.sh
├── .skills/ # Step-by-step guides for AI coding assistants
└── src/
├── app/ # Expo Router routes: one-line re-exports of feature screens
│ ├── _layout.tsx # Root layout: bootstrap, providers, auth gating
│ ├── (auth)/login.tsx # → LoginScreen
│ └── (tabs)/ # index → Home, settings → Settings
├── features/ # One folder per feature
│ ├── auth/ # screens/, store/, api/, hooks.ts, session.ts, types.ts
│ │ └── index.ts # The feature's public API
│ ├── home/ # HomeScreen (posts demo)
│ ├── posts/ # api/: example query and mutation hooks
│ └── settings/ # SettingsScreen (language picker, logout)
├── shared/ # Feature-agnostic building blocks
│ ├── components/ # Button, TextField, ControlledTextField, ScreenWrapper, tab bar parts, AppOverlay
│ ├── constants/ # Route names
│ ├── hooks/ # useAppState, useDebounce, useBlockBackNavigation, useScreenLoader
│ └── utils/ # Responsive scale helpers, toasts, app loader
├── providers/ # AppProviders + useAppBootstrap()
├── services/ # Axios client, QueryClient provider, pagination helpers
├── store/ # createPersistedStore(), rehydrateStores(), StoreProvider
├── storage/ # Encrypted MMKV + token storage
├── localization/ # i18n setup, translations, typed hooks
├── styles/ · theme/ # Themes, breakpoints, fonts, text styles
└── assets/ # Fonts and images

With React Navigation, src/app/ is replaced by src/navigation/ (root stack, auth stack, main tabs, linking config and param-list types) and src/App.tsx.

Everything for a feature lives in src/features/<feature>/:

Folder / file Contains
screens/ The feature’s screens
api/ react-query-kit hooks for its endpoints
store/ Its state (Zustand store or Redux slice), private to the feature
hooks.ts The hooks other code uses to read and change that state
types.ts Its types
index.ts The public API. The only file other code may import
  • Outside a feature, import it only through its index.ts: import { useLogin } from '@/features/auth'. ESLint rejects deep imports such as @/features/auth/store/useUserStore.
  • Inside a feature, use relative imports.
  • src/store/ is the one exception: it wires every feature’s persisted store into start-up, so it may import stores directly.
  • Never import axios outside src/services/. Use the shared client.
Alias Maps to Example
@/* src/* import { Button } from '@/shared/components'
@env env.ts (a mock under Jest) import Env from '@env'

Both are set in tsconfig.json and babel.config.js.

Some things have to happen in order before the first screen renders. useAppBootstrap(), called from the root layout, handles it:

  1. initStorage() reads the encryption key from the keychain and opens MMKV.
  2. rehydrateStores() loads persisted state, so isLoggedIn is correct before anything renders.
  3. initI18n() reads the saved language from MMKV.
  4. The splash screen hides and the navigator mounts the auth or the main group.

The custom index.ts entry loads the style configuration before expo-router/entry, because route modules create styles at import time and need the themes registered first.

android/ and ios/ are generated by expo prebuild from app.config.ts and are in .gitignore:

  • Native config differs per environment (app name, env values). Committing one environment’s native code would conflict with the others.
  • Prebuild is deterministic, so anyone can regenerate identical native projects.
  • The repo stays small.