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
isSupportedbefore 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