Stack: Expo SDK 57, React Native 0.86.3, React 19.2, Expo Router ~57. Auth uses
@clerk/expo ^4.8; icons use lucide-react-native; analytics/flags use posthog-react-native. UI style follows the OS (userInterfaceStyle: automatic); ThemeContext resolves a persisted preference — System (the default) tracks the device color scheme, while an explicit Light/Dark selection under You → Settings → Appearance overrides it.Navigation
Expo Router
The app uses Expo Router for file-based navigation. Routes are defined by the file structure in theapp/ directory. There are four tabs — events, mailbox, browse, you (the tab bar labels them Gigs / Mailbox / Browse / You).
customers, dashboard, files, inventory, leads, org-engagements, payments, setlists, songs, sponsors, tasks, team-availability, teams, venues. Most of these are feature-flag gated (see Feature flags).
The tab bar (components/AnimatedTabBar.tsx) is wrapped in <RequireAppAccess> and hides individual tabs via useHasVisibleMenuEntries(...) plus the mobile-inbox-top-right / floating-bottom-nav flags. Gigs (events) always stays; Mailbox is removed from the tab bar when mobile-inbox-top-right is on; You stays when Profile (and other You entries) remain visible.
Navigation patterns
State management
Context providers
Server state uses TanStack React Query v5 (see Data fetching); cross-cutting app state uses React Context. The provider stack spansapp/_layout.tsx and lib/providers/AppProviders.tsx:
Available contexts
Context providers live inlib/context/:
The React Query client is not a
lib/context provider — it lives in lib/query/QueryProvider.tsx (wraps QueryClientProvider with the shared queryClient and an AppState focusManager bridge).
Using contexts
Styling & theme
Native appearance support usesuserInterfaceStyle: 'automatic' and expo-system-ui; applying these native changes requires rebuilding and reinstalling the app, not just a JavaScript reload or OTA update.
NativeWind v4
Styling is primarily NativeWind v4 — Tailwind utility classes viaclassName, configured in tailwind.config.js with a global.css (imported in _layout.tsx) that defines semantic HSL tokens mirroring the web app’s globals.css (--primary, --secondary, --destructive, --success, --warning, --info, --finance, plus .dark overrides). Newer surfaces use a shadcn-for-RN layer in components/ui/ built on @rn-primitives/* + class-variance-authority.
The brand primary is now an indigo (
--primary: 239 84% 67%), not the legacy purple. #9b87f5 survives only as the app-icon/splash background (app.config.js) and the switch-active accent.Legacy token system
The olderconstants/theme/ token set (tokens/colors.ts, typography.ts, shadows.ts + useThemeColors) still supplies typography, shadows, and theme colors to native components. Prefer NativeWind classes for new work.
Component architecture
The app has a single UI layer: NativeWind v4 +@rn-primitives in components/ui.
- New components →
components/ui/(NativeWind +@rn-primitives, icons vialucide-react-nativere-exported throughlib/icons/). Form sheets usecomponents/ui/bottom-sheet-modal.tsx. - Gluestack is removed.
npm run check:gluestackis a permanent ban (empty allowlist).
Data fetching
TanStack React Query + service layer
Server state is managed with React Query v5. Query hooks live inlib/query/hooks/ (e.g. useCustomersQuery, useEngagementsQuery, useEventDetailsQuery) and call typed service classes in lib/api/ (OrganizationService, EventService, CrewOfferService, …). Those services share an axios transport, lib/api/httpClient.ts, which injects the Clerk token.
Use React Query for data fetching. Don’t hand-roll fetch/cache hooks; the legacy
useRefreshableData hook has been removed.Token management
Clerk JWT tokens are automatically attached to requests:Custom hooks
Available hooks
Example: tab focus hook
Performance optimizations
Tab preloading
All tabs are preloaded for instant switching:Data caching
React Query handles caching and background revalidation:- Stale-While-Revalidate: cached data shows instantly, fresh data fetches in the background
- Focus refetch: a
focusManagerAppState bridge (inQueryProvider) revalidates on app foreground - Pull-to-Refresh: invalidates/refetches the relevant queries
Component optimization
Error handling
Error boundary
The app includes a global error boundary:API error handling
Best practices
Component guidelines
- Keep components small - Split large components into smaller ones
- Use TypeScript - All components should be fully typed
- Prefer composition - Build complex UIs from simple components
- Memoize when needed - Use
memo()for expensive renders
State management guidelines
- Colocate state - Keep state as close to where it’s used as possible
- Context for global state - Use context for truly global state
- Avoid prop drilling - Use context when passing props through many levels
Performance guidelines
- Lazy load screens - Only when not in tab navigation
- Use FlatList - For long lists instead of ScrollView
- Optimize images - Use appropriate sizes and formats
- Minimize re-renders - Use
useMemoanduseCallbackappropriately
Feature flags
Nearly every mobile surface is PostHog feature-flag gated. Two things drive visibility:lib/constants/featureFlags.ts— re-exports the flag catalog generated fromflags/registry.jsonintolib/generated/featureFlags.ts:SHARED_FEATURE_FLAGS(keys shared with web, such ascrew-availability-offers,departments,event-chats,open-shifts) andMOBILE_FEATURE_FLAGS(mobile-only keys such asORG_ENGAGEMENTS,TEAM_AVAILABILITY,MAILBOX_TOP_RIGHT). Add or retire flags in the registry and runnpm run generate:flags; never edit the generated file.lib/navigation/menuRegistry.ts— each menu entry declares an optionalflag,tab,group, andrequiresRole.lib/navigation/menuVisibility.ts(useVisibleMenuEntries) resolves visibility in order: role gate → no-flag entries always visible → localEXPO_PUBLIC_FF_*override → PostHog evaluation.
PostHogContext calls posthog.group('organization', orgId) and seeds person properties (organization_ids, active_organization_id/role, is_organization_owner/admin, environment) so flags can target by membership and active org, then reloads flags.
Because targeting keys off the org, a flag scoped to an
organization_id only lights up on mobile once that org is the active org. See the rollout matrix in Mobile feature flag rollout.Native projects
Theios/ and android/ directories are gitignored and generated by expo prebuild from app.config.js — never commit them. See Environment Setup for prebuild commands.
Related
- Mobile feature flag rollout - Which flags to enable per org type
- Mobile Setup - Set up your development environment
- Authentication - Learn about auth flow and tokens
- Mobile Overview - General overview of the mobile app