Skip to main content

Storage Architecture

DPM Delivery Mobile uses a multi-layered storage strategy:
  • MMKV: Fast, encrypted native storage for general data
  • SecureStore: iOS Keychain and Android Keystore for sensitive tokens
  • Web Crypto API: Browser-based encryption for web platform
  • localStorage: Web platform fallback with encryption

Storage Abstraction

All storage operations go through a unified API:
// src/utils/storage.ts
import { createMMKV } from "react-native-mmkv";
import * as SecureStore from "expo-secure-store";
import { Platform } from "react-native";

export enum StorageKeys {
  AUTH_TOKEN = "auth_token",
  REFRESH_TOKEN = "refresh_token",
  USER = "user",
  DEVICE_KEY = "__device_key",
  WEB_CRYPTO_KEY = "__web_crypto_key",
}

export const Storage = {
  setItem,      // MMKV for general data
  getString,
  setObject,
  getObject,
  removeItem,
  setToken,     // SecureStore/Web Crypto for tokens
  getToken,
  deleteToken,
};

MMKV Storage

MMKV provides fast, encrypted key-value storage:
// src/utils/storage.ts
const ENCRYPTION_KEY = ENV.encryptionKey || "";

const mmkvStorage = createMMKV({
  encryptionKey: Platform.OS === "web" ? undefined : ENCRYPTION_KEY,
  id: Platform.OS === "web" ? "" : "mmkv_storage",
});

const setItem = (key: StorageKeys, value: string) => {
  mmkvStorage.set(key, value);
};

const getString = (key: StorageKeys, defaultValue?: string) => {
  return mmkvStorage.getString(key) || defaultValue || "";
};

const setObject = (key: StorageKeys, value: any) => {
  mmkvStorage.set(key, JSON.stringify(value));
};

const getObject = (key: StorageKeys, defaultValue?: any) => {
  const value = mmkvStorage.getString(key);
  return value ? JSON.parse(value) : defaultValue;
};

const removeItem = (key: StorageKeys) => {
  mmkvStorage.remove(key);
};
Features:
  • Synchronous API for better performance
  • Encrypted at rest (native platforms)
  • Type-safe with StorageKeys enum
  • JSON serialization for objects
Use Cases:
  • User profile data
  • App settings
  • Cached data
  • Non-sensitive information

Secure Token Storage

Tokens use platform-specific secure storage:
// src/utils/storage.ts
async function setToken(key: string, value: string): Promise<void> {
  if (Platform.OS === "web") {
    const encrypted = await encryptForWeb(value);
    localStorage.setItem(key, encrypted);
  } else {
    await SecureStore.setItemAsync(key, value, {
      // keychainAccessible: SecureStore.WHEN_UNLOCKED,
    });
  }
}

async function getToken(key: string): Promise<string | null> {
  if (Platform.OS === "web") {
    const encrypted = localStorage.getItem(key);
    if (!encrypted) return null;
    return await decryptForWeb(encrypted);
  } else {
    return await SecureStore.getItemAsync(key);
  }
}

async function deleteToken(key: string): Promise<void> {
  if (Platform.OS === "web") {
    localStorage.removeItem(key);
  } else {
    await SecureStore.deleteItemAsync(key);
  }
}
Native Platforms (iOS/Android):
  • iOS: Stored in Keychain
  • Android: Stored in Keystore
  • Hardware-backed encryption
  • Survives app uninstall (configurable)
Web Platform:
  • Encrypted using Web Crypto API
  • Stored in localStorage
  • Device-specific encryption key

Web Crypto Implementation

Browser-based encryption for sensitive data:
// src/utils/web-crypto.ts
const IV_LENGTH = 12;
const KEY_LENGTH = 256;

// Generate or retrieve encryption key
async function getWebEncryptionKey(): Promise<CryptoKey> {
  const storedKeyData = localStorage.getItem(WEB_CRYPTO_KEY);

  if (storedKeyData) {
    const keyData = JSON.parse(storedKeyData);
    return await crypto.subtle.importKey(
      "jwk",
      keyData,
      { name: "AES-GCM", length: KEY_LENGTH },
      true,
      ["encrypt", "decrypt"]
    );
  }

  const key = await crypto.subtle.generateKey(
    { name: "AES-GCM", length: KEY_LENGTH },
    true,
    ["encrypt", "decrypt"]
  );

  const exportedKey = await crypto.subtle.exportKey("jwk", key);
  localStorage.setItem(WEB_CRYPTO_KEY, JSON.stringify(exportedKey));

  return key;
}

// Encrypt using AES-GCM
export async function encryptForWeb(plaintext: string): Promise<string> {
  try {
    const key = await getWebEncryptionKey();
    const iv = generateRandomBytes(IV_LENGTH);
    
    const encoder = new TextEncoder();
    const data = encoder.encode(plaintext);

    const encryptedData = await crypto.subtle.encrypt(
      { name: "AES-GCM", iv: iv as BufferSource },
      key,
      data as BufferSource
    );

    // Combine IV + encrypted data
    const encryptedArray = new Uint8Array(encryptedData);
    const combined = new Uint8Array(iv.length + encryptedArray.length);
    combined.set(iv, 0);
    combined.set(encryptedArray, iv.length);

    // Convert to base64
    return btoa(String.fromCharCode(...combined));
  } catch (error) {
    logger.error("Web encryption failed:", error as Error);
    throw error;
  }
}

// Decrypt using AES-GCM
export async function decryptForWeb(ciphertext: string): Promise<string> {
  try {
    const key = await getWebEncryptionKey();

    const combined = new Uint8Array(
      atob(ciphertext)
        .split("")
        .map((char) => char.charCodeAt(0))
    );

    const iv = combined.slice(0, IV_LENGTH);
    const encryptedData = combined.slice(IV_LENGTH);

    const decryptedData = await crypto.subtle.decrypt(
      { name: "AES-GCM", iv: iv as BufferSource },
      key,
      encryptedData as BufferSource
    );

    const decoder = new TextDecoder();
    return decoder.decode(decryptedData);
  } catch (error) {
    logger.error("Web decryption failed:", error as Error);
    throw error;
  }
}
Features:
  • AES-GCM 256-bit encryption
  • Random IV for each encryption
  • Device-specific key generation
  • Base64 encoding for storage

Device Encryption Keys

Device-specific keys for additional security:
// src/utils/storage.ts
import * as Crypto from "expo-crypto";
import DeviceInfo from "react-native-device-info";

const ENCRYPTION_KEY = ENV.encryptionKey || "";

async function getWebEncryptionKey(): Promise<string> {
  const stored = localStorage.getItem(StorageKeys.DEVICE_KEY);
  if (stored) return stored;

  const key = await Crypto.digestStringAsync(
    Crypto.CryptoDigestAlgorithm.SHA256,
    `${Date.now()}:${Math.random()}`
  );

  localStorage.setItem(StorageKeys.DEVICE_KEY, key);
  return key;
}

export async function getDeviceEncryptionKey(): Promise<string> {
  if (Platform.OS === "web") {
    return await getWebEncryptionKey();
  }

  const deviceId = await DeviceInfo.getUniqueId();

  const key = await Crypto.digestStringAsync(
    Crypto.CryptoDigestAlgorithm.SHA256,
    `${deviceId}:${ENCRYPTION_KEY}`
  );

  return key;
}
Strategy:
  • Native: Device ID + app encryption key
  • Web: Random key generated once per browser
  • SHA-256 hashing for key derivation

Usage Examples

Storing User Data

import { Storage, StorageKeys } from "@/utils/storage";

// Store user object
const user = { id: "123", name: "John Doe", email: "[email protected]" };
Storage.setObject(StorageKeys.USER, user);

// Retrieve user object
const storedUser = Storage.getObject(StorageKeys.USER);

// Remove user data
Storage.removeItem(StorageKeys.USER);

Managing Auth Tokens

import { Storage, StorageKeys } from "@/utils/storage";

// Store tokens (secure)
await Storage.setToken(StorageKeys.AUTH_TOKEN, accessToken);
await Storage.setToken(StorageKeys.REFRESH_TOKEN, refreshToken);

// Retrieve tokens
const authToken = await Storage.getToken(StorageKeys.AUTH_TOKEN);
const refreshToken = await Storage.getToken(StorageKeys.REFRESH_TOKEN);

// Delete tokens (logout)
await Storage.deleteToken(StorageKeys.AUTH_TOKEN);
await Storage.deleteToken(StorageKeys.REFRESH_TOKEN);

Auth Guard with Storage

// src/app/(parcel)/_layout.tsx
import { User } from "@/types/auth.types";
import { Storage, StorageKeys } from "@/utils/storage";
import { Redirect, Stack } from "expo-router";

export default function PublicLayout() {
  const user = Storage.getObject(StorageKeys.USER) as User;
  
  if (!user) {
    return <Redirect href="/(public)/(auth)/sign-in" />;
  }

  return <Stack screenOptions={{ headerShown: false }} />;
}

HTTP Client with Token Storage

// src/services/http.service.ts
this.instance.interceptors.request.use(
  async (config) => {
    // Fetch fresh auth token on each request
    const authToken = await Storage.getToken(StorageKeys.AUTH_TOKEN);

    if (authToken) {
      config.headers.Authorization = `Bearer ${authToken}`;
    }
    return config;
  },
  (error: Error) => Promise.reject(error)
);

Storage Keys

All storage keys are defined in an enum for type safety:
export enum StorageKeys {
  AUTH_TOKEN = "auth_token",
  REFRESH_TOKEN = "refresh_token",
  USER = "user",
  DEVICE_KEY = "__device_key",
  WEB_CRYPTO_KEY = "__web_crypto_key",
}
Naming Convention:
  • Snake case for all keys
  • Prefix internal keys with __
  • Descriptive names

Platform Differences

FeatureiOS/AndroidWeb
General StorageMMKV (encrypted)MMKV (unencrypted)
Token StorageSecureStore (Keychain/Keystore)localStorage + Web Crypto
EncryptionHardware-backedAES-GCM 256-bit
PersistenceSurvives app restartSurvives browser restart
Size Limit~2MB practical~5-10MB per domain
PerformanceSynchronous, very fastAsynchronous encryption

Security Best Practices

  1. Use SecureStore for tokens: Never store auth tokens in MMKV or AsyncStorage
  2. Encrypt sensitive data: Use MMKV encryption key for native platforms
  3. Device-specific keys: Tie encryption to device ID
  4. Clear on logout: Remove all user data and tokens
  5. Validate storage: Check for data integrity on app launch
  6. Handle errors: Storage operations can fail (disk full, permissions)

Clearing Storage

// Logout and clear all data
export async function clearAllStorage() {
  // Clear tokens
  await Storage.deleteToken(StorageKeys.AUTH_TOKEN);
  await Storage.deleteToken(StorageKeys.REFRESH_TOKEN);
  
  // Clear user data
  Storage.removeItem(StorageKeys.USER);
  
  // Optionally clear device key (web only)
  if (Platform.OS === "web") {
    clearWebEncryptionKey();
  }
}

Build docs developers (and LLMs) love