A step-by-step guide for new contributors to go from a fresh clone to a running simulator, and to understand the codebase well enough to contribute effectively.
- Prerequisites
- Initial Setup
- Running the App
- Folder Structure
- Routing — Expo Router & the
app/(tabs)Structure - Environment Configuration
- Running Against Local Backend vs Production
- Styling
- Shared Components & Key Utilities
- Available Scripts
- Common Issues
Install all of these before cloning.
Runtime & Package Manager
| Tool | Version | Install |
|---|---|---|
| Node.js | 18.x or later | nodejs.org |
| pnpm | latest | npm install -g pnpm |
Expo Tooling
npm install -g expo-cliFor iOS (macOS only)
- Xcode 14+ — install from the Mac App Store
- Xcode Command Line Tools:
xcode-select --install - iOS Simulator is bundled with Xcode (no separate install needed)
- CocoaPods:
sudo gem install cocoapods
For Android
- Android Studio — includes the Android emulator
- During Android Studio setup, install the following via SDK Manager:
- Android SDK Platform (API Level 33 or higher)
- Android Emulator
- Android SDK Platform-Tools
- Set the
ANDROID_HOMEenvironment variable (Android Studio will prompt you)
Physical Device Testing
- Install Expo Go from the App Store or Google Play
- Your phone and development machine must be on the same Wi-Fi network
Note: The app uses Expo SDK ~54 and React Native 0.81.5. Expo Go supports specific SDK versions — if you encounter a version mismatch, use the Expo Dev Client instead (see
expo-dev-clientinpackage.json).
# Fork the repo on GitHub first, then:
git clone https://github.com/<your-username>/lumenpulse.git
cd lumenpulsegit checkout -b docs/mobile-developer-guidecd apps/mobile
pnpm installThere is no .env.example file yet — create .env manually in apps/mobile/:
# apps/mobile/.env
EXPO_PUBLIC_API_URL=http://localhost:3000
EXPO_PUBLIC_APP_VARIANT=developmentThe EXPO_PUBLIC_ prefix is required by Expo. Any variable without this prefix will not be available in the app bundle.
Start the Expo development server from inside apps/mobile/:
pnpm startOnce the Metro bundler is running, use the following keyboard shortcuts in the terminal:
| Key | Action |
|---|---|
a |
Open on Android emulator |
i |
Open on iOS simulator |
w |
Open in browser (web) |
r |
Reload the app |
m |
Toggle the dev menu |
Or scan the QR code printed in the terminal with the Expo Go app on your phone.
Platform-specific quick launch:
pnpm android # Opens directly on Android emulator
pnpm ios # Opens directly on iOS simulator
pnpm web # Opens in browserapps/mobile/
├── app/ # All screens and navigation (Expo Router)
│ ├── _layout.tsx # Root layout — wraps the entire app (providers, fonts)
│ ├── +not-found.tsx # 404 screen for unmatched routes
│ ├── (tabs)/ # Tab navigator group
│ │ ├── _layout.tsx # Defines the bottom tab bar and its tabs
│ │ ├── index.tsx # Home tab screen
│ │ ├── portfolio.tsx # Portfolio tab screen
│ │ ├── settings.tsx # Settings tab screen
│ │ └── news/ # News stack nested inside tabs
│ │ ├── _layout.tsx # Stack navigator for news
│ │ ├── index.tsx # News list screen
│ │ └── [id].tsx # Dynamic route — individual article
│ └── auth/ # Auth screens (outside the tab navigator)
│ ├── _layout.tsx # Auth stack layout
│ ├── login.tsx # Login screen
│ └── register.tsx # Register screen
│
├── components/ # Reusable UI components
│ └── ProtectedRoute.tsx # Wraps screens that require authentication
│
├── contexts/ # React Context providers
│ ├── AuthContext.tsx # Auth state: user, token, login/logout methods
│ └── ThemeContext.tsx # App theme (light/dark)
│
├── lib/ # Core utilities and services
│ ├── api-client.ts # Low-level HTTP client (GET, POST, PUT, DELETE)
│ ├── api.ts # Domain-specific API calls (authApi, newsApi, etc.)
│ ├── config.ts # Centralized environment config
│ ├── storage.ts # Secure token storage (expo-secure-store)
│ └── api-examples.ts # Usage examples for the API client
│
├── theme/
│ └── colors.ts # Color palette constants
│
├── assets/ # Images, icons, splash screen
├── app.json # Expo config (name, bundle ID, SDK version)
├── babel.config.js # Babel config with expo preset
└── tsconfig.json # TypeScript config
This app uses Expo Router v6, which maps the file system directly to routes — the same convention as Next.js.
Every file inside app/ becomes a route. The filename becomes the URL path segment.
| File | Route |
|---|---|
app/(tabs)/index.tsx |
/ (Home) |
app/(tabs)/portfolio.tsx |
/portfolio |
app/(tabs)/settings.tsx |
/settings |
app/(tabs)/news/index.tsx |
/news |
app/(tabs)/news/[id].tsx |
/news/123 |
app/auth/login.tsx |
/auth/login |
app/auth/register.tsx |
/auth/register |
The parentheses (tabs) create a route group — it organizes screens into a shared layout (the bottom tab bar) without adding tabs to the URL. The layout for this group is defined in app/(tabs)/_layout.tsx.
Any _layout.tsx file wraps all sibling and child routes. The nesting follows the folder structure:
app/_layout.tsx ← wraps everything (root providers)
└── app/(tabs)/_layout.tsx ← wraps all tab screens (tab bar)
└── app/(tabs)/news/_layout.tsx ← wraps news screens (stack header)
Square brackets create dynamic segments. In app/(tabs)/news/[id].tsx, the id parameter is accessed like this:
import { useLocalSearchParams } from 'expo-router';
export default function ArticleScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
// fetch article by id...
}import { router } from 'expo-router';
// Navigate to a route
router.push('/news/123');
// Replace current screen (no back button)
router.replace('/auth/login');
// Go back
router.back();Screens that require authentication use the ProtectedRoute component from components/ProtectedRoute.tsx. It reads from AuthContext and redirects to /auth/login if no session exists.
All environment config is centralized in lib/config.ts. It resolves values in this priority order:
EXPO_PUBLIC_*environment variables (highest priority)app.json→expo.extravalues- Hardcoded fallback defaults
// lib/config.ts — what each field controls
config.api.baseUrl // Backend URL (default: http://localhost:3000)
config.api.timeout // Request timeout in ms (default: 30000)
config.app.variant // 'development' | 'production'
config.isDevelopment // true when variant === 'development'
config.isProduction // true when variant === 'production'The app.json extra block holds the defaults used when no .env file is present:
"extra": {
"backendUrl": "http://localhost:3000",
"environment": "development"
}Make sure the backend is running first:
# From the repo root
cd apps/backend
pnpm install
pnpm start:dev # NestJS starts on port 3000Your apps/mobile/.env should have:
EXPO_PUBLIC_API_URL=http://localhost:3000
EXPO_PUBLIC_APP_VARIANT=developmentAndroid emulator note:
localhostinside an Android emulator refers to the emulator itself, not your machine. Use10.0.2.2instead:EXPO_PUBLIC_API_URL=http://10.0.2.2:3000
Physical device note: Use your machine's local IP address (e.g.,
http://192.168.1.x:3000). Find it withifconfig(macOS/Linux) oripconfig(Windows).
EXPO_PUBLIC_API_URL=https://api.lumenpulse.io
EXPO_PUBLIC_APP_VARIANT=productionWith these values set, config.isProduction will be true and config.isDevelopment will be false, which can be used to conditionally show dev-only UI.
The app uses React Native's built-in StyleSheet API with a custom dark-themed design system. There is no NativeWind/Tailwind at this time.
All colors are defined in theme/colors.ts. Always import from there rather than hardcoding hex values:
import { colors } from '@/theme/colors';
const styles = StyleSheet.create({
container: {
backgroundColor: colors.background,
},
title: {
color: colors.text.primary,
},
});The tsconfig.json configures @/ as an alias for the project root, so imports look like:
import { colors } from '@/theme/colors';
import { apiClient } from '@/lib/api-client';
import { AuthContext } from '@/contexts/AuthContext';Wrap any screen that requires an authenticated user. It handles the redirect to login automatically:
import ProtectedRoute from '@/components/ProtectedRoute';
export default function PortfolioScreen() {
return (
<ProtectedRoute>
{/* screen content */}
</ProtectedRoute>
);
}Low-level typed HTTP client. All requests automatically attach the auth token from secure storage.
import { apiClient } from '@/lib/api-client';
const response = await apiClient.get<User>('/users/me');
if (response.success) {
console.log(response.data); // typed as User
} else {
console.error(response.error.message);
}Pre-built methods grouped by domain. Prefer these over calling apiClient directly:
import { authApi, newsApi, healthApi } from '@/lib/api';
// Auth
await authApi.login({ email, password });
await authApi.register({ email, password });
await authApi.logout();
// News
const articles = await newsApi.getArticles();
const article = await newsApi.getArticleById('123');
// Health check
const status = await healthApi.check();Wraps expo-secure-store for persisting auth tokens safely. Used internally by api-client.ts — you generally won't need to call this directly.
Provides user, token, isLoading, login(), and logout() to the entire component tree. Access it with the useAuth hook:
import { useAuth } from '@/contexts/AuthContext';
const { user, login, logout, isLoading } = useAuth();Run these from inside apps/mobile/:
| Command | Description |
|---|---|
pnpm start |
Start the Metro bundler (Expo dev server) |
pnpm android |
Launch on Android emulator |
pnpm ios |
Launch on iOS simulator |
pnpm web |
Launch in browser |
pnpm lint |
Run ESLint |
pnpm format |
Run Prettier on all files |
pnpm tsc |
TypeScript type-check (no emit) |
Metro bundler cache problems
pnpm start --cleariOS simulator not opening
Make sure Xcode is installed and you've opened the simulator at least once manually via Xcode → Open Developer Tool → Simulator.
Android emulator not detected
Make sure a virtual device is running in Android Studio's Device Manager before running pnpm android.
localhost not resolving on Android emulator
Use 10.0.2.2 instead of localhost in EXPO_PUBLIC_API_URL. See Running Against Local Backend.
Expo SDK version mismatch in Expo Go
The app targets Expo SDK ~54. If Expo Go shows a version mismatch, either update Expo Go on your device or use the dev client:
pnpm expo run:android # builds and installs dev client on emulator
pnpm expo run:ios # builds and installs dev client on simulatorFor questions about the backend API, see apps/backend/README.md. For the overall architecture, see document/ARCHITECTURE.md.