ScaleRocket/Mobile

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-storage
import 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 TypeSolutionExample
Auth tokensSecureStoreSession tokens, API keys
User preferencesAsyncStorageTheme, language, onboarding state
FilesSupabase Storage / Convex FilesAvatars, documents, images
App stateReact state / contextCurrent screen data, form inputs

On this page