Architecture & Implementation
Tech stack, data models, RevenueCat integration, and build configuration for Lovin' Van Life.
Overview
Lovin' Van Life is a native iOS app built with React Native and Expo (managed workflow). It uses Firebase for auth, real-time data, and storage, RevenueCat for subscription management, and Zustand for client-side state. The app targets iOS 16+ with the New Architecture (Fabric/TurboModules) enabled.
Tech Stack
| Layer | Technology | Purpose |
|---|---|---|
| Framework | React Native + Expo | Cross-platform mobile app (iOS primary) |
| Navigation | Expo Router | File-based routing with typed routes |
| Auth | Firebase Auth | Email/password authentication |
| Database | Cloud Firestore | Real-time document database with security rules |
| Storage | Firebase Cloud Storage | Profile photos and media |
| Payments | RevenueCat SDK | Subscription management, entitlements, paywall UI |
| State | Zustand + AsyncStorage | Persisted client-side state management |
| Location | expo-location | GPS positioning and trajectory tracking |
| Language | TypeScript | Type safety throughout the codebase |
Architecture
Expo Router (File-Based Navigation)
app/(auth)/ · app/(onboarding)/ · app/(tabs)/ · app/edit-profile · app/van-build
Dating Tab
Trajectory matching, swipe cards, map view, messaging
Community Tab
Interest Beacons, activity map, join/leave
Builders Tab
Expert directory, booking, consultations
Profile Tab
Edit profile, van details, theme picker, settings
Zustand Stores
authStore · themeStore · locationStore · caravanStore · beaconStore
Firebase Backend
Auth · Firestore · Cloud Storage · Security Rules
File Layout
app/
(auth)/ # Login, register, invite-code screens
_layout.tsx
login.tsx
register.tsx
invite-code.tsx
(onboarding)/ # 3-step onboarding flow
_layout.tsx
activities.tsx
van-details.tsx
trajectory.tsx
(tabs)/ # Main tab navigator
_layout.tsx
dating.tsx
community.tsx
builders.tsx
profile.tsx
_layout.tsx # Root layout (auth gate)
edit-profile.tsx
van-build.tsx
modal.tsx
stores/
authStore.ts # Auth state + Firebase listener
themeStore.ts # Theme persistence (7 themes)
locationStore.ts # GPS + location sharing
caravanStore.ts # Group travel (v2, deferred)
beaconStore.ts # Activity beacons state
lib/
revenuecat.ts # RevenueCat SDK wrapper
geo.ts # Geospatial utilities
synchronicity.ts # Match scoring algorithm
seedDatabase.ts # Demo data seeder
constants/
Colors.ts # 7 themes: palettes, buildColors(), buildMapStyle()
types/
index.ts # All TypeScript interfaces and types
Theming System
The app supports 7 color themes: golden-hour (default), campfire, northern-lights, crimson-night, sunset-fade, forest, and arctic. The theme system is built around a ThemePalette → buildColors() → ThemeColors pipeline in constants/Colors.ts.
A Zustand store (stores/themeStore.ts) persists only the themeId string to AsyncStorage and rebuilds the full colors and mapStyle objects on rehydration. All components consume colors via useThemeStore((s) => s.colors) and use a makeStyles(colors) pattern to generate dynamic StyleSheet objects.
Data Models
All data is stored in Cloud Firestore. Below are the core document types.
User
The central document in users/{uid}. Contains profile information, travel preferences, van details, and computed fields.
interface User {
id: string
email: string
displayName: string
photoURL?: string
photos?: string[]
bio?: string
inviteCode: string // Generated on registration
invitedBy?: string // UID of inviter
isPremium: boolean // Synced from RevenueCat
activities: Activity[]
vanDetails?: VanDetails
age?: number
gender?: Gender
currentLocation?: { lat, lng, name }
travelPace?: TravelPace // 'slow' | 'moderate' | 'fast'
campingPreferences?: CampingPreference[]
workStyle?: WorkStyle
socialVibe?: SocialVibe
lookingFor?: LookingFor[]
synchronicityScore?: number // Computed, not stored
}
Trajectory
Stored in trajectories/{id}. Each trajectory contains an array of planned destination locations with time windows.
interface Trajectory {
id: string
userId: string
locations: TrajectoryLocation[] // city, state, lat, lng, startDate, endDate
isActive: boolean
updatedAt: Date
}
Beacon
Activity beacons for community meetups. Stored in beacons/{id}.
interface Beacon {
id: string
userId: string
activity: Activity // 'hiking' | 'climbing' | 'yoga' | ...
title: string
location: { lat, lng, name }
dateTime: Date
maxParticipants: number
participants: string[] // Array of UIDs
status: BeaconStatus // 'open' | 'full' | 'completed' | 'cancelled'
}
Builder & Consultation
interface Builder {
id: string
userId: string
specialties: BuilderSpecialty[] // 'electrical' | 'solar' | 'plumbing' | ...
hourlyRate: number
rating: number
reviewCount: number
isAvailable: boolean
}
interface Consultation {
builderId: string
clientId: string
status: ConsultationStatus
price: number
topic: string
revenuecatTransactionId?: string // Links to RevenueCat
}
Match & Messaging
interface Match {
users: [string, string]
status: 'pending' | 'matched' | 'declined'
synchronicityScore: number
likedBy: string[]
}
interface Conversation {
participants: [string, string]
lastMessage?: string
unreadCount: number
}
Auth & Invite System
Authentication Flow
- User receives an invite code from an existing member
- Enters code on
invite-code.tsx— validated against Firestoreinvites/{code}document - Registers with email/password via Firebase Auth
- Three-step onboarding: select activities, enter van details, set trajectory destinations
- User document created in Firestore with a freshly generated invite code for them to share
Invite Code Architecture
Each invite code is a Firestore document at invites/{code}. Validation uses Firestore transactions to atomically check availability and mark the code as consumed, preventing race conditions. Security rules allow any authenticated user to read (validate) codes, but only the system can write to them.
interface Invite {
code: string // 8-char alphanumeric
createdBy: string // UID of the member who generated it
usedBy?: string // UID of the person who redeemed it
createdAt: Date
usedAt?: Date
}
RevenueCat Implementation
All subscription management flows through RevenueCat. The app uses react-native-purchases and react-native-purchases-ui for SDK integration and native paywall presentation.
SDK Configuration
RevenueCat is initialized in lib/revenuecat.ts with a singleton pattern. Configuration is deferred until first use and supports both anonymous and identified users.
// lib/revenuecat.ts — initialization
import Purchases from 'react-native-purchases';
const configureRevenueCat = async (appUserID?: string) => {
const configured = await Purchases.isConfigured();
if (configured) return;
await Purchases.setLogLevel(LOG_LEVEL.DEBUG);
Purchases.configure({ apiKey, appUserID });
};
Products & Entitlements
| Concept | Identifier | Description |
|---|---|---|
| Entitlement | Lovin Van life Pro | Grants access to all premium features |
| Product (Monthly) | monthly | $14.99/month auto-renewing subscription |
| Product (Annual) | annual | $99.99/year auto-renewing subscription |
Paywall Presentation
The app uses RevenueCatUI to present native paywalls. Two presentation modes are used:
// Present paywall unconditionally (e.g., from profile settings)
import RevenueCatUI from 'react-native-purchases-ui';
const result = await RevenueCatUI.presentPaywall();
// Present only if user lacks the Pro entitlement
const result = await RevenueCatUI.presentPaywallIfNeeded({
requiredEntitlementIdentifier: 'Lovin Van life Pro',
});
Entitlement Checking
Premium status is checked via checkPremiumStatus() which queries the RevenueCat SDK for the active "Lovin Van life Pro" entitlement. This is called on app launch and after subscription events, with the result stored in the authStore Zustand store so all screens can gate features consistently.
export const checkPremiumStatus = async (): Promise<boolean> => {
const customerInfo = await Purchases.getCustomerInfo();
return typeof customerInfo.entitlements.active['Lovin Van life Pro'] !== 'undefined';
};
Feature Gating
Premium features are gated at the point of action, not at the tab level. Free users can browse and discover but are prompted to upgrade when they:
- Exceed daily match limits on the Dating tab
- Try to book a builder consultation
- Attempt to create more than the free-tier beacon limit
- Try to view profile visitors
Additional RevenueCat APIs Used
Purchases.logIn(userId)— Associates Firebase UID with RevenueCat customer on loginPurchases.logOut()— Resets to anonymous on sign-outPurchases.getOfferings()— Fetches configured offerings for custom paywall scenariosPurchases.purchasePackage(pkg)— Direct package purchase (used for builder consultations)Purchases.restorePurchases()— Restore purchases flowRevenueCatUI.presentCustomerCenter()— Native subscription management screen
Feature Breakdown
Trajectory Matching
Trajectories are stored as arrays of discrete destinations (city, state, lat/lng, date window). Matching compares overlapping destinations within time windows between two users. A synchronicity score (computed in lib/synchronicity.ts) weights proximity, temporal overlap, and shared interests to rank matches. Firestore doesn't support native geospatial intersection queries, so matching is computed client-side by comparing destination documents.
Interest Beacons
Beacons are Firestore documents with geolocation, activity type, time, and a participants array. The Community tab renders beacons on an interactive map using react-native-maps. Users can join/leave beacons, and real-time Firestore listeners keep participant counts live.
Builder Marketplace
Builder profiles are stored in a builders collection linked to user documents. Consultations are separate documents tracking status, pricing, scheduling, and an optional revenuecatTransactionId for linking payments. Builder booking is gated behind the Pro entitlement.
Van Build Tracker
Each user's van build is tracked through vanDetails.buildFeatures — an array of categorized features (electrical, kitchen, bathroom, climate, water, bed, storage, tech, exterior) with status tracking (wishlist, planned, in-progress, installed), cost, specs, and photos.
Build Configuration
Expo Config
| Setting | Value |
|---|---|
| Expo SDK | Managed workflow |
| New Architecture | true (Fabric/TurboModules) |
| iOS Deployment Target | 16.0 |
| Bundle ID (iOS) | com.vanlife.lovin |
| Package (Android) | com.sunnysparyowstudios.lovinvanlife |
| Typed Routes | true |
iOS-Specific Build Configuration
Running @react-native-firebase with Expo's New Architecture requires specific build settings:
// expo-build-properties plugin config
{
ios: {
useFrameworks: "static", // Required for Firebase on iOS
buildReactNativeFromSource: true,
deploymentTarget: "16.0",
cppFlags: "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 ..." // Folly compat
}
}
// Custom plugin: plugins/withFirebaseFix.js
// Patches Xcode project for Firebase + New Architecture compatibility
Hosting
The landing page is deployed to Firebase Hosting at lovin-van-life.web.app (aliased to lovinvanlife.com). The app binary is distributed via TestFlight for iOS beta testing.