Storage
Secure token storage with SecureStore, preferences with AsyncStorage, and file uploads with Supabase or Convex.
Storage
ScaleRocket Mobile uses different storage solutions depending on the type of data.
SecureStore (Tokens)
Authentication tokens are stored using expo-secure-store, which provides encrypted storage on the device. This is more secure than AsyncStorage for sensitive data.
// lib/supabase.ts
import * as SecureStore from "expo-secure-store";
const SecureStoreAdapter = {
getItem: (key: string) => SecureStore.getItemAsync(key),
setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
removeItem: (key: string) => SecureStore.deleteItemAsync(key),
};The Supabase client uses this adapter automatically:
createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: SecureStoreAdapter,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});Note: SecureStore requires a development build. It does not work in Expo Go.
SecureStore Limits
- iOS: Data stored in the Keychain (encrypted, backed up to iCloud if enabled)
- Android: Data stored in SharedPreferences with Android Keystore encryption
- Size limit: 2KB per value. For larger data, use AsyncStorage or file storage
AsyncStorage (Preferences)
For non-sensitive data like user preferences, theme settings, or onboarding state, use @react-native-async-storage/async-storage:
npx expo install @react-native-async-storage/async-storageimport AsyncStorage from "@react-native-async-storage/async-storage";
// Save a preference
await AsyncStorage.setItem("theme", "dark");
await AsyncStorage.setItem("onboarding_complete", "true");
// Read a preference
const theme = await AsyncStorage.getItem("theme");
// Remove a preference
await AsyncStorage.removeItem("theme");File Uploads
Use Supabase Storage to upload files from the device:
import * as ImagePicker from "expo-image-picker";
import { supabase } from "../lib/supabase";
async function uploadAvatar() {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.8,
});
if (result.canceled) return;
const file = result.assets[0];
const fileName = `${user.id}/avatar.jpg`;
const formData = new FormData();
formData.append("file", {
uri: file.uri,
name: "avatar.jpg",
type: "image/jpeg",
} as any);
const { error } = await supabase.storage
.from("avatars")
.upload(fileName, formData, { upsert: true });
if (error) throw error;
}Get a public URL for uploaded files:
const { data } = supabase.storage
.from("avatars")
.getPublicUrl(`${user.id}/avatar.jpg`);
const avatarUrl = data.publicUrl;Use Convex File Storage to upload files:
import * as ImagePicker from "expo-image-picker";
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
async function uploadAvatar() {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.8,
});
if (result.canceled) return;
const file = result.assets[0];
// Get upload URL from Convex
const uploadUrl = await generateUploadUrl();
// Upload the file
const response = await fetch(file.uri);
const blob = await response.blob();
const uploadResult = await fetch(uploadUrl, {
method: "POST",
body: blob,
headers: { "Content-Type": "image/jpeg" },
});
const { storageId } = await uploadResult.json();
// Save the storage ID to the user's profile
await saveAvatar({ storageId });
}When to Use What
| Data Type | Solution | Example |
|---|---|---|
| Auth tokens | SecureStore | Session tokens, API keys |
| User preferences | AsyncStorage | Theme, language, onboarding state |
| Files | Supabase Storage / Convex Files | Avatars, documents, images |
| App state | React state / context | Current screen data, form inputs |