Production-ready Expo starter with routing, theming, auth, and a full component library. Break from boilerplate.
Rift is a production-ready Expo React Native template with file-based routing, a full UI component library, auth patterns, and a typed API layer — everything wired up so you can break from boilerplate and start building what matters.
| Layer | Library |
|---|---|
| Framework | Expo SDK 54 + Expo Router v6 |
| Language | TypeScript (strict) |
| Styling | NativeWind v4 (Tailwind for RN) |
| State | Zustand v5 |
| Server state | TanStack React Query v5 |
| Forms | react-hook-form + zod |
| HTTP | Axios (with auth interceptors) |
| Secure storage | expo-secure-store / AsyncStorage |
| Fonts | Inter + Geist Mono via expo-google-fonts |
# 1. Clone / fork
git clone <repo-url> my-app
cd my-app
# 2. Install dependencies
npm install
# 3. Configure environment
cp .env.example .env
# Edit .env with your values
# 4. Start development server
npm run devCopy .env.example to .env and fill in the values. All client-side env vars must be prefixed with EXPO_PUBLIC_.
| Variable | Required | Description |
|---|---|---|
EXPO_PUBLIC_APP_ENV |
No | development | staging | production (default: development) |
EXPO_PUBLIC_API_URL |
No | Backend REST API base URL |
EXPO_PUBLIC_SUPABASE_URL |
No | Supabase project URL |
EXPO_PUBLIC_SUPABASE_ANON_KEY |
No | Supabase anonymous key |
Typed via constants/Config.ts — import Config instead of reading process.env directly.
.
├── app/ # Expo Router file-based routes
│ ├── _layout.tsx # Root layout (fonts, providers, nav)
│ ├── +not-found.tsx
│ ├── form-demo.tsx # Form patterns demo screen
│ ├── (auth)/ # Auth group (no tab bar)
│ │ ├── _layout.tsx
│ │ ├── login.tsx
│ │ ├── register.tsx
│ │ └── forgot-password.tsx
│ ├── (tabs)/ # Tab group (guarded — requires auth)
│ │ ├── _layout.tsx # Tab bar + auth redirect
│ │ ├── index.tsx # Home — FlatList with pagination
│ │ ├── explore.tsx # Component + pattern browser
│ │ └── profile.tsx # Settings + logout
│ └── item/
│ └── [id].tsx # Item detail (dynamic route)
│
├── components/
│ ├── ui/ # Design system components
│ │ ├── Text.tsx
│ │ ├── Button.tsx # 5 variants, 3 sizes, icons
│ │ ├── TextInput.tsx # Labels, errors, icons, multiline
│ │ ├── Card.tsx # elevated / outlined / filled
│ │ ├── Badge.tsx # 6 variants, dot indicator
│ │ ├── Avatar.tsx # Image or initials, status dot
│ │ ├── BottomSheet.tsx # @gorhom + useBottomSheet hook
│ │ ├── Toast.tsx # ToastProvider + useToast hook
│ │ ├── Modal.tsx # Slide-up with actions
│ │ ├── Skeleton.tsx # Shimmer placeholder
│ │ ├── EmptyState.tsx # Icon + title + CTA
│ │ ├── ErrorBoundary.tsx # Class component with retry
│ │ ├── Divider.tsx
│ │ └── index.ts # Barrel export
│ ├── layout/
│ │ ├── Screen.tsx # Safe area + scroll wrapper
│ │ ├── Stack.tsx # Flex gap layout primitive
│ │ └── ThemeProvider.tsx # System/light/dark context
│ └── forms/
│ └── TextInput.tsx # Re-exports ui/TextInput
│
├── constants/
│ ├── Colors.ts # Full light + dark color tokens
│ ├── Layout.ts # Spacing, radius, shadows
│ ├── Typography.ts # Font scale + families
│ └── Config.ts # Typed EXPO_PUBLIC_* env vars
│
├── hooks/
│ ├── useAppStore.ts # Zustand store access
│ ├── useColorScheme.ts # system/light/dark resolution
│ ├── useThemeColor.ts # Returns full ThemeColors object
│ └── queries/
│ ├── useItems.ts # useInfiniteItems, useItem
│ └── useMutateItem.ts # useUpdateItem, useDeleteItem
│
├── lib/
│ ├── api.ts # Axios instance + refresh interceptor
│ ├── queryClient.ts # TanStack QueryClient singleton
│ ├── storage.ts # SecureStore + AsyncStorage wrappers
│ ├── mock.ts # Mock data + stub API functions
│ └── utils.ts # Shared utility functions
│
├── store/
│ └── index.ts # Zustand store (auth + app slices)
│
├── types/
│ └── index.ts # Shared TypeScript types
│
├── .env # Local env (gitignored)
├── .env.example # Template for env vars
├── app.config.ts # Expo config with env variants
├── eas.json # EAS build profiles
├── tailwind.config.js # NativeWind config
├── babel.config.js # Babel with NativeWind preset
├── metro.config.js # Metro with NativeWind
├── eslint.config.js # ESLint flat config (typescript + react-hooks)
└── .prettierrc # Prettier config
# Development
npm run dev # Start Expo dev server (web + simulator)
# Type checking
npm run typecheck # tsc --noEmit
# Linting + formatting
npm run lint # ESLint with --fix
npm run lint:check # ESLint read-only
npm run format # Prettier --write
npm run format:check # Prettier read-only
# Web build
npm run build:web # Export static web bundle
# EAS builds
npm run build:dev # Development build (dev client)
npm run build:preview # Preview / internal distribution
npm run build:production # Production build for app stores
# EAS submit
npm run submit:ios # Submit to App Store Connect
npm run submit:android # Submit to Google PlayThree profiles are configured in eas.json:
| Profile | Distribution | Use case |
|---|---|---|
development |
Internal | Local dev, simulator, dev client |
preview |
Internal | Stakeholder testing via TestFlight / internal track |
production |
Store | App Store + Google Play submission |
# Install EAS CLI
npm install -g eas-cli
# Log in
eas login
# Link project (creates projectId in app.config.ts extra.eas)
eas init
# Build development client
npm run build:dev- Create
app/your-screen.tsx(or nested under a group). - Register it in
app/_layout.tsxunder<Stack>if needed. - Use
<Screen>from@/components/layoutfor safe-area + scroll handling. - Navigate with
router.push('/your-screen')or<Link href="/your-screen">.
# Clone
git clone <repo-url> my-new-app
cd my-new-app
# Remove origin, add your own
git remote remove origin
git remote add origin git@github.com:<you>/<your-repo>.git
# Update package name
# Edit: package.json name, app.config.ts APP_VARIANTS bundle IDs/package names
# Install and start
npm install
cp .env.example .env
npm run devThe template uses mock stubs in lib/mock.ts. To connect to a real auth provider:
- Replace
mockLogin/mockRegisterin the auth screens with real API calls. - The
api.tsrequest interceptor reads the token fromStorageKeys.AUTH_TOKENautomatically. - On 401, the response interceptor calls
POST /auth/refresh, retries the request, and falls back to logout + redirect. - The unauthorized handler is registered in
app/_layout.tsxviasetUnauthorizedHandler.
- Light / dark / system modes via
ThemeModein the Zustand store. - Toggle from Profile screen (cycles light → dark → system).
- All colors defined in
constants/Colors.tsasColors.lightandColors.dark. - Access anywhere with
useThemeColor()oruseColor('primary').