useLargeBlob

Store and retrieve data directly in passkey credentials using the WebAuthn Large Blob extension.

Use Cases: Offline authentication, encrypted vaults, credential metadata, and settings sync

Import

import { useLargeBlob } from '@heliorim/sdk-react';

Type Signature

interface LargeBlobState {
  isSupported: boolean;
  isReading: boolean;
  isWriting: boolean;
  data: Map<string, BlobData>;
  capacity: number; // bytes
  used: number; // bytes
  error: Error | null;
}

interface LargeBlobActions {
  write: (key: string, data: any, options?: WriteOptions) => Promise<void>;
  read: (key: string) => Promise<any>;
  remove: (key: string) => Promise<void>;
  clear: () => Promise<void>;
  list: () => Promise<BlobEntry[]>;
  getCapacity: () => Promise<number>;
  compress: (data: any) => Uint8Array;
  decompress: (data: Uint8Array) => any;
}

interface BlobData {
  key: string;
  value: any;
  size: number;
  created: number;
  modified: number;
  encrypted: boolean;
}

interface WriteOptions {
  encrypt?: boolean;
  compress?: boolean;
  ttl?: number; // Time to live in seconds
}

interface BlobEntry {
  key: string;
  size: number;
  created: number;
  modified: number;
}

type UseLargeBlobReturn = LargeBlobState & LargeBlobActions;

Basic Usage

function OfflineVault() {
  const {
    isSupported,
    write,
    read,
    remove,
    list,
    capacity,
    used
  } = useLargeBlob();

  const [vaultData, setVaultData] = useState(null);

  const saveToVault = async (data: any) => {
    try {
      // Store encrypted data in passkey
      await write('vault', data, {
        encrypt: true,
        compress: true
      });
      console.log('Data saved to passkey storage');
    } catch (err) {
      console.error('Failed to save to vault:', err);
    }
  };

  const loadFromVault = async () => {
    try {
      const data = await read('vault');
      setVaultData(data);
      console.log('Data loaded from passkey storage');
    } catch (err) {
      console.error('Failed to load from vault:', err);
    }
  };

  const clearVault = async () => {
    await remove('vault');
    setVaultData(null);
  };

  if (!isSupported) {
    return <div>Large Blob storage not supported on this device</div>;
  }

  return (
    <div>
      <h3>Offline Vault</h3>
      <div className="storage-info">
        <p>Storage Used: {Math.round(used / 1024)}KB / {Math.round(capacity / 1024)}KB</p>
        <div className="progress-bar">
          <div
            className="progress-fill"
            style={{ width: `${(used / capacity) * 100}%` }}
          />
        </div>
      </div>

      <button onClick={saveToVault}>Save to Vault</button>
      <button onClick={loadFromVault}>Load from Vault</button>
      <button onClick={clearVault}>Clear Vault</button>

      {vaultData && (
        <div>
          <h4>Vault Contents</h4>
          <pre>{JSON.stringify(vaultData, null, 2)}</pre>
        </div>
      )}
    </div>
  );
}

Use Cases

Encrypted Password Manager

function PasswordManager() {
  const { write, read, list } = useLargeBlob();
  const [passwords, setPasswords] = useState<PasswordEntry[]>([]);

  interface PasswordEntry {
    id: string;
    site: string;
    email: string;
    password: string;
    notes?: string;
  }

  const savePassword = async (entry: PasswordEntry) => {
    try {
      // Get existing passwords
      const existing = await read('passwords') ?? [];

      // Add new entry
      const updated = [...existing, { ...entry, id: crypto.randomUUID() }];

      // Save encrypted
      await write('passwords', updated, {
        encrypt: true,
        compress: true
      });

      setPasswords(updated);
    } catch (err) {
      console.error('Failed to save password:', err);
    }
  };

  const loadPasswords = async () => {
    try {
      const data = await read('passwords');
      setPasswords(data ?? []);
    } catch (err) {
      console.error('Failed to load passwords:', err);
    }
  };

  const deletePassword = async (id: string) => {
    const updated = passwords.filter(p => p.id !== id);
    await write('passwords', updated, {
      encrypt: true,
      compress: true
    });
    setPasswords(updated);
  };

  useEffect(() => {
    loadPasswords();
  }, []);

  return (
    <div className="password-manager">
      <h3>Secure Password Manager</h3>
      <p className="text-sm text-gray-600">
        Passwords stored encrypted in your passkey
      </p>

      <form onSubmit={(e) => {
        e.preventDefault();
        const formData = new FormData(e.target as HTMLFormElement);
        savePassword({
          id: '',
          site: formData.get('site') as string,
          email: formData.get('email') as string,
          password: formData.get('password') as string,
          notes: formData.get('notes') as string
        });
        (e.target as HTMLFormElement).reset();
      }}>
        <input name="site" placeholder="Website" required />
        <input name="email" placeholder="Email" required />
        <input name="password" type="password" placeholder="Password" required />
        <textarea name="notes" placeholder="Notes (optional)" />
        <button type="submit">Save Password</button>
      </form>

      <div className="password-list">
        {passwords.map(entry => (
          <div key={entry.id} className="password-entry">
            <h4>{entry.site}</h4>
            <p>Email: {entry.email}</p>
            <button onClick={() => deletePassword(entry.id)}>
              Delete
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

Offline Session Storage

function OfflineSession() {
  const { write, read } = useLargeBlob();
  const { session, user } = useAuth();

  const saveOfflineSession = async () => {
    if (!session ?? !user) return;

    const offlineData = {
      user: {
        id: user.id,
        email: user.email,
        name: user.name
      },
      sessionToken: session.token,
      expiresAt: session.expiresAt,
      permissions: session.permissions,
      cachedAt: Date.now()
    };

    await write('offline_session', offlineData, {
      encrypt: true,
      ttl: 7 * 24 * 60 * 60 // 7 days
    });

    console.log('Session cached for offline use');
  };

  const loadOfflineSession = async () => {
    try {
      const offlineData = await read('offline_session');

      if (!offlineData) {
        console.log('No offline session found');
        return null;
      }

      // Check if expired
      if (offlineData.expiresAt < Date.now()) {
        console.log('Offline session expired');
        await remove('offline_session');
        return null;
      }

      return offlineData;
    } catch (err) {
      console.error('Failed to load offline session:', err);
      return null;
    }
  };

  const verifyOfflineAccess = async () => {
    const offline = await loadOfflineSession();

    if (offline) {
      console.log('Offline access granted');
      // Use offline session data
      return true;
    }

    console.log('Offline access denied - authentication required');
    return false;
  };

  return (
    <div>
      <h3>Offline Access</h3>
      <button onClick={saveOfflineSession}>
        Enable Offline Mode
      </button>
      <button onClick={verifyOfflineAccess}>
        Test Offline Access
      </button>
    </div>
  );
}

Settings Synchronization

function SettingsSync() {
  const { write, read } = useLargeBlob();
  const [settings, setSettings] = useState({
    theme: 'light',
    language: 'en',
    notifications: true,
    autoLock: 5,
    biometrics: true
  });

  const saveSettings = async (newSettings: typeof settings) => {
    try {
      await write('user_settings', newSettings, {
        compress: true
      });
      setSettings(newSettings);
      console.log('Settings saved to passkey');
    } catch (err) {
      console.error('Failed to save settings:', err);
    }
  };

  const loadSettings = async () => {
    try {
      const saved = await read('user_settings');
      if (saved) {
        setSettings(saved);
        console.log('Settings loaded from passkey');
      }
    } catch (err) {
      console.error('Failed to load settings:', err);
    }
  };

  useEffect(() => {
    loadSettings();
  }, []);

  const updateSetting = (key: string, value: any) => {
    const updated = { ...settings, [key]: value };
    saveSettings(updated);
  };

  return (
    <div className="settings-sync">
      <h3>Settings (Synced via Passkey)</h3>

      <label>
        Theme:
        <select
          value={settings.theme}
          onChange={(e) => updateSetting('theme', e.target.value)}
        >
          <option value="light">Light</option>
          <option value="dark">Dark</option>
          <option value="auto">Auto</option>
        </select>
      </label>

      <label>
        <input
          type="checkbox"
          checked={settings.notifications}
          onChange={(e) => updateSetting('notifications', e.target.checked)}
        />
        Enable Notifications
      </label>

      <label>
        Auto-lock after (minutes):
        <input
          type="number"
          value={settings.autoLock}
          onChange={(e) => updateSetting('autoLock', parseInt(e.target.value))}
        />
      </label>

      <label>
        <input
          type="checkbox"
          checked={settings.biometrics}
          onChange={(e) => updateSetting('biometrics', e.target.checked)}
        />
        Use Biometrics
      </label>
    </div>
  );
}

Storage Management

Capacity Monitoring

function StorageMonitor() {
  const { capacity, used, list, remove, getCapacity } = useLargeBlob();
  const [entries, setEntries] = useState<BlobEntry[]>([]);

  useEffect(() => {
    loadEntries();
  }, []);

  const loadEntries = async () => {
    const items = await list();
    setEntries(items.sort((a, b) => b.size - a.size));
  };

  const cleanupOldData = async () => {
    const items = await list();
    const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);

    for (const item of items) {
      if (item.modified < thirtyDaysAgo) {
        await remove(item.key);
      }
    }

    await loadEntries();
  };

  const percentUsed = (used / capacity) * 100;

  return (
    <div className="storage-monitor">
      <h3>Storage Usage</h3>

      <div className="usage-chart">
        <div className="usage-bar">
          <div
            className="usage-fill"
            style={{
              width: `${percentUsed}%`,
              backgroundColor: percentUsed > 80 ? '#ef4444' :
                             percentUsed > 60 ? '#f59e0b' :
                             '#10b981'
            }}
          />
        </div>
        <p>{Math.round(used / 1024)}KB / {Math.round(capacity / 1024)}KB used</p>
      </div>

      <div className="storage-entries">
        <h4>Stored Items</h4>
        <table>
          <thead>
            <tr>
              <th>Key</th>
              <th>Size</th>
              <th>Modified</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {entries.map(entry => (
              <tr key={entry.key}>
                <td>{entry.key}</td>
                <td>{Math.round(entry.size / 1024)}KB</td>
                <td>{new Date(entry.modified).toLocaleDateString()}</td>
                <td>
                  <button onClick={() => remove(entry.key).then(loadEntries)}>
                    Delete
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <button onClick={cleanupOldData}>
        Clean Up Old Data (30+ days)
      </button>
    </div>
  );
}

Data Compression

function CompressionExample() {
  const { write, read, compress, decompress } = useLargeBlob();

  const demonstrateCompression = async () => {
    const largeData = {
      // Large JSON object
      items: Array.from({ length: 1000 }, (_, i) => ({
        id: i,
        name: `Item ${i}`,
        description: 'Sample item metadata for compression demonstration',
        metadata: {
          created: Date.now(),
          tags: ['tag1', 'tag2', 'tag3']
        }
      }))
    };

    // Manual compression
    const compressed = compress(largeData);
    console.log('Original size:', JSON.stringify(largeData).length);
    console.log('Compressed size:', compressed.length);
    console.log('Compression ratio:', (compressed.length / JSON.stringify(largeData).length * 100).toFixed(2) + '%');

    // Auto-compression when writing
    await write('large_dataset', largeData, {
      compress: true
    });

    // Reading automatically decompresses
    const restored = await read('large_dataset');
    console.log('Data restored:', restored.items.length, 'items');
  };

  return (
    <button onClick={demonstrateCompression}>
      Test Compression
    </button>
  );
}

Browser Compatibility

Large Blob Extension Support:

  • Chrome/Edge 109+ (Full support)
  • Safari 16.4+ (Limited support)
  • Firefox: Not yet supported
  • Storage limit: ~64KB per credential
  • Platform authenticators only (not security keys)

Best Practices

✓ DO

  • Always check isSupported before use
  • Encrypt sensitive data before storage
  • Compress large JSON objects
  • Monitor storage capacity
  • Implement TTL for temporary data
  • Handle storage errors gracefully

✗ DON'T

  • Store unencrypted sensitive data
  • Exceed 64KB storage limit
  • Rely on it as primary storage
  • Assume cross-device sync
  • Store frequently changing data

Related Hooks