Architecture Overview
High-level overview of the Ship React Native boilerplate architecture. The current repo supports two backend modes: Convex as the recommended full backend and Expo API Routes as the lighter server-side alternative.
Project Structure
ship-react-native/
├── src/
│ ├── app/ # Expo Router pages (file-based routing)
│ │ ├── _layout.tsx # Root layout with providers
│ │ ├── index.tsx # Entry point with auth/paywall routing
│ │ ├── api/ # Expo API Routes (lighter backend mode)
│ │ ├── (app)/ # App routes
│ │ ├── onboarding.tsx # Onboarding flow
│ │ └── login.tsx # Authentication
│ ├── components/ # Reusable components
│ ├── constants/ # Brand, features, backend mode, config
│ ├── hooks/ # App hooks and backend-specific hooks
│ ├── providers/ # Convex, Query, and root providers
│ ├── services/ # AI, purchases, notifications, auth
│ ├── store/ # Zustand state stores
│ ├── locales/ # i18n translations
│ └── utils/ # Helpers and storage
│
├── convex/ # Recommended full backend
├── scripts/ # Setup and utility scripts
└── __tests__/ # Jest testsNavigation Structure
/ # Entry point (index.tsx)
├── /onboarding # Cinematic onboarding flow
├── /login # Authentication
├── /privacy # Privacy policy
├── /terms # Terms of service
│
└── /(app) # Authenticated routes
├── /(tabs) # Tab navigation
│ ├── / # Home (index)
│ ├── /gallery # Gallery
│ └── /settings # Settings
│
├── /generate # Image generation
├── /video-generation # Video generation
├── /speech # Text-to-speech
├── /transcription # Speech-to-text
├── /image-identify # Image analysis
├── /chat # AI chat
└── /premium # PaywallState Management
Zustand stores handle all app state with AsyncStorage persistence:
| Store | State | Actions |
|---|---|---|
useAuthStore | user, session, isLoading | signIn(), signOut() |
useAIStore | credits, isGenerating, history | generate(), addCredits() |
usePremiumStore | isSubscribed, offerings | purchase(), restore() |
useGeneralStore | App-wide preferences | — |
useSettingsStore | User settings | — |
Backend Architecture
Ship React Native supports two backend paths. Convex is the recommended full backend for auth, persistence, credits, subscriptions, and AI orchestration. Expo API Routes are the lighter alternative when you mainly need secure server-side AI endpoints.
Mobile App (React Native)
↓ useQuery / useMutation / useAction
Convex
↓ auth, database, credits, subscriptions, AI actions
Provider APIs
↓ JSON / media result
Mobile App (display result)Mobile App (React Native)
↓ fetch("/api/[feature]", { body: userInput })
Expo API Route ← server-side env vars
↓ fetch("https://api.openai.com/...")
Provider APIs
↓ JSON result
Mobile App (display result)Backend Selection
Backend mode is selected in src/constants/api.ts. The current repo defaults to convex.
export type ApiMode = "convex" | "expo-api-routes";
export const API_MODE: ApiMode = "convex";Hard Paywall Architecture
The boilerplate uses a 3-layer hard paywall system. Every layer serves a distinct purpose — together they guarantee no free access to app features.
Layer 1: index.tsx (Entry Router)
// App launches -> index.tsx checks:
// 1. Onboarding completed? No -> /onboarding
// 2. isPremium? No -> /(app)/premium
// 3. isPremium? Yes -> /(app)/(tabs)
useEffect(() => {
if (isLoading || isPremiumLoading) return;
if (!isOnboardingCompleted) {
router.replace("/onboarding");
} else if (!isPremium) {
router.replace("/(app)/premium");
} else {
router.replace("/(app)/(tabs)");
}
}, [isOnboardingCompleted, isPremium, isPremiumLoading, isLoading]);Why: Handles the initial routing decision on cold start. Waits for usePremiumStatus to finish loading before routing, preventing flash of wrong screen.
Layer 2: (app)/_layout.tsx (Guard)
useEffect(() => {
if (isStatusLoading) return;
const onPremium = segments.some((s) => s === "premium");
if (!isSubscribed && !onPremium) {
router.replace("/(app)/premium");
}
}, [isSubscribed, isStatusLoading, segments, router]);Why: Catches edge cases where a user navigates directly to an app route (deep link, back button) without being subscribed. Acts as a safety net.
Layer 3: usePremiumStatus Hook
// Checks RevenueCat on mount, then every 5 minutes
// Syncs result to shared usePremiumStore (Zustand)
const isPremium = await purchasesService.checkSubscriptionStatus();
usePremiumStore.setState({ isSubscribed: isPremium });Why: Single source of truth for subscription state. Both Layer 1 and Layer 2 read from usePremiumStore.
Purchase Flow
// 1. User taps Subscribe on premium.tsx
// 2. purchasePackage() calls RevenueCat
// 3. On success, refreshPremiumStatus() re-checks entitlements
// 4. usePremiumStore.isSubscribed becomes true
// 5. Layer 2's useEffect fires — user can now access app
// 6. After confetti animation, navigate to /(app)/(tabs)__DEV__ mode, the premium screen shows a "Skip (Dev)" button. It must call usePremiumStore.setState({ isSubscribed: true }) — not just markPremiumScreenSeen(). The latter only marks onboarding as done, it does not bypass the paywall guard.Security Architecture
| Layer | What's Protected |
|---|---|
| Mobile App | No AI API keys in bundle. Only public RevenueCat/PostHog keys. |
| Convex / Expo API Routes | AI keys stay server-side. Validate inputs. Enforce auth and limits in the selected backend layer. |
| RevenueCat | Premium status verification — subscription-gated features. |
Component Hierarchy
App (_layout.tsx)
├── Providers
│ ├── ThemeProvider ← dark/light mode
│ ├── PurchasesProvider ← RevenueCat
│ └── PostHogProvider ← Analytics
│
└── Navigator (Expo Router)
├── OnboardingScreen ← Cinematic branded slides
├── LoginScreen ← Auth (anonymous or email)
└── AppLayout
├── TabNavigator
│ ├── HomeScreen
│ └── SettingsScreen
│
└── StackScreens
├── FeatureScreen ← Your AI feature
└── PremiumScreen ← VidNotes-style paywallPerformance Considerations
| Optimization | Implementation |
|---|---|
| Lazy loading | Screens loaded on demand via Expo Router |
| Memoization | React.memo for expensive components |
| Virtualized lists | FlatList for galleries |
| Background tasks | AI generation processed server-side |
| Image caching | expo-image for efficient caching |
Everything wired up correctly
Convex or Expo API Routes, Zustand stores, RevenueCat, and the design system — all connected.