Skip to content

Mobile Overview

apps/mobile · @playpals/mobile

The product. Expo SDK 54, React Native, expo-router for navigation, TanStack Query for server state.

Running it

bash
pnpm --filter @playpals/mobile ios
pnpm --filter @playpals/mobile android
pnpm --filter @playpals/mobile start      # Metro only
pnpm --filter @playpals/mobile lint

You need a development build, not Expo Go

The app depends on native modules Expo Go does not bundle — @livekit/react-native-webrtc, expo-notifications, @bugsnag/expo. Install a development client on your simulator first. See Builds & Releases.

Environment

apps/mobile/.env, read through lib/env.ts:

bash
EXPO_PUBLIC_ENV=development
EXPO_PUBLIC_API_URL=http://localhost:4000/api/v1
EXPO_PUBLIC_REALTIME_URL=http://localhost:4010

The app logs an error at startup if apiUrl or realtimeUrl is missing. EXPO_PUBLIC_API_URL must include /api/v1 — it is the axios baseURL verbatim.

app.config.ts derives identity from EXPO_PUBLIC_ENV:

ValueApp nameScheme
developmentPlayPalz Devplaypalz-dev
stagingPlayPalz Stagingplaypalz-staging
productionPlayPalzplaypalz

Provider stack

app/_layout.tsx composes the root:

AuthProvider                  auth state, token, API base config
└─ RevenueCatProvider         purchases and entitlements
   └─ GestureHandlerRootView
      └─ ThemeProvider        design tokens, colour scheme
         └─ CommentSheetProvider
            └─ CreatePostSheetProvider
               ├─ <Slot />    the routed screen
               ├─ SnackbarHost
               └─ StatusBar

Two details in that file are worth knowing:

Fonts gate the render. useFonts(fontLoadMap) must resolve before anything renders, and the splash screen is held until it does:

ts
SplashScreen.preventAutoHideAsync();
useEffect(() => { if (fontsLoaded) SplashScreen.hideAsync(); }, [fontsLoaded]);
if (!fontsLoaded) return null;

Bugsnag is required lazily in non-development builds:

ts
if (process?.env.EXPO_PUBLIC_ENV !== "development") {
  const Bugsnag = require("@bugsnag/expo").default;
  Bugsnag.start();
}

The comment explains why — Bugsnag depends on @react-native-community/netinfo, whose native module is absent in development, and a static import would throw before the module's export default is reached.

LiveKit globals are registered at module load, wrapped in a try/catch so a failure warns rather than crashes.

Auth gate

app/(app)/_layout.tsx guards the entire authenticated tree:

ts
if (isLoading) return <Spinner />;
if (!user) return <Redirect href="/(auth)/login" />;
if (!user.onboarded) return <Redirect href="/(onboarding)" />;

It also mounts three app-wide hooks: usePushNotifications, useRealtimeNotifications, and useRealtimeMediaProcessing. Because they live here rather than on a screen, they stay active for the whole authenticated session.

Directory layout

apps/mobile/
├── app/            expo-router screens — the file tree IS the route table
├── api/            one axios module per domain (auth, feed, post, …)
├── hooks/          TanStack Query hooks, grouped by domain, each with keys.ts
├── components/     shared UI, grouped by feature
├── context/        auth, chat UI
├── lib/            axios, env, query-client, navigation, revenueCat, snackbar, media
├── theme/          colors, typography, tokens, ThemeContext
├── constants/      colors, spacing, typography — older; prefer theme/
├── plugins/        Expo config plugins
├── utils/
└── app.config.ts   dynamic Expo config

Two styling sources

constants/colors.ts and theme/colors.ts both exist, and files import from both — sometimes in the same file (app/(app)/(tabs)/_layout.tsx uses Colors from constants and colors from theme). theme/ is the newer, fuller system. Prefer it, and migrate constants/ usages when you touch them.

Key libraries

ConcernLibrary
Navigationexpo-router 6
Server state@tanstack/react-query 5
HTTPaxios
Realtime@playpals/socket-client
Voice / video@livekit/react-native
Lists@shopify/flash-list
Bottom sheets@gorhom/bottom-sheet
Imagesexpo-image
Videoexpo-av
Purchasesreact-native-purchases
Notificationsexpo-notifications
Crash reporting@bugsnag/expo
Storage@react-native-async-storage/async-storage

Note babel-plugin-react-compiler is enabled — the React Compiler handles memoisation, so manual useMemo / useCallback are usually unnecessary.

Next

Internal documentation — PlayPalz platform