Table of Contents
Overview
NVLP provides comprehensive support for mobile applications through the unified client library. This guide covers integration patterns, offline capabilities, authentication flows, and performance optimization specifically for mobile development.
📱 Cross-Platform Support
Works with React Native CLI, Expo, and hybrid applications across iOS and Android.
🔒 Mobile Authentication
Deep linking, biometric authentication, and secure session management.
📡 Offline-First
Built-in request queuing, local storage, and automatic synchronization.
⚡ Performance Optimized
Lazy loading, request batching, and memory-efficient data handling.
🔄 Real-time Updates
WebSocket support for live data updates and collaborative features.
🛡️ Security Built-in
Secure storage, automatic token refresh, and certificate validation.
Platform Support
React Native CLI
✅ Fully Supported
- Full native module access
- Custom native code integration
- Advanced offline storage options
- Biometric authentication
- Push notifications
Expo Managed
✅ Supported
- Zero native code required
- Over-the-air updates
- Simplified deployment
- Limited to Expo SDK features
- AsyncStorage for persistence
Expo Bare Workflow
✅ Fully Supported
- Best of both worlds
- Expo tooling + native access
- Full customization
- Advanced security features
- Custom native modules
Project Setup
React Native CLI Setup
1. Install Dependencies
# Core NVLP client
npm install @nvlp/client
# React Native dependencies
npm install @react-native-async-storage/async-storage
npm install @supabase/supabase-js
npm install react-native-url-polyfill
# Optional: Enhanced features
npm install react-native-keychain # Secure storage
npm install @react-native-community/netinfo # Network status
npm install react-native-biometrics # Biometric auth
2. Platform Configuration
// metro.config.js - Add polyfills
const { getDefaultConfig } = require('metro-config');
module.exports = (async () => {
const {
resolver: { sourceExts, assetExts },
} = await getDefaultConfig();
return {
resolver: {
assetExts: assetExts.filter(ext => ext !== 'svg'),
sourceExts: [...sourceExts, 'svg'],
},
transformer: {
babelTransformerPath: require.resolve('react-native-svg-transformer'),
},
};
})();
// index.js - Add polyfills at the top
import 'react-native-url-polyfill/auto';
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
AppRegistry.registerComponent(appName, () => App);
Expo Setup
1. Install Dependencies
# Core NVLP client
npx expo install @nvlp/client
# Expo-compatible dependencies
npx expo install @react-native-async-storage/async-storage
npx expo install @supabase/supabase-js
npx expo install expo-secure-store # For secure token storage
npx expo install expo-auth-session # For OAuth flows
npx expo install expo-linking # For deep linking
npx expo install expo-network # For network status
2. App Configuration
// app.json - Configure deep linking and permissions
{
"expo": {
"name": "NVLP Budget App",
"slug": "nvlp-budget",
"scheme": "nvlp",
"platforms": ["ios", "android"],
"ios": {
"bundleIdentifier": "com.yourcompany.nvlp",
"buildNumber": "1.0.0",
"infoPlist": {
"NSFaceIDUsageDescription": "Use Face ID to secure your budget data"
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png"
},
"permissions": [
"USE_FINGERPRINT",
"USE_BIOMETRIC"
]
}
}
}
Mobile Authentication
Complete Authentication Setup
// src/services/AuthService.ts
import { createClient, SupabaseClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createNVLPClient, NVLPClient, NVLPSessionProvider } from '@nvlp/client';
import { Platform, Linking } from 'react-native';
export class MobileAuthService implements NVLPSessionProvider {
private supabase: SupabaseClient;
private nvlp: NVLPClient;
private sessionListeners: Set<(session: any) => void> = new Set();
constructor() {
// Initialize Supabase with custom storage
this.supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL!,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
{
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: Platform.OS === 'web',
},
}
);
// Initialize NVLP client
this.nvlp = createNVLPClient({
supabaseUrl: process.env.EXPO_PUBLIC_SUPABASE_URL!,
supabaseAnonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
sessionProvider: this,
offlineQueue: {
enabled: true,
maxSize: 100,
retryOnReconnect: true,
},
});
// Listen for auth state changes
this.supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth state changed:', event, session?.user?.id);
this.notifySessionListeners(session);
});
// Handle deep linking for magic links
this.setupDeepLinking();
}
// NVLPSessionProvider implementation
async getSession() {
const { data: { session } } = await this.supabase.auth.getSession();
return session;
}
async ensureValidSession() {
const { data: { session }, error } = await this.supabase.auth.getSession();
if (!session) {
throw new Error('No active session');
}
// Check if token needs refresh
if (session.expires_at && session.expires_at * 1000 < Date.now()) {
const { data: refreshData, error: refreshError } =
await this.supabase.auth.refreshSession();
if (refreshError || !refreshData.session) {
throw new Error('Failed to refresh session');
}
return refreshData.session;
}
return session;
}
onSessionChange(handler: (session: any) => void) {
this.sessionListeners.add(handler);
return () => this.sessionListeners.delete(handler);
}
private notifySessionListeners(session: any) {
this.sessionListeners.forEach(listener => listener(session));
}
// Magic link authentication
async signInWithMagicLink(email: string) {
const { data, error } = await this.supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: 'nvlp://auth/callback',
},
});
if (error) throw error;
return data;
}
// OAuth authentication
async signInWithOAuth(provider: 'google' | 'apple') {
const { data, error } = await this.supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: 'nvlp://auth/callback',
},
});
if (error) throw error;
return data;
}
// Sign out
async signOut() {
await this.nvlp.clearOfflineQueue();
const { error } = await this.supabase.auth.signOut();
if (error) throw error;
}
// Deep linking setup
private setupDeepLinking() {
const handleDeepLink = (url: string) => {
if (url.includes('auth/callback')) {
const urlParams = new URL(url);
const accessToken = urlParams.searchParams.get('access_token');
const refreshToken = urlParams.searchParams.get('refresh_token');
if (accessToken && refreshToken) {
this.supabase.auth.setSession({
access_token: accessToken,
refresh_token: refreshToken,
});
}
}
};
Linking.addEventListener('url', (event) => {
handleDeepLink(event.url);
});
Linking.getInitialURL().then((url) => {
if (url) handleDeepLink(url);
});
}
getNVLPClient(): NVLPClient {
return this.nvlp;
}
}
Offline Support
Complete Offline Implementation
// src/services/OfflineService.ts
import { createNVLPClient, AsyncStorageImpl } from '@nvlp/client';
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';
export class OfflineService {
private nvlp: any;
private isOnline: boolean = true;
private offlineListeners: Set<(isOnline: boolean) => void> = new Set();
constructor(authService: MobileAuthService) {
// Configure NVLP client with offline support
this.nvlp = createNVLPClient({
supabaseUrl: process.env.EXPO_PUBLIC_SUPABASE_URL!,
supabaseAnonKey: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
sessionProvider: authService,
offlineQueue: {
enabled: true,
maxSize: 200,
maxRetries: 3,
retryDelay: 2000,
storage: new AsyncStorageImpl(AsyncStorage),
retryOnReconnect: true,
},
});
this.setupNetworkMonitoring();
}
private setupNetworkMonitoring() {
NetInfo.addEventListener(state => {
const wasOffline = !this.isOnline;
this.isOnline = state.isConnected && state.isInternetReachable;
console.log('Network status:', {
connected: state.isConnected,
reachable: state.isInternetReachable,
type: state.type,
});
// Notify listeners
this.offlineListeners.forEach(listener => listener(this.isOnline));
// Process queue when coming back online
if (wasOffline && this.isOnline) {
this.processOfflineQueue();
}
});
}
async processOfflineQueue() {
try {
console.log('Processing offline queue...');
const processed = await this.nvlp.processOfflineQueue();
console.log(`Processed ${processed} queued requests`);
return processed;
} catch (error) {
console.error('Error processing offline queue:', error);
return 0;
}
}
onNetworkChange(listener: (isOnline: boolean) => void) {
this.offlineListeners.add(listener);
return () => this.offlineListeners.delete(listener);
}
getNetworkStatus(): boolean {
return this.isOnline;
}
}
State Management Integration
React Context + Hooks Pattern
// src/context/NVLPContext.tsx
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { MobileAuthService } from '../services/AuthService';
import { OfflineService } from '../services/OfflineService';
interface NVLPContextType {
session: any;
user: any;
signIn: (email: string) => Promise;
signOut: () => Promise;
isOnline: boolean;
queueSize: number;
authService: MobileAuthService;
offlineService: OfflineService;
isLoading: boolean;
}
const NVLPContext = createContext(null);
export function NVLPProvider({ children }: { children: ReactNode }) {
const [authService] = useState(() => new MobileAuthService());
const [offlineService] = useState(() => new OfflineService(authService));
const [session, setSession] = useState(null);
const [user, setUser] = useState(null);
const [isOnline, setIsOnline] = useState(true);
const [queueSize, setQueueSize] = useState(0);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Initialize auth state
const initAuth = async () => {
try {
const currentSession = await authService.getSession();
setSession(currentSession);
setUser(currentSession?.user || null);
} catch (error) {
console.error('Error initializing auth:', error);
} finally {
setIsLoading(false);
}
};
initAuth();
// Listen for auth changes
const unsubscribeAuth = authService.onSessionChange((newSession) => {
setSession(newSession);
setUser(newSession?.user || null);
});
// Listen for network changes
const unsubscribeNetwork = offlineService.onNetworkChange((online) => {
setIsOnline(online);
});
return () => {
unsubscribeAuth();
unsubscribeNetwork();
};
}, [authService, offlineService]);
const signIn = async (email: string) => {
await authService.signInWithMagicLink(email);
};
const signOut = async () => {
await authService.signOut();
};
const value: NVLPContextType = {
session,
user,
signIn,
signOut,
isOnline,
queueSize,
authService,
offlineService,
isLoading,
};
return (
{children}
);
}
export function useNVLP() {
const context = useContext(NVLPContext);
if (!context) {
throw new Error('useNVLP must be used within NVLPProvider');
}
return context;
}
Performance Optimization
📊 Mobile Performance Tips
Key strategies for optimal mobile performance:
Data Loading Optimization
// src/hooks/useBudgetData.ts
import { useState, useEffect, useCallback } from 'react';
import { useNVLP } from '../context/NVLPContext';
import AsyncStorage from '@react-native-async-storage/async-storage';
export function useBudgetData(budgetId: string) {
const { authService } = useNVLP();
const nvlp = authService.getNVLPClient();
const [data, setData] = useState({
budget: null,
categories: [],
envelopes: [],
recentTransactions: [],
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const cacheKey = `budget_data_${budgetId}`;
const loadCachedData = useCallback(async () => {
try {
const cached = await AsyncStorage.getItem(cacheKey);
if (cached) {
const parsedData = JSON.parse(cached);
const cacheAge = Date.now() - parsedData.timestamp;
if (cacheAge < 5 * 60 * 1000) { // 5 minutes
setData(parsedData.data);
return true;
}
}
} catch (error) {
console.error('Error loading cached data:', error);
}
return false;
}, [cacheKey]);
const fetchFreshData = useCallback(async () => {
try {
setError(null);
// Load data in parallel for better performance
const [budget, categories, envelopes, recentTransactions] = await Promise.all([
nvlp.budgets.eq('id', budgetId).single(),
nvlp.categories.eq('budget_id', budgetId).order('display_order').get(),
nvlp.envelopes.eq('budget_id', budgetId).eq('is_active', true).get(),
nvlp.transactions.eq('budget_id', budgetId).order('created_at', false).limit(20).get(),
]);
const freshData = { budget, categories, envelopes, recentTransactions };
setData(freshData);
// Cache the fresh data
await AsyncStorage.setItem(cacheKey, JSON.stringify({
data: freshData,
timestamp: Date.now(),
}));
} catch (err) {
console.error('Error fetching budget data:', err);
setError(err);
} finally {
setLoading(false);
}
}, [nvlp, budgetId, cacheKey]);
const refresh = useCallback(async () => {
setLoading(true);
await fetchFreshData();
}, [fetchFreshData]);
useEffect(() => {
const loadData = async () => {
const hasCachedData = await loadCachedData();
if (hasCachedData) {
setLoading(false);
fetchFreshData(); // Background refresh
} else {
await fetchFreshData();
}
};
loadData();
}, [budgetId, loadCachedData, fetchFreshData]);
return { ...data, loading, error, refresh };
}
Security Considerations
🔐 Mobile Security Best Practices
Essential security measures for mobile apps:
Secure Token Storage
// src/services/SecureStorage.ts
import * as Keychain from 'react-native-keychain';
// import * as SecureStore from 'expo-secure-store'; // For Expo
export class SecureTokenStorage {
private static readonly TOKEN_KEY = 'nvlp_auth_token';
static async storeTokens(accessToken: string, refreshToken: string): Promise {
try {
// React Native CLI with Keychain
await Keychain.setInternetCredentials(
this.TOKEN_KEY,
'nvlp_user',
JSON.stringify({ accessToken, refreshToken })
);
// Expo alternative:
// await SecureStore.setItemAsync(this.TOKEN_KEY, accessToken);
} catch (error) {
console.error('Failed to store tokens securely:', error);
throw new Error('Token storage failed');
}
}
static async getTokens(): Promise<{ accessToken: string; refreshToken: string } | null> {
try {
const credentials = await Keychain.getInternetCredentials(this.TOKEN_KEY);
if (credentials) {
return JSON.parse(credentials.password);
}
} catch (error) {
console.error('Failed to retrieve tokens:', error);
}
return null;
}
static async clearTokens(): Promise {
try {
await Keychain.resetInternetCredentials(this.TOKEN_KEY);
} catch (error) {
console.error('Failed to clear tokens:', error);
}
}
}
Complete App Example
Main App Component
// App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { StatusBar, Platform } from 'react-native';
import { NVLPProvider, useNVLP } from './src/context/NVLPContext';
import { LoadingScreen } from './src/screens/LoadingScreen';
import { AuthScreen } from './src/screens/AuthScreen';
import { BudgetDashboard } from './src/screens/BudgetDashboard';
const Stack = createStackNavigator();
function AppNavigator() {
const { session, isLoading } = useNVLP();
if (isLoading) {
return ;
}
return (
{session ? (
) : (
)}
);
}
export default function App() {
return (
);
}
🎉 Complete Mobile Integration
You now have a complete mobile app setup with:
- ✅ Cross-platform authentication with deep linking
- ✅ Offline-first architecture with automatic sync
- ✅ Secure token storage and session management
- ✅ Performance optimized data loading and caching
- ✅ Production-ready deployment configuration
🚀 Next Steps
Ready to build your mobile app? Explore these resources:
- Client Library Guide - Core API integration patterns
- Authentication Guide - Magic links and OAuth setup
- PostgREST Integration - Direct database access
- API Reference - Complete endpoint documentation